blob: da1d3e38db10206d1c62dd3ed8c17586f328b7ff [file] [log] [blame]
Sergiusz Bazanskia8854882020-01-05 00:34:38 +01001package irc
2
3import (
4 "context"
5 "time"
6
7 "github.com/golang/glog"
8)
9
10// Manager maintains a set of IRC connections to a server and channel. Its has
11// three interfaces to the outside world:
12// - control, from the owner of Manager (eg. a bridge to another protocol)
13// that allows sending messages as a given user and to subscribe to
14// notifications
15// - events, from IRC connections, to update the manager about a connection
16// state (lifecycle or nick change)
17// - subscriptions, that pass received messages from IRC to a channel requested
18// by control.
19//
20// The Manager will maintain exactly one 'receiver', which is an IRC connection
21// that is used as a source of truth for messages on an IRC channel. This will
22// either be an existing connection for a user, or a 'backup' connection that
23// will close as soon as a real/named connection exists and is fully connected.
24type Manager struct {
25 // maximum IRC sessions to maintain
26 max int
Michal Zagorski5b1aa132020-03-01 17:05:05 +010027 // IRC bot's username
28 login string
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010029 // IRC server address
30 server string
31 // IRC channel name
32 channel string
33 // control channel (from owner)
34 ctrl chan *control
35 // event channel (from connections)
36 event chan *event
37
38 // map from user name to IRC connection
39 conns map[string]*ircconn
40 // map from user name to IRC nick
41 nickmap map[string]string
42 // set of users that we shouldn't attempt to bridge, and their expiry times
43 shitlist map[string]time.Time
44 // set of subscribing channels for notifications
45 subscribers map[chan *Notification]bool
46 // context representing the Manager lifecycle
47 runctx context.Context
48}
49
Michal Zagorski5b1aa132020-03-01 17:05:05 +010050func NewManager(max int, server, channel string, login string) *Manager {
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010051 return &Manager{
52 max: max,
Michal Zagorski5b1aa132020-03-01 17:05:05 +010053 login: login,
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010054 server: server,
55 channel: channel,
56 ctrl: make(chan *control),
57 event: make(chan *event),
58 }
59}
60
61// Notifications are sent to subscribers when things happen on IRC
62type Notification struct {
63 // A new message appeared on the channel
64 Message *NotificationMessage
65 // Nicks of our connections have changed
66 Nickmap *map[string]string
67}
68
69// NotificationMessage is a message that happened in the connected IRC channel
70type NotificationMessage struct {
71 // Nick is the IRC nickname of the sender
72 Nick string
73 // Message is the plaintext message from IRC
74 Message string
75}
76
77// Run maintains the main logic of the Manager - servicing control and event
78// messages, and ensuring there is a receiver on the given channel.
79func (m *Manager) Run(ctx context.Context) {
80 m.conns = make(map[string]*ircconn)
81 m.nickmap = make(map[string]string)
82 m.shitlist = make(map[string]time.Time)
83 m.subscribers = make(map[chan *Notification]bool)
84 m.runctx = context.Background()
85
86 glog.Infof("IRC Manager %s/%s running...", m.server, m.channel)
87
88 t := time.NewTicker(1 * time.Second)
89
90 for {
91 select {
92 case <-ctx.Done():
93 return
94 case c := <-m.ctrl:
95 m.doctrl(ctx, c)
96 case e := <-m.event:
97 m.doevent(ctx, e)
98 case <-t.C:
99 }
100
101 m.ensureReceiver(ctx)
102 }
103}
104
105// ensureReceiver ensures that there is exactly one 'receiver' IRC connection,
106// possibly creating a backup receiver if needed.
107func (m *Manager) ensureReceiver(ctx context.Context) {
108 // Ensure backup listener does not exist if there is a named connection
109 active := 0
110 for _, c := range m.conns {
111 if !c.IsConnected() {
112 continue
113 }
114 active += 1
115 }
116 if active > 1 {
117 var backup *ircconn
118 for _, c := range m.conns {
119 if c.backup {
120 backup = c
121 }
122 }
123 if backup != nil {
124 glog.Infof("Evicting backup listener")
125 backup.Evict()
126 delete(m.conns, backup.user)
127 }
128 }
129 // Ensure there exists exactly one reciever
130 count := 0
131 for _, c := range m.conns {
132 if !c.IsConnected() && !c.backup {
133 c.receiver = false
134 continue
135 }
136 if c.receiver {
137 count += 1
138 }
139 if count >= 2 {
140 c.receiver = false
141 }
142 }
143 // No receivers? make one.
144 if count == 0 {
145 if len(m.conns) == 0 {
146 // Noone said anything on telegram, make backup
147 glog.Infof("No receiver found, making backup")
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100148 name := m.login
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100149 c, err := m.newconn(ctx, name, true)
150 if err != nil {
151 glog.Errorf("Could not make backup receiver: %v", err)
152 } else {
153 m.conns[name] = c
154 }
155 } else {
156 // Make first conn a receiver
157 glog.Infof("No receiver found, using conn")
158 for _, v := range m.conns {
159 glog.Infof("Elected %s for receiver", v.user)
160 v.receiver = true
161 }
162 }
163 }
164}