blob: 9e8f977445150ee9d8a907f212bb41dcb781ca84 [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
Michal Zagorski5b1aa132020-03-01 17:05:05 +010030 flagIRCLogin string
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010031)
32
33// server is responsible for briding IRC and Telegram.
34type server struct {
35 // groupId is the Telegram Group ID to bridge.
36 groupId int64
37 tel *tgbotapi.BotAPI
38 mgr *irc.Manager
39
40 // backlog from telegram
41 telLog chan *telegramPlain
42 // backlog from IRC
43 ircLog chan *irc.Notification
44}
45
46// telegramPlain is a plaintext telegram message - ie. one that's ready to send
47// to IRC, possibly in mutliple lines.
48type telegramPlain struct {
49 // Telegram name that sent message - without '@'.
50 user string
51 // Plain text of message, possibly multiline.
52 text string
53}
54
55func newServer(groupId int64, mgr *irc.Manager) (*server, error) {
56 tel, err := tgbotapi.NewBotAPI(flagTelegramToken)
57 if err != nil {
58 return nil, fmt.Errorf("when creating telegram bot: %v", err)
59 }
60
61 glog.Infof("Authorized with Telegram as %q", tel.Self.UserName)
62
63 return &server{
64 groupId: groupId,
65 tel: tel,
66 mgr: mgr,
67
68 telLog: make(chan *telegramPlain),
69 ircLog: make(chan *irc.Notification),
70 }, nil
71}
72
73func main() {
74 flag.StringVar(&flagTelegramToken, "telegram_token", "", "Telegram Bot API Token")
75 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")
76 flag.StringVar(&flagTeleimgRoot, "teleimg_root", "https://teleimg.hswaw.net/fileid/", "Root URL of teleimg file serving URL")
77 flag.IntVar(&flagIRCMaxConnections, "irc_max_connections", 10, "How many simulataneous connections can there be to IRC before they get recycled")
78 flag.StringVar(&flagIRCServer, "irc_server", "chat.freenode.net:6667", "The address (with port) of the IRC server to connect to")
79 flag.StringVar(&flagIRCChannel, "irc_channel", "", "The channel name (including hash(es)) to bridge")
Michal Zagorski5b1aa132020-03-01 17:05:05 +010080 flag.StringVar(&flagIRCLogin, "irc_login", "lelegram[t]", "The login of irc user used by bot")
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010081 flag.Parse()
82
83 if flagTelegramToken == "" {
84 glog.Exitf("telegram_token must be set")
85 }
86
87 if flagIRCChannel == "" {
88 glog.Exitf("irc_channel must be set")
89 }
Michal Zagorski5b1aa132020-03-01 17:05:05 +010090 if flagIRCLogin == "" {
91 flagIRCLogin = "lelegram"
92 }
93 glog.Infof("dabug: Backup login in IRC: %s", flagIRCLogin)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010094 // Parse given group ID.
95 // If not set, start server in 'lame' mode, ie. one that will not actually
96 // perform any bridging, but will let you figure out the IDs of groups that
97 // this bot is part of.
98 var groupId int64
99 if flagTelegramChat == "" {
100 glog.Warningf("telegram_chat NOT GIVEN, STARTING IN LAME MODE")
101 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.")
102 } else {
103 g, err := strconv.ParseInt(flagTelegramChat, 10, 64)
104 if err != nil {
105 glog.Exitf("telegram_chat must be a number")
106 }
107 groupId = g
108 }
109
110 m := mirko.New()
111 if err := m.Listen(); err != nil {
112 glog.Exitf("Listen(): %v", err)
113 }
114
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100115 // https://tools.ietf.org/html/rfc2812#section-1.3 "Channel names are case insensitive"
116 mgr := irc.NewManager(flagIRCMaxConnections, flagIRCServer, strings.ToLower(flagIRCChannel), flagIRCLogin)
117 glog.V(4).Infof("telegram/debug4: Linking to group: %d", groupId)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100118 s, err := newServer(groupId, mgr)
119 if err != nil {
120 glog.Exitf("newServer(): %v", err)
121 }
122
123 if err := m.Serve(); err != nil {
124 glog.Exitf("Serve(): %v", err)
125 }
126
127 ctx := m.Context()
128
129 // Start IRC manager
130 go mgr.Run(ctx)
131
132 // Start piping Telegram messages into telLog
133 go s.telegramLoop(ctx)
134
135 // Start piping IRC messages into ircLog
136 mgr.Subscribe(s.ircLog)
137
138 // Start message processing bridge (connecting telLog and ircLog)
139 go s.bridge(ctx)
140
141 <-m.Done()
142}
143
144// bridge connects telLog with ircLog, exchanging messages both ways and
145// performing nick translation given an up-to-date nickmap.
146func (s *server) bridge(ctx context.Context) {
147 nickmap := make(map[string]string)
148 for {
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100149 glog.V(32).Info("bridge/debug32: New element in queue")
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100150 select {
151 case <-ctx.Done():
152 return
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100153 case m := <-s.telLog:
154 // Event from Telegram (message). Translate Telegram names into IRC names.
155 text := m.text
156 for t, i := range nickmap {
157 text = strings.ReplaceAll(text, "@"+t, i)
158 }
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100159 glog.Infof("telegram/info/%s: %v", m.user, text)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100160
161 // Attempt to route message to IRC twice.
162 // This blocks until success or failure, making sure the log stays
163 // totally ordered in the face of some of our IRC connections being
164 // dead/slow.
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100165 ctxT, cancel := context.WithTimeout(ctx, 15*time.Second)
166 err := s.mgr.SendMessage(ctxT, m.user, text)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100167 if err != nil {
168 glog.Warningf("Attempting redelivery of %v after error: %v...", m, err)
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100169 err = s.mgr.SendMessage(ctx, m.user, text)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100170 glog.Errorf("Redelivery of %v failed: %v...", m, err)
171 }
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100172 cancel()
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100173
174 case n := <-s.ircLog:
Michal Zagorski5b1aa132020-03-01 17:05:05 +0100175 glog.V(4).Infof("bridge/irc/debug4: Get message from irc: %s", n.Message)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100176 // Notification from IRC (message or new nickmap)
177 switch {
178 case n.Nickmap != nil:
179 // Nicks on IRC changed.
180 for k, v := range *n.Nickmap {
181 nickmap[k] = v
182 }
183 glog.Infof("New nickmap: %v", nickmap)
184
185 case n.Message != nil:
186 // New IRC message. Translate IRC names into Telegram names.
187 text := n.Message.Message
188 for t, i := range nickmap {
189 text = strings.ReplaceAll(text, i, "@"+t)
190 }
191 // And send message to Telegram.
192 msg := tgbotapi.NewMessage(s.groupId, fmt.Sprintf("<%s> %s", n.Message.Nick, text))
193 s.tel.Send(msg)
194 }
195 }
196 }
197}