From 213d1028e238a564eb3814a7d57276e9d5f9d0b0 Mon Sep 17 00:00:00 2001 From: Nihilazo Date: Sat, 6 Mar 2021 20:06:27 +0000 Subject: [PATCH] initial commit --- Makefile | 8 +++ go.mod | 3 + go.sum | 5 ++ main.go | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++ main_test.go | 20 ++++++ 5 files changed, 219 insertions(+) create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 main_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5170f0d --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +all: thebutton update + +thebutton: main.go + CGO_ENABLED=0 go build + +update: thebutton + scp -r thebutton nihilazo@tilde.town:~/thebutton + @echo "Done" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..73f9d72 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module tildegit.org/nihilazo/thebutton + +go 1.14 \ No newline at end of file diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fbef1aa --- /dev/null +++ b/go.sum @@ -0,0 +1,5 @@ +github.com/thoj/go-ircevent v0.0.0-20190807115034-8e7ce4b5a1eb h1:EavwSqheIJl3nb91HhkL73DwnT2Fk8W3yM7T7TuLZvA= +github.com/thoj/go-ircevent v0.0.0-20190807115034-8e7ce4b5a1eb/go.mod h1:I0ZT9x8wStY6VOxtNOrLpnDURFs7HS0z1e1vhuKUEVc= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7408293 --- /dev/null +++ b/main.go @@ -0,0 +1,183 @@ +package main + +/* + this is the source code for thebuttonbot, an IRC bot that plays the game "the button" on the tilde.town IRC network. + the game is played like this: + the bot maintains a counter. At a random interval, the counter ticks down. + at any point, any person can press the button. This resets the counter. This gives the player points. You get more points the lower the counter. + If the counter hits zero, the button is frozen and nobody can press it any more. + + firstly, we need to import all the libraries we need. + we need fmt for debugging. We need net for connecting to IRC. We need bufio for reading from the network socket we create to connect to IRC with. We need strings for splitting and testing strings and regexp to help parse messages. +*/ +import ( + "bufio" + "fmt" + "net" + "regexp" + "strings" +) + +/* + as we'll be referencing them a lot later, we should create some constants to store things like the network address and our IRC nick/user name. This is set up for connecting to tilde.town from localhost. +*/ +const ( + nick string = "thebuttonbot" + user string = "thebuttonbot" + realname string = "thebuttonbot" + channel string = "#thebutton" + server string = "localhost:6667" +) + +// IRCMessage is a type that contains the contents of an incoming IRC message. +// I'm only using this for message parsing, not construction, as I'm only ever constructing a small subset of IRC messages. +// [@Tags] [:SourceNick!SourceUser@SourceAddress] +type IRCMessage struct { + Tags string // Not parsing tags further than I need to right now + SourceNick string + SourceUser string + SourceHost string + Command string + Parameters []string +} + +/* +function parseMessage parses an IRC message, and returns an IRCMessage. +It assumes it's getting well-formed messages. +reference: modern.ircdocs.horse/index.html#messages +*/ +func parseMessage(s string) IRCMessage { + // create an empty message object we're writing into. + var m IRCMessage + // split on spaces, as everything in the message is delimited by them + parts := strings.Split(s, " ") + for index, item := range parts { + // tags start with an @, and can only occur as the first item in a message. + if index == 0 && strings.HasPrefix(item, "@") { + m.Tags = strings.TrimPrefix(item, "@") + // source can be either the first or second item in a message, depending on if there are tags first. + } else if (m.Tags != "" && index == 1 || m.Tags == "" && index == 0) && strings.HasPrefix(item, ":") { + // TODO tidy this up + // set the source regexp. Matches :nick!user@host or :host + sourceRegexp, _ := regexp.Compile(":((.+)!(.+)@)?(.+)") + // use it + matches := sourceRegexp.FindStringSubmatch(item) + // if the length of matches is longer than 2, we matched a nick and user. Otherwise we only matched a host. + if len(matches) > 3 { + m.SourceNick = matches[2] + m.SourceUser = matches[3] + m.SourceHost = matches[4] + } else { + m.SourceHost = matches[1] + } + // if we don't already have a command and the thing doesn't match otherwise, it's a command. + } else if m.Command == "" { + m.Command = item + // if nothing else is true, it's a parameter to the command. + } else { + m.Parameters = append(m.Parameters, item) + } + + } + return m + +} + +// sendMessage sends a message using connection conn on IRC channel c with content s +func sendMessage(conn *net.Conn, c string, s string) { + fmt.Fprintf(*conn, "PRIVMSG %s :%s\r\n", c, s) +} + +func main() { + // open a connection to the IRC network over a TCP connection. + conn, err := net.Dial("tcp", server) + if err != nil { + panic(err) + } + // create a buffered reader to read from the connection. + reader := bufio.NewReader(conn) + // create a scanner that will let us easily read single messages from IRC. + // as IRC happens in lines, we can just scan lines. + scanner := bufio.NewScanner(reader) + /* + Upon connecting to IRC, we need to set our username with the USER command, and nickname with the NICK command. + They have the following structures: + ``` + USER [username] 0 * :[realname] + NICK [nickname] + ``` + Then we connect to the channel and send a message as a test, with JOIN and PRIVMSG. + ``` + JOIN [channel] + PRIVMSG [destination] :[content] + ``` + We can write these to the connection with fmt.Fprintf. + ref: https://modern.ircdocs.horse + */ + fmt.Fprintf(conn, "USER %s 0 * :%s\r\n", user, realname) + fmt.Fprintf(conn, "NICK %s\r\n", nick) + fmt.Fprintf(conn, "JOIN %s\r\n", channel) + + /* + loop every time there is a new line sent over the connection (a new IRC message/command). + We can get the content of it with scanner.Text(). + */ + for scanner.Scan() { + t := scanner.Text() + // parse the message using the message parsing function. + msg := parseMessage(t) + /* + Now we actually deal with the IRC messages! + First, PING. The server will send a PING sometimes to make sure the client is still awake. + When we get a PING, we need to respond with a PONG: + Server: PING [parameter] + Client: PONG [parameter] + */ + // Print the message for debugging purposes + fmt.Println(msg) + if msg.Command == "PING" { + fmt.Fprintf(conn, "PONG %s\r\n", msg.Parameters[0]) + } + /* + Next, actual messages from people. These use the PRIVMSG command. + PRIVMSG from :content + because of my dumb message parser, the contents are spread throughout the parameters array. + However, we only need to look at the first bit, because we're just reading some set commands: + ,scores: prints the scoreboard (when it's implemented) + ,press: press the button, resetting the countdown (when that's implemented) + ,ping: sends a "pong" as a PRIVMSG to the channel. + ,countdown: sends the current countdown value + ,help: prints the things you can do + these only matter if they're sent in the bot's channel. + scores might get an argument sometime idk + TODO implement all these + */ + if msg.Command == "PRIVMSG" { + msgChannel := msg.Parameters[0] + // msgSender := msg.SourceNick + msgContent := strings.TrimPrefix(msg.Parameters[1], ":") + fmt.Println(msgContent) + if msgChannel == channel { + if msgContent == ",ping" { + sendMessage(&conn, channel, "Pong!\r\n") + } else if msgContent == ",scores" { + // TODO implement actual game + sendMessage(&conn, channel, "Not implemented yet") + } else if msgContent == ",press" { + sendMessage(&conn, channel, "Not implemented yet") + } else if msgContent == ",countdown" { + sendMessage(&conn, channel, "Not implemented yet") + } else if msgContent == ",help" { + sendMessage(&conn, channel, ",scores: prints current scores") + sendMessage(&conn, channel, ",press: press the button! resets the countdown, adds to your score. The lower the count, the higher your score!") + sendMessage(&conn, channel, ",countdown: prints current countdown position") + } + } + } + + } + // If the loop is broken, there might be a scanner error. If so, just panic for now. + if err := scanner.Err(); err != nil { + panic(err) + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..cb7831c --- /dev/null +++ b/main_test.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "testing" +) + +var strs []string = []string{":irc.example.com CAP LS * :multi-prefix extended-join sasl", + "@id=234AB :nihilazo!nihilazo@localhost PRIVMSG #thebutton :This is content", + ":dan!d@localhost PRIVMSG #chan :Hey what's up!", + "CAP REQ :sasl", +} + +// TestParseMessage parses some messages and prints the results. +// TODO make these into cases +func TestParseMessage(t *testing.T) { + for _, s := range strs { + fmt.Printf("#%v", parseMessage(s)) + } +}