lelegram: init
This is an IRC/Telegram bridge.
It does multi-account puppet-like access to IRC making everyone's life
easier.
Compared to teleirc it also:
- is smarter about converting messages
- uses teleimg for public image access
- is not written in JS
Experimental for now.
Change-Id: I66ba3f83abdfdea6463ab3be5380d8d3f2769291
diff --git a/personal/q3k/lelegram/irc/manager.go b/personal/q3k/lelegram/irc/manager.go
new file mode 100644
index 0000000..4e8365d
--- /dev/null
+++ b/personal/q3k/lelegram/irc/manager.go
@@ -0,0 +1,161 @@
+package irc
+
+import (
+ "context"
+ "time"
+
+ "github.com/golang/glog"
+)
+
+// Manager maintains a set of IRC connections to a server and channel. Its has
+// three interfaces to the outside world:
+// - control, from the owner of Manager (eg. a bridge to another protocol)
+// that allows sending messages as a given user and to subscribe to
+// notifications
+// - events, from IRC connections, to update the manager about a connection
+// state (lifecycle or nick change)
+// - subscriptions, that pass received messages from IRC to a channel requested
+// by control.
+//
+// The Manager will maintain exactly one 'receiver', which is an IRC connection
+// that is used as a source of truth for messages on an IRC channel. This will
+// either be an existing connection for a user, or a 'backup' connection that
+// will close as soon as a real/named connection exists and is fully connected.
+type Manager struct {
+ // maximum IRC sessions to maintain
+ max int
+ // IRC server address
+ server string
+ // IRC channel name
+ channel string
+ // control channel (from owner)
+ ctrl chan *control
+ // event channel (from connections)
+ event chan *event
+
+ // map from user name to IRC connection
+ conns map[string]*ircconn
+ // map from user name to IRC nick
+ nickmap map[string]string
+ // set of users that we shouldn't attempt to bridge, and their expiry times
+ shitlist map[string]time.Time
+ // set of subscribing channels for notifications
+ subscribers map[chan *Notification]bool
+ // context representing the Manager lifecycle
+ runctx context.Context
+}
+
+func NewManager(max int, server, channel string) *Manager {
+ return &Manager{
+ max: max,
+ server: server,
+ channel: channel,
+ ctrl: make(chan *control),
+ event: make(chan *event),
+ }
+}
+
+// Notifications are sent to subscribers when things happen on IRC
+type Notification struct {
+ // A new message appeared on the channel
+ Message *NotificationMessage
+ // Nicks of our connections have changed
+ Nickmap *map[string]string
+}
+
+// NotificationMessage is a message that happened in the connected IRC channel
+type NotificationMessage struct {
+ // Nick is the IRC nickname of the sender
+ Nick string
+ // Message is the plaintext message from IRC
+ Message string
+}
+
+// Run maintains the main logic of the Manager - servicing control and event
+// messages, and ensuring there is a receiver on the given channel.
+func (m *Manager) Run(ctx context.Context) {
+ m.conns = make(map[string]*ircconn)
+ m.nickmap = make(map[string]string)
+ m.shitlist = make(map[string]time.Time)
+ m.subscribers = make(map[chan *Notification]bool)
+ m.runctx = context.Background()
+
+ glog.Infof("IRC Manager %s/%s running...", m.server, m.channel)
+
+ t := time.NewTicker(1 * time.Second)
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case c := <-m.ctrl:
+ m.doctrl(ctx, c)
+ case e := <-m.event:
+ m.doevent(ctx, e)
+ case <-t.C:
+ }
+
+ m.ensureReceiver(ctx)
+ }
+}
+
+// ensureReceiver ensures that there is exactly one 'receiver' IRC connection,
+// possibly creating a backup receiver if needed.
+func (m *Manager) ensureReceiver(ctx context.Context) {
+ // Ensure backup listener does not exist if there is a named connection
+ active := 0
+ for _, c := range m.conns {
+ if !c.IsConnected() {
+ continue
+ }
+ active += 1
+ }
+ if active > 1 {
+ var backup *ircconn
+ for _, c := range m.conns {
+ if c.backup {
+ backup = c
+ }
+ }
+ if backup != nil {
+ glog.Infof("Evicting backup listener")
+ backup.Evict()
+ delete(m.conns, backup.user)
+ }
+ }
+ // Ensure there exists exactly one reciever
+ count := 0
+ for _, c := range m.conns {
+ if !c.IsConnected() && !c.backup {
+ c.receiver = false
+ continue
+ }
+ if c.receiver {
+ count += 1
+ }
+ if count >= 2 {
+ c.receiver = false
+ }
+ }
+ // No receivers? make one.
+ if count == 0 {
+ if len(m.conns) == 0 {
+ // Noone said anything on telegram, make backup
+ glog.Infof("No receiver found, making backup")
+ name := "lelegram"
+ c, err := m.newconn(ctx, name, true)
+ if err != nil {
+ glog.Errorf("Could not make backup receiver: %v", err)
+ } else {
+ m.conns[name] = c
+ }
+ } else {
+ // Make first conn a receiver
+ glog.Infof("No receiver found, using conn")
+ for _, v := range m.conns {
+ glog.Infof("Elected %s for receiver", v.user)
+ v.receiver = true
+ }
+ }
+ }
+}