blob: f4360a980050295dc7aa7c7087331ad39084cd5a [file] [log] [blame]
Sergiusz Bazanskia8854882020-01-05 00:34:38 +01001package main
2
3import (
4 "context"
5 "flag"
6 "fmt"
7 "strconv"
8 "strings"
Sergiusz Bazanski83e26902020-01-23 14:18:25 +01009 "time"
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010010
11 "code.hackerspace.pl/hscloud/go/mirko"
12
13 tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
14 "github.com/golang/glog"
15
16 "code.hackerspace.pl/hscloud/personal/q3k/lelegram/irc"
17)
18
19func init() {
20 flag.Set("logtostderr", "true")
21}
22
23var (
24 flagTelegramToken string
25 flagTelegramChat string
26 flagTeleimgRoot string
27 flagIRCMaxConnections int
28 flagIRCServer string
29 flagIRCChannel string
30)
31
32// server is responsible for briding IRC and Telegram.
33type server struct {
34 // groupId is the Telegram Group ID to bridge.
35 groupId int64
36 tel *tgbotapi.BotAPI
37 mgr *irc.Manager
38
39 // backlog from telegram
40 telLog chan *telegramPlain
41 // backlog from IRC
42 ircLog chan *irc.Notification
43}
44
45// telegramPlain is a plaintext telegram message - ie. one that's ready to send
46// to IRC, possibly in mutliple lines.
47type telegramPlain struct {
48 // Telegram name that sent message - without '@'.
49 user string
50 // Plain text of message, possibly multiline.
51 text string
52}
53
54func newServer(groupId int64, mgr *irc.Manager) (*server, error) {
55 tel, err := tgbotapi.NewBotAPI(flagTelegramToken)
56 if err != nil {
57 return nil, fmt.Errorf("when creating telegram bot: %v", err)
58 }
59
60 glog.Infof("Authorized with Telegram as %q", tel.Self.UserName)
61
62 return &server{
63 groupId: groupId,
64 tel: tel,
65 mgr: mgr,
66
67 telLog: make(chan *telegramPlain),
68 ircLog: make(chan *irc.Notification),
69 }, nil
70}
71
72func main() {
73 flag.StringVar(&flagTelegramToken, "telegram_token", "", "Telegram Bot API Token")
74 flag.StringVar(&flagTelegramChat, "telegram_chat", "", "Telegram chat/group ID to bridge. If not given, bridge will start in lame mode and allow you to find out IDs of groups which the bridge bot is part of")
75 flag.StringVar(&flagTeleimgRoot, "teleimg_root", "https://teleimg.hswaw.net/fileid/", "Root URL of teleimg file serving URL")
76 flag.IntVar(&flagIRCMaxConnections, "irc_max_connections", 10, "How many simulataneous connections can there be to IRC before they get recycled")
77 flag.StringVar(&flagIRCServer, "irc_server", "chat.freenode.net:6667", "The address (with port) of the IRC server to connect to")
78 flag.StringVar(&flagIRCChannel, "irc_channel", "", "The channel name (including hash(es)) to bridge")
79 flag.Parse()
80
81 if flagTelegramToken == "" {
82 glog.Exitf("telegram_token must be set")
83 }
84
85 if flagIRCChannel == "" {
86 glog.Exitf("irc_channel must be set")
87 }
88
89 // Parse given group ID.
90 // If not set, start server in 'lame' mode, ie. one that will not actually
91 // perform any bridging, but will let you figure out the IDs of groups that
92 // this bot is part of.
93 var groupId int64
94 if flagTelegramChat == "" {
95 glog.Warningf("telegram_chat NOT GIVEN, STARTING IN LAME MODE")
96 glog.Warningf("Watch for logs to find out the ID of groups which this bot is part of. Then, restart the bot with telegram_chat set.")
97 } else {
98 g, err := strconv.ParseInt(flagTelegramChat, 10, 64)
99 if err != nil {
100 glog.Exitf("telegram_chat must be a number")
101 }
102 groupId = g
103 }
104
105 m := mirko.New()
106 if err := m.Listen(); err != nil {
107 glog.Exitf("Listen(): %v", err)
108 }
109
110 mgr := irc.NewManager(flagIRCMaxConnections, flagIRCServer, flagIRCChannel)
111
112 s, err := newServer(groupId, mgr)
113 if err != nil {
114 glog.Exitf("newServer(): %v", err)
115 }
116
117 if err := m.Serve(); err != nil {
118 glog.Exitf("Serve(): %v", err)
119 }
120
121 ctx := m.Context()
122
123 // Start IRC manager
124 go mgr.Run(ctx)
125
126 // Start piping Telegram messages into telLog
127 go s.telegramLoop(ctx)
128
129 // Start piping IRC messages into ircLog
130 mgr.Subscribe(s.ircLog)
131
132 // Start message processing bridge (connecting telLog and ircLog)
133 go s.bridge(ctx)
134
135 <-m.Done()
136}
137
138// bridge connects telLog with ircLog, exchanging messages both ways and
139// performing nick translation given an up-to-date nickmap.
140func (s *server) bridge(ctx context.Context) {
141 nickmap := make(map[string]string)
142 for {
143 select {
144 case <-ctx.Done():
145 return
146
147 case m := <-s.telLog:
148 // Event from Telegram (message). Translate Telegram names into IRC names.
149 text := m.text
150 for t, i := range nickmap {
151 text = strings.ReplaceAll(text, "@"+t, i)
152 }
153 glog.Infof("telegram/%s: %v", m.user, text)
154
155 // Attempt to route message to IRC twice.
156 // This blocks until success or failure, making sure the log stays
157 // totally ordered in the face of some of our IRC connections being
158 // dead/slow.
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100159 ctxT, cancel := context.WithTimeout(ctx, 15*time.Second)
160 err := s.mgr.SendMessage(ctxT, m.user, text)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100161 if err != nil {
162 glog.Warningf("Attempting redelivery of %v after error: %v...", m, err)
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100163 err = s.mgr.SendMessage(ctx, m.user, text)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100164 glog.Errorf("Redelivery of %v failed: %v...", m, err)
165 }
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100166 cancel()
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100167
168 case n := <-s.ircLog:
169 // Notification from IRC (message or new nickmap)
170 switch {
171 case n.Nickmap != nil:
172 // Nicks on IRC changed.
173 for k, v := range *n.Nickmap {
174 nickmap[k] = v
175 }
176 glog.Infof("New nickmap: %v", nickmap)
177
178 case n.Message != nil:
179 // New IRC message. Translate IRC names into Telegram names.
180 text := n.Message.Message
181 for t, i := range nickmap {
182 text = strings.ReplaceAll(text, i, "@"+t)
183 }
184 // And send message to Telegram.
185 msg := tgbotapi.NewMessage(s.groupId, fmt.Sprintf("<%s> %s", n.Message.Nick, text))
186 s.tel.Send(msg)
187 }
188 }
189 }
190}