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