@ -0,0 +1,8 @@ | |||||
# Default ignored files | |||||
/shelf/ | |||||
/workspace.xml | |||||
# Editor-based HTTP Client requests | |||||
/httpRequests/ | |||||
# Datasource local storage ignored files | |||||
/dataSources/ | |||||
/dataSources.local.xml |
@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/twitchbot.iml" filepath="$PROJECT_DIR$/.idea/twitchbot.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="WEB_MODULE" version="4"> | |||||
<component name="Go" enabled="true" /> | |||||
<component name="NewModuleRootManager"> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@ -0,0 +1,3 @@ | |||||
module twitch | |||||
go 1.17 |
@ -0,0 +1,39 @@ | |||||
package main | |||||
import ( | |||||
"encoding/json" | |||||
"fmt" | |||||
"io/ioutil" | |||||
"log" | |||||
"twitch/twitchbot" | |||||
) | |||||
type Config struct { | |||||
Bot struct { | |||||
Token string `json:"token"` | |||||
Nick string `json:"nick"` | |||||
} `json:"bot"` | |||||
} | |||||
func main() { | |||||
var config Config | |||||
data, _ := ioutil.ReadFile("config.json") | |||||
err := json.Unmarshal(data, &config) | |||||
if err != nil { | |||||
log.Panicln(err) | |||||
} | |||||
bot := twitchbot.NewBot(config.Bot.Token, config.Bot.Nick, []string{"witer33"}) | |||||
bot.OnMessage(func(bot *twitchbot.Bot, message *twitchbot.Message) { | |||||
fmt.Println(message) | |||||
if message.Message == "!ping" { | |||||
message.Reply("pong") | |||||
message.Delete() | |||||
} | |||||
}) | |||||
bot.Run() | |||||
} |
@ -0,0 +1,184 @@ | |||||
package twitchbot | |||||
import "log" | |||||
type EventHandler struct { | |||||
messageHandlers []func(*Bot, *Message) | |||||
} | |||||
type Bot struct { | |||||
client *Client | |||||
host string | |||||
onLogin func(*Bot) | |||||
events EventHandler | |||||
channels []string | |||||
} | |||||
type User struct { | |||||
ID string | |||||
Name string | |||||
} | |||||
type Message struct { | |||||
ID string | |||||
Channel string | |||||
User *User | |||||
Message string | |||||
Bot *Bot | |||||
} | |||||
func ParseMessage(command *Command, bot *Bot) *Message { | |||||
return &Message{ | |||||
ID: command.Tags["id"], | |||||
Channel: command.Args[0][1:], | |||||
User: &User{ID: command.Tags["user-id"], Name: command.Tags["display-name"]}, | |||||
Message: command.Suffix, | |||||
Bot: bot, | |||||
} | |||||
} | |||||
func NewBot(token string, nick string, channels []string) *Bot { | |||||
client := Client{Token: token, Nick: nick} | |||||
return &Bot{client: &client, host: "irc.chat.twitch.tv:6667", events: EventHandler{}, channels: channels} | |||||
} | |||||
func (message *Message) Reply(msg string) error { | |||||
err := message.Bot.SendMessage(&Message{Message: msg, Channel: message.Channel}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (message *Message) Delete() error { | |||||
err := message.Bot.DeleteMessage(&Message{ID: message.ID, Channel: message.Channel}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (message *Message) Ban() error { | |||||
err := message.Bot.BanUser(message.Channel, message.User.Name) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (event *EventHandler) configure(bot *Bot) { | |||||
bot.client.AddHandler("PRIVMSG", func(command *Command) bool { | |||||
message := ParseMessage(command, bot) | |||||
for _, handler := range event.messageHandlers { | |||||
handler(bot, message) | |||||
} | |||||
return true | |||||
}) | |||||
} | |||||
func (bot *Bot) OnLogin(f func(*Bot)) { | |||||
bot.onLogin = f | |||||
} | |||||
func (bot *Bot) SendMessage(message *Message) error { | |||||
err := bot.client.Send(&Command{Command: "PRIVMSG", Args: []string{"#" + message.Channel}, Suffix: message.Message}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (bot *Bot) DeleteMessage(message *Message) error { | |||||
err := bot.SendMessage(&Message{Channel: message.Channel, Message: "/delete " + message.ID}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (bot *Bot) Join(channel string) error { | |||||
err := bot.client.Join("#" + channel) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (bot *Bot) BanUser(channel string, user string) error { | |||||
err := bot.SendMessage(&Message{Channel: channel, Message: "/ban " + user}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (bot *Bot) OnMessage(f func(*Bot, *Message)) { | |||||
bot.events.messageHandlers = append(bot.events.messageHandlers, f) | |||||
} | |||||
func (bot *Bot) GetClient() *Client { | |||||
return bot.client | |||||
} | |||||
func (bot *Bot) Run() { | |||||
for { | |||||
err := bot.Start() | |||||
if err != nil { | |||||
log.Printf("Bot error: %s\n", err) | |||||
} | |||||
} | |||||
} | |||||
func (bot *Bot) Start() error { | |||||
err := bot.client.Connect(bot.host) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer bot.client.Close() | |||||
err = bot.client.Connect(bot.host) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = bot.client.Auth() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
bot.events.configure(bot) | |||||
bot.client.AddHandler("PING", func(command *Command) bool { | |||||
err := bot.client.Send(&Command{Command: "PONG", Suffix: "tmi.twitch.tv"}) | |||||
if err != nil { | |||||
return false | |||||
} | |||||
return true | |||||
}) | |||||
bot.client.AddHandler("376", func(command *Command) bool { | |||||
err = bot.GetClient().CapReq("twitch.tv/tags twitch.tv/commands") | |||||
if err != nil { | |||||
return false | |||||
} | |||||
for _, channel := range bot.channels { | |||||
err = bot.Join(channel) | |||||
if err != nil { | |||||
return false | |||||
} | |||||
} | |||||
if bot.onLogin != nil { | |||||
bot.onLogin(bot) | |||||
} | |||||
return true | |||||
}) | |||||
err = bot.client.Handle() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} |
@ -0,0 +1,20 @@ | |||||
package twitchbot | |||||
import "strings" | |||||
func (c *Command) Build() string { | |||||
builder := strings.Builder{} | |||||
if c.Prefix != "" { | |||||
builder.WriteString(":" + c.Prefix + " ") | |||||
} | |||||
builder.WriteString(c.Command) | |||||
for _, arg := range c.Args { | |||||
if arg != "" { | |||||
builder.WriteString(" " + arg) | |||||
} | |||||
} | |||||
if c.Suffix != "" { | |||||
builder.WriteString(" :" + c.Suffix) | |||||
} | |||||
return builder.String() | |||||
} |
@ -0,0 +1,106 @@ | |||||
package twitchbot | |||||
import ( | |||||
"bufio" | |||||
"net" | |||||
"net/textproto" | |||||
) | |||||
type Client struct { | |||||
Token string | |||||
Nick string | |||||
conn net.Conn | |||||
writer *textproto.Writer | |||||
reader *textproto.Reader | |||||
handlers map[string][]func(*Command) bool | |||||
} | |||||
func (client *Client) Connect(host string) error { | |||||
conn, err := net.Dial("tcp", host) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
client.conn = conn | |||||
client.writer = textproto.NewWriter(bufio.NewWriter(conn)) | |||||
client.reader = textproto.NewReader(bufio.NewReader(conn)) | |||||
return nil | |||||
} | |||||
func (client *Client) Auth() error { | |||||
err := client.writer.PrintfLine("PASS %s", client.Token) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = client.writer.PrintfLine("NICK %s", client.Nick) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (client *Client) AddHandler(command string, f func(*Command) bool) { | |||||
if client.handlers == nil { | |||||
client.handlers = make(map[string][]func(*Command) bool) | |||||
} | |||||
handlers, ok := client.handlers[command] | |||||
if !ok { | |||||
client.handlers[command] = []func(*Command) bool{f} | |||||
} else { | |||||
client.handlers[command] = append(handlers, f) | |||||
} | |||||
} | |||||
func (client *Client) Send(command *Command) error { | |||||
err := client.writer.PrintfLine(command.Build()) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (client *Client) CapReq(cap string) error { | |||||
err := client.Send(&Command{ | |||||
Command: "CAP", | |||||
Args: []string{"REQ"}, | |||||
Suffix: cap, | |||||
}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (client *Client) Join(channel string) error { | |||||
err := client.Send(&Command{ | |||||
Command: "JOIN", | |||||
Args: []string{channel}, | |||||
}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (client *Client) Close() { | |||||
err := client.conn.Close() | |||||
if err != nil { | |||||
return | |||||
} | |||||
} | |||||
func (client *Client) Handle() error { | |||||
for { | |||||
packet, err := client.reader.ReadLine() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
command := ParsePacket(packet) | |||||
handlers := client.handlers[command.Command] | |||||
for _, handler := range handlers { | |||||
if !handler(command) { | |||||
return nil | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,84 @@ | |||||
package twitchbot | |||||
import ( | |||||
"strings" | |||||
) | |||||
type Command struct { | |||||
Tags map[string]string | |||||
Prefix string | |||||
Command string | |||||
Args []string | |||||
Suffix string | |||||
} | |||||
func (c *Command) String() string { | |||||
return "Prefix: " + c.Prefix + " Command: " + c.Command + " Args: " + strings.Join(c.Args, " ") + " Suffix: " + c.Suffix | |||||
} | |||||
func ReadString(reader *strings.Reader, until byte) string { | |||||
result := strings.Builder{} | |||||
char, ok := reader.ReadByte() | |||||
for ok == nil { | |||||
if char == until { | |||||
break | |||||
} | |||||
result.WriteByte(char) | |||||
char, ok = reader.ReadByte() | |||||
} | |||||
return result.String() | |||||
} | |||||
func ReadTags(reader *strings.Reader) map[string]string { | |||||
result := make(map[string]string) | |||||
for { | |||||
tag := ReadString(reader, '=') | |||||
if tag == "" { | |||||
break | |||||
} | |||||
value := ReadString(reader, ';') | |||||
result[tag] = value | |||||
} | |||||
return result | |||||
} | |||||
func ParsePacket(packet string) *Command { | |||||
reader := strings.NewReader(packet) | |||||
command := Command{} | |||||
args := make([]string, 15) | |||||
arg := 0 | |||||
char, ok := reader.ReadByte() | |||||
for ok == nil { | |||||
if char == ':' && command.Prefix == "" && command.Command == "" { | |||||
command.Prefix = ReadString(reader, ' ') | |||||
} else if char == '@' && command.Tags == nil { | |||||
command.Tags = ReadTags(strings.NewReader(ReadString(reader, ' '))) | |||||
} else if command.Command == "" { | |||||
_, err := reader.Seek(-1, 1) | |||||
if err != nil { | |||||
continue | |||||
} | |||||
command.Command = ReadString(reader, ' ') | |||||
} else if char == ':' { | |||||
command.Suffix = ReadString(reader, '\r') | |||||
} else { | |||||
_, err := reader.Seek(-1, 1) | |||||
if err != nil { | |||||
continue | |||||
} | |||||
args[arg] = ReadString(reader, ' ') | |||||
arg++ | |||||
} | |||||
char, ok = reader.ReadByte() | |||||
} | |||||
command.Args = args | |||||
return &command | |||||
} |