blob: 88467167b1716a899127f49febe0f0d983c015f3 [file] [log] [blame]
Sergiusz Bazanskia8854882020-01-05 00:34:38 +01001package irc
2
3import (
4 "context"
5 "fmt"
6 "net"
Sergiusz Bazanski83e26902020-01-23 14:18:25 +01007 "regexp"
Sergiusz Bazanskia8854882020-01-05 00:34:38 +01008 "strings"
9 "sync"
10 "sync/atomic"
11 "time"
12
13 "github.com/golang/glog"
14 irc "gopkg.in/irc.v3"
15)
16
17// ircconn is a connection to IRC as a given user.
18type ircconn struct {
19 // server to connect to
20 server string
21 // channel to join
22 channel string
23 // 'native' name of this connection.
24 user string
25
26 // Event Handler, usually a Manager
27 eventHandler func(e *event)
28
29 // TCP connection to IRC
30 conn net.Conn
31 // IRC client
32 irc *irc.Client
33
34 /// Fields used by the manager - do not access from ircconn.
35 // last time this connection was used
36 last time.Time
37 // is primary source of IRC data
38 receiver bool
39 // only exists to be a receiver
40 backup bool
41 // iq is the IRC Queue of IRC messages, populated by the IRC client and
42 // read by the connection.
43 iq chan *irc.Message
44 // sq is the Say Queue of controlMessages, populated by the Manager and
45 // read by the connection (and passed onto IRC)
46 sq chan *controlMessage
47 // eq is the Evict Queue, used by the manager to signal that a connection
48 // should die.
49 eq chan struct{}
50
51 // connected is a flag (via sync/atomic) that is used to signal to the
52 // manager that this connection is up and healthy.
53 connected int64
54}
55
Michal Zagorski5b1aa132020-03-01 17:05:05 +010056var reIRCNick = regexp.MustCompile(`[^A-Za-z0-9]`)
Sergiusz Bazanski83e26902020-01-23 14:18:25 +010057
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010058// Say is called by the Manager when a message should be sent out by the
59// connection.
60func (i *ircconn) Say(msg *controlMessage) {
61 i.sq <- msg
62}
63
64// Evict is called by the Manager when a connection should die.
65func (i *ircconn) Evict() {
66 close(i.eq)
67}
68
69// ircMessage is a message received on IRC by a connection, sent over to the
70// Manager.
71type IRCMessage struct {
72 conn *ircconn
73 nick string
74 text string
75}
76
Michal Zagorski5b1aa132020-03-01 17:05:05 +010077func NewConn(server, channel, userTelegram string, backup bool, h func(e *event)) (*ircconn, error) {
78 // Generate IRC nick from username.
79 nick := reIRCNick.ReplaceAllString(userTelegram, "")
80 username := nick
81 if len(username) > 9 {
82 username = username[:9]
83 }
84 nick = strings.ToLower(nick)
85 if len(nick) > 13 {
86 nick = nick[:13]
87 }
88 if len(nick) == 0 {
89 glog.Errorf("Could not create IRC nick for %q", userTelegram)
90 nick = "wtf"
91 }
92 nick += "[t]"
93 glog.Infof("Connecting to IRC/%s/%s/%s as %s from %s...", server, channel, userTelegram, nick, username)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010094 conn, err := net.Dial("tcp", server)
95 if err != nil {
96 return nil, fmt.Errorf("Dial(_, %q): %v", server, err)
97 }
98
99 i := &ircconn{
100 server: server,
101 channel: channel,
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100102 user: userTelegram,
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100103
104 eventHandler: h,
105
106 conn: conn,
107 irc: nil,
108
109 last: time.Now(),
110 backup: backup,
111 receiver: backup,
112
113 iq: make(chan *irc.Message),
114 sq: make(chan *controlMessage),
115 eq: make(chan struct{}),
116
117 connected: int64(0),
118 }
119
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100120
121
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100122
123 // Configure IRC client to populate the IRC Queue.
124 config := irc.ClientConfig{
125 Nick: nick,
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100126 User: username,
127 Name: userTelegram,
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100128 Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
129 i.iq <- m
130 }),
131 }
132
133 i.irc = irc.NewClient(conn, config)
134 return i, nil
135}
136
137func (i *ircconn) Run(ctx context.Context) {
138 var wg sync.WaitGroup
139 wg.Add(2)
140
141 go func() {
142 i.loop(ctx)
143 wg.Done()
144 }()
145
146 go func() {
147 err := i.irc.RunContext(ctx)
148 if err != ctx.Err() {
149 glog.Errorf("IRC/%s/%s/%s exited: %v", i.server, i.channel, i.user, err)
150 i.conn.Close()
151 i.eventHandler(&event{
152 dead: &eventDead{i},
153 })
154 }
155 wg.Wait()
156 }()
157
158 wg.Wait()
159}
160
161// IsConnected returns whether a connection is fully alive and able to receive
162// messages.
163func (i *ircconn) IsConnected() bool {
164 return atomic.LoadInt64(&i.connected) > 0
165}
166
167// loop is the main loop of an IRC connection.
168// It synchronizes the Handler Queue, Say Queue and Evict Queue, parses
169func (i *ircconn) loop(ctx context.Context) {
170 sayqueue := []*controlMessage{}
171 connected := false
172 dead := false
173
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100174 die := func(err error) {
175 // drain queue of say messages...
176 for _, s := range sayqueue {
177 glog.Infof("IRC/%s/say: [drop] %q", i.user, s.message)
178 s.done <- err
179 }
180 sayqueue = []*controlMessage{}
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100181 dead = true
182 i.conn.Close()
183 go i.eventHandler(&event{
184 dead: &eventDead{i},
185 })
186 }
187 msg := func(s *controlMessage) {
188 lines := strings.Split(s.message, "\n")
189 for _, l := range lines {
190 l = strings.TrimSpace(l)
191 if l == "" {
192 continue
193 }
194 err := i.irc.WriteMessage(&irc.Message{
195 Command: "PRIVMSG",
196 Params: []string{
197 i.channel,
198 l,
199 },
200 })
201 if err != nil {
202 glog.Errorf("IRC/%s: WriteMessage: %v", i.user, err)
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100203 die(err)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100204 s.done <- err
205 return
206 }
207 }
208 s.done <- nil
209 }
210
211 // Timeout ticker - give up connecting to IRC after 15 seconds.
212 t := time.NewTicker(time.Second * 15)
213
214 previousNick := ""
215
216 for {
217 select {
218 case <-ctx.Done():
219 return
220
221 case <-i.eq:
222 glog.Infof("IRC/%s/info: got evicted", i.user)
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100223 die(fmt.Errorf("evicted"))
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100224 return
225
226 case m := <-i.iq:
227 if m.Command != "372" {
228 glog.V(1).Infof("IRC/%s/debug: %+v", i.user, m)
229 }
230
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100231 glog.V(16).Infof("irc/debug16: Message: cmd(%s), channel(%s)", m.Command, m.Params[0])
232 glog.V(16).Infof("irc/debug16: Current: channel(%s), command(%s)", i.channel, "PRIVMSG")
233 glog.V(16).Infof("irc/debug16: Current: channel-eq(%t), command-eq(%t)", i.channel == m.Params[0], "PRIVMSG" == m.Command)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100234 switch {
235 case m.Command == "001":
236 glog.Infof("IRC/%s/info: joining %s...", i.user, i.channel)
237 i.irc.Write("JOIN " + i.channel)
238
239 case m.Command == "353":
240 glog.Infof("IRC/%s/info: joined and ready", i.user)
241 connected = true
242 atomic.StoreInt64(&i.connected, 1)
243 // drain queue of say messages...
244 for _, s := range sayqueue {
245 glog.Infof("IRC/%s/say: [backlog] %q", i.user, s.message)
246 msg(s)
247 }
248 sayqueue = []*controlMessage{}
249
250 case m.Command == "474":
251 // We are banned! :(
252 glog.Infof("IRC/%s/info: banned!", i.user)
253 go i.eventHandler(&event{
254 banned: &eventBanned{i},
255 })
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100256 die(nil)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100257 return
258
259 case m.Command == "KICK" && m.Params[1] == i.irc.CurrentNick():
260 glog.Infof("IRC/%s/info: got kicked", i.user)
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100261 die(nil)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100262 return
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100263 case m.Command == "PRIVMSG" && strings.ToLower(m.Params[0]) == i.channel:
264 glog.V(8).Infof("IRC/%s/debug8: received message on %s", i.user, i.channel)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100265 go i.eventHandler(&event{
266 message: &eventMessage{i, m.Prefix.Name, m.Params[1]},
267 })
268 }
269
270 // update nickmap if needed
271 nick := i.irc.CurrentNick()
272 if previousNick != nick {
273 i.eventHandler(&event{
274 nick: &eventNick{i, nick},
275 })
276 previousNick = nick
277 }
278
279 case s := <-i.sq:
280 if dead {
281 glog.Infof("IRC/%s/say: [DEAD] %q", i.user, s.message)
282 s.done <- fmt.Errorf("connection is dead")
283 } else if connected {
284 glog.Infof("IRC/%s/say: %s", i.user, s.message)
285 msg(s)
286 } else {
287 glog.Infof("IRC/%s/say: [writeback] %q", i.user, s.message)
288 sayqueue = append(sayqueue, s)
289 }
290
291 case <-t.C:
292 if !connected {
293 glog.Errorf("IRC/%s/info: connection timed out, dying", i.user)
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100294 die(fmt.Errorf("connection timeout"))
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100295 return
296 }
297 }
298 }
299}