Browse Source

Add IRC bot plusone

TODO:
    Write README file
Remi Reuvekamp 3 years ago
commit
d85cbc9607
4 changed files with 370 additions and 0 deletions
  1. 1
    0
      .gitignore
  2. 68
    0
      plusone/config.go
  3. 209
    0
      plusone/irc.go
  4. 92
    0
      plusone/main.go

+ 1
- 0
.gitignore View File

@@ -0,0 +1 @@
1
+config.json

+ 68
- 0
plusone/config.go View File

@@ -0,0 +1,68 @@
1
+package main
2
+
3
+import (
4
+	"encoding/json"
5
+	"io/ioutil"
6
+)
7
+
8
+type config struct {
9
+	Server struct {
10
+		Host string `json:"host"`
11
+		Port int    `json:"port"`
12
+	} `json:"server"`
13
+
14
+	User struct {
15
+		NickName string `json:"nickname"`
16
+		UserName string `json:"username"`
17
+		RealName string `json:"realname"`
18
+		Password string `json:"password"`
19
+	} `json:"user"`
20
+
21
+	Channels  []string `json:"channels"`
22
+	RespondIn int      `json:"respondOnceIn"`
23
+
24
+	Response struct {
25
+		Responses  []string `json:"responses"`
26
+		BaseSleep  int      `json:"baseSleepDurationInMilliseconds"`
27
+		ExtraSleep int      `json:"extraSleepDurationInMilliSecondsPerCharacter"`
28
+	} `json:"response"`
29
+}
30
+
31
+// loadConfig parses the configuration file with the given filename and creates one
32
+// with empty values it if there is none with the given name.
33
+// The returned boolean is true if the config file didn't exist yet but was created.
34
+func loadConfig(filename string) (config, bool, error) {
35
+	var c config
36
+
37
+	data, err := ioutil.ReadFile(filename)
38
+
39
+	if err != nil {
40
+		// File with given filename doesn't exist. Create it.
41
+
42
+		// Set default
43
+		c.Channels = []string{}
44
+		c.Response.Responses = []string{}
45
+		c.Response.BaseSleep = 5000
46
+		c.Response.ExtraSleep = 65
47
+
48
+		data, err := json.MarshalIndent(&c, "", "\t")
49
+		if err != nil {
50
+			return c, false, err
51
+		}
52
+
53
+		err = ioutil.WriteFile(filename, data, 0644)
54
+		if err != nil {
55
+			return c, false, err
56
+		}
57
+
58
+		return c, true, nil
59
+	}
60
+
61
+	err = json.Unmarshal(data, &c)
62
+
63
+	if err != nil {
64
+		return c, false, err
65
+	}
66
+
67
+	return c, false, nil
68
+}

+ 209
- 0
plusone/irc.go View File

@@ -0,0 +1,209 @@
1
+package main
2
+
3
+import (
4
+	"bufio"
5
+	"fmt"
6
+	"log"
7
+	"net"
8
+	"strconv"
9
+	"strings"
10
+	"time"
11
+)
12
+
13
+type connection struct {
14
+	conn net.Conn
15
+
16
+	// write: messages that will be sent to the IRC server.
17
+	write chan string
18
+
19
+	// msg is were incoming PRIVMSG's end up.
20
+	msg chan message
21
+
22
+	// quit is used to signal there has been an unrecoverable error with the IRC
23
+	// connection.
24
+	quit chan struct{}
25
+
26
+	// log is used to log incoming messages from the IRC server.
27
+	log *log.Logger
28
+
29
+	// logErr is used to log errors that occurred.
30
+	logErr *log.Logger
31
+
32
+	me user
33
+}
34
+
35
+type message struct {
36
+	msg       string
37
+	channel   string
38
+	user      user
39
+	isPrivate bool
40
+}
41
+
42
+type user struct {
43
+	nick string
44
+	user string
45
+	host string
46
+}
47
+
48
+// connect opens a TCP connection to the given IRC server and sets up the goroutines
49
+// that handle writing and reading from it. The given loggers are used to log
50
+// incoming IRC messages and possible errors.
51
+func connect(server string, port int, log, logErr *log.Logger) (c *connection, err error) {
52
+	c = &connection{
53
+		write: make(chan string, 5), // Buffer, just in case.
54
+		msg:   make(chan message, 5),
55
+
56
+		log:    log,
57
+		logErr: logErr,
58
+	}
59
+
60
+	c.conn, err = net.Dial("tcp", server+":"+strconv.Itoa(port))
61
+	if err != nil {
62
+		return nil, err
63
+	}
64
+
65
+	go c.reader()
66
+	go c.writer()
67
+
68
+	return c, nil
69
+}
70
+
71
+func (c *connection) reader() {
72
+	r := bufio.NewReader(c.conn)
73
+	for {
74
+		line, err := r.ReadString(byte('\n'))
75
+		if err != nil {
76
+			c.logErr.Println(err)
77
+			c.quit <- struct{}{}
78
+		}
79
+
80
+		if strings.HasPrefix(line, "PING") {
81
+			code := strings.TrimPrefix(line, "PING")
82
+			c.write <- "PONG" + code
83
+			continue
84
+		}
85
+
86
+		line = strings.TrimSuffix(line, "\r\n")
87
+
88
+		if strings.Contains(line, " PRIVMSG ") {
89
+			msg, err := c.parsePrivMsg(line)
90
+			if err != nil {
91
+				c.logErr.Println(err)
92
+				continue
93
+			}
94
+
95
+			c.log.Println(msg)
96
+
97
+			c.msg <- msg
98
+			continue
99
+		}
100
+
101
+		c.log.Println(line)
102
+	}
103
+}
104
+
105
+func (c *connection) writer() {
106
+	w := bufio.NewWriter(c.conn)
107
+	for {
108
+		line := <-c.write
109
+
110
+		if !strings.HasPrefix(line, "\r\n") {
111
+			line += "\r\n"
112
+		}
113
+
114
+		_, err := w.WriteString(line)
115
+		if err != nil {
116
+			c.logErr.Println(err)
117
+			continue
118
+		}
119
+		w.Flush()
120
+	}
121
+}
122
+
123
+// authenticate requests the IRC server to use the given nickname, username,
124
+// realname and, optionally, password.
125
+func (c *connection) authenticate(nick, user, realname, pass string) {
126
+	if len(pass) > 0 {
127
+		c.write <- "PASS " + pass
128
+	}
129
+	c.write <- "NICK " + nick
130
+	c.write <- "USER " + user + " * * :" + realname
131
+
132
+	c.me.nick = nick
133
+	c.me.user = user
134
+}
135
+
136
+// join requests the IRC server to join the given channel.
137
+func (c *connection) join(channel string) {
138
+	c.write <- "JOIN " + channel
139
+}
140
+
141
+// message requests the IRC server to sent the given message to the given recipients.
142
+func (c *connection) message(recipients []string, message string) {
143
+	msgs := strings.Split(message, "\n")
144
+	for _, msg := range msgs {
145
+		split := strings.Split(msg, "|:|")
146
+		if len(split) > 1 {
147
+			if sleep, err := strconv.Atoi(split[0]); err == nil {
148
+				time.Sleep(time.Duration(sleep) * time.Millisecond)
149
+				msg = split[1]
150
+			}
151
+		}
152
+		c.write <- "PRIVMSG " + strings.Join(recipients, ",") + " :" + msg
153
+	}
154
+}
155
+
156
+func (c *connection) parsePrivMsg(line string) (msg message, err error) {
157
+	// For reference:
158
+	//:remi!~remi@novaember.com PRIVMSG #remi :Hey
159
+
160
+	errMsg := "Wut? parsePrivMsg %q split length is less than %d? Part: %q Line: " + line
161
+
162
+	line = strings.TrimPrefix(line, ":")
163
+
164
+	split := strings.Split(line, " PRIVMSG ")
165
+	if len(split) < 2 {
166
+		return msg, fmt.Errorf(errMsg, ":", 2, "")
167
+	}
168
+
169
+	userInfo := split[0]
170
+	msgInfo := split[1]
171
+
172
+	userInfos := strings.Split(userInfo, "!~")
173
+	if len(userInfos) < 2 {
174
+		return msg, fmt.Errorf(errMsg, "!~", 2, userInfo)
175
+	}
176
+
177
+	nick := userInfos[0]
178
+	hostInfo := userInfos[1]
179
+
180
+	msg.user.nick = nick
181
+
182
+	hostInfos := strings.Split(hostInfo, "@")
183
+	if len(hostInfos) < 2 {
184
+		return msg, fmt.Errorf(errMsg, "@", 2, hostInfo)
185
+	}
186
+
187
+	user := hostInfos[0]
188
+	host := hostInfos[1]
189
+
190
+	msg.user.user = user
191
+	msg.user.host = host
192
+
193
+	msgInfos := strings.Split(msgInfo, " :")
194
+	if len(msgInfos) < 2 {
195
+		return msg, fmt.Errorf(errMsg, " :", 2, msgInfo)
196
+	}
197
+
198
+	channel := msgInfos[0]
199
+	message := msgInfos[1]
200
+
201
+	msg.channel = channel
202
+	msg.msg = message
203
+
204
+	if msg.channel == c.me.nick {
205
+		msg.isPrivate = true
206
+	}
207
+
208
+	return
209
+}

+ 92
- 0
plusone/main.go View File

@@ -0,0 +1,92 @@
1
+package main
2
+
3
+import (
4
+	"fmt"
5
+	"log"
6
+	"math/rand"
7
+	"os"
8
+	"time"
9
+)
10
+
11
+func main() {
12
+	// Loading configuration from file.
13
+	cfg, created, err := loadConfig("config.json")
14
+	if err != nil {
15
+		log.Fatal(err)
16
+	}
17
+
18
+	if created {
19
+		fmt.Println("Configuration file created. Please fill it in.")
20
+		return
21
+	}
22
+
23
+	// Connect to the IRC server.
24
+	c, err := connect(cfg.Server.Host, cfg.Server.Port,
25
+		log.New(os.Stdout, "irc: ", log.Ldate|log.Ltime),
26
+		log.New(os.Stderr, "ircerr: ", log.Ldate|log.Ltime))
27
+
28
+	if err != nil {
29
+		log.Fatal(err)
30
+	}
31
+
32
+	time.Sleep(time.Second)
33
+
34
+	// Authenticate to the IRC server.
35
+	c.authenticate(cfg.User.NickName, cfg.User.UserName, cfg.User.RealName, cfg.User.Password)
36
+
37
+	// Join channels.
38
+	for _, ch := range cfg.Channels {
39
+		time.Sleep(500 * time.Millisecond)
40
+		c.join(ch)
41
+	}
42
+
43
+	rand.Seed(time.Now().UnixNano())
44
+
45
+	// Main loop.
46
+	// Wait for incomming PRIVMSG's or stop the application on request.
47
+	for {
48
+		var msg message
49
+		select {
50
+		case msg = <-c.msg:
51
+		case <-c.quit:
52
+			return
53
+		}
54
+
55
+		if msg.isPrivate {
56
+			log.Println("Is private")
57
+			continue
58
+		}
59
+
60
+		var found bool
61
+		for _, ch := range cfg.Channels {
62
+			if msg.channel == ch {
63
+				found = true
64
+				break
65
+			}
66
+		}
67
+
68
+		if !found {
69
+			continue
70
+		}
71
+
72
+		if rand.Intn(cfg.RespondIn) != 1 {
73
+			continue
74
+		}
75
+
76
+		go func() {
77
+			dur := cfg.Response.BaseSleep + (cfg.Response.ExtraSleep * len(msg.msg))
78
+
79
+			log.Println("It is time:", dur/1000)
80
+
81
+			time.Sleep(time.Duration(dur) * time.Millisecond)
82
+
83
+			// Determine message
84
+			var i int
85
+			if len(cfg.Response.Responses) > 1 {
86
+				i = rand.Intn(len(cfg.Response.Responses))
87
+			}
88
+
89
+			c.message([]string{msg.channel}, cfg.Response.Responses[i])
90
+		}()
91
+	}
92
+}

Loading…
Cancel
Save