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