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
+}