From e1e269295dc4f726274b5b3b612e95d19fc1b6a6 Mon Sep 17 00:00:00 2001 From: witer33 Date: Mon, 3 Jan 2022 16:34:22 +0100 Subject: [PATCH] Initial commit --- .idea/.gitignore | 8 ++ .idea/modules.xml | 8 ++ .idea/twitch.iml | 9 +++ go.mod | 3 + test.go | 39 +++++++++ twitchbot/bot.go | 184 +++++++++++++++++++++++++++++++++++++++++++ twitchbot/builder.go | 20 +++++ twitchbot/client.go | 106 +++++++++++++++++++++++++ twitchbot/parser.go | 84 ++++++++++++++++++++ 9 files changed, 461 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/twitch.iml create mode 100644 go.mod create mode 100644 test.go create mode 100644 twitchbot/bot.go create mode 100644 twitchbot/builder.go create mode 100644 twitchbot/client.go create mode 100644 twitchbot/parser.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ea9acf7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/twitch.iml b/.idea/twitch.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/twitch.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..11f7040 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module twitch + +go 1.17 diff --git a/test.go b/test.go new file mode 100644 index 0000000..c697357 --- /dev/null +++ b/test.go @@ -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() +} diff --git a/twitchbot/bot.go b/twitchbot/bot.go new file mode 100644 index 0000000..b1db99a --- /dev/null +++ b/twitchbot/bot.go @@ -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 +} diff --git a/twitchbot/builder.go b/twitchbot/builder.go new file mode 100644 index 0000000..cd9d155 --- /dev/null +++ b/twitchbot/builder.go @@ -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() +} diff --git a/twitchbot/client.go b/twitchbot/client.go new file mode 100644 index 0000000..d2aa7f8 --- /dev/null +++ b/twitchbot/client.go @@ -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 + } + } + } +} diff --git a/twitchbot/parser.go b/twitchbot/parser.go new file mode 100644 index 0000000..b17e457 --- /dev/null +++ b/twitchbot/parser.go @@ -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 +}