Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 1 | package irc |
| 2 | |
| 3 | import ( |
| 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. |
| 24 | type Manager struct { |
| 25 | // maximum IRC sessions to maintain |
| 26 | max int |
Michal Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 27 | // IRC bot's username |
| 28 | login string |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 29 | // 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 Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 50 | func NewManager(max int, server, channel string, login string) *Manager { |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 51 | return &Manager{ |
| 52 | max: max, |
Michal Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 53 | login: login, |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 54 | 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 |
| 62 | type 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 |
| 70 | type 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. |
| 79 | func (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. |
| 107 | func (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 Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 148 | name := m.login |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 149 | 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 | } |