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