initial commit

This commit is contained in:
Nico 2021-03-06 20:06:27 +00:00
commit 213d1028e2
5 changed files with 219 additions and 0 deletions

8
Makefile Normal file
View File

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

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module tildegit.org/nihilazo/thebutton
go 1.14

5
go.sum Normal file
View File

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

183
main.go Normal file
View File

@ -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] <Command> <Parameters>
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)
}
}

20
main_test.go Normal file
View File

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