Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "flag" |
| 6 | "fmt" |
| 7 | "strconv" |
| 8 | "strings" |
Sergiusz Bazanski | 83e2690 | 2020-01-23 14:18:25 +0100 | [diff] [blame] | 9 | "time" |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 10 | |
| 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 | |
| 19 | func init() { |
| 20 | flag.Set("logtostderr", "true") |
| 21 | } |
| 22 | |
| 23 | var ( |
| 24 | flagTelegramToken string |
| 25 | flagTelegramChat string |
| 26 | flagTeleimgRoot string |
| 27 | flagIRCMaxConnections int |
| 28 | flagIRCServer string |
| 29 | flagIRCChannel string |
Michal Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 30 | flagIRCLogin string |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 31 | ) |
| 32 | |
| 33 | // server is responsible for briding IRC and Telegram. |
| 34 | type 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. |
| 48 | type telegramPlain struct { |
| 49 | // Telegram name that sent message - without '@'. |
| 50 | user string |
| 51 | // Plain text of message, possibly multiline. |
| 52 | text string |
| 53 | } |
| 54 | |
| 55 | func 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 | |
| 73 | func 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 Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 80 | flag.StringVar(&flagIRCLogin, "irc_login", "lelegram[t]", "The login of irc user used by bot") |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 81 | 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 Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 90 | if flagIRCLogin == "" { |
| 91 | flagIRCLogin = "lelegram" |
| 92 | } |
| 93 | glog.Infof("dabug: Backup login in IRC: %s", flagIRCLogin) |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 94 | // 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 Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 115 | // 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 Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 118 | 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. |
| 146 | func (s *server) bridge(ctx context.Context) { |
| 147 | nickmap := make(map[string]string) |
| 148 | for { |
Michal Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 149 | glog.V(32).Info("bridge/debug32: New element in queue") |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 150 | select { |
| 151 | case <-ctx.Done(): |
| 152 | return |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 153 | 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 Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 159 | glog.Infof("telegram/info/%s: %v", m.user, text) |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 160 | |
| 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 Bazanski | 83e2690 | 2020-01-23 14:18:25 +0100 | [diff] [blame] | 165 | ctxT, cancel := context.WithTimeout(ctx, 15*time.Second) |
| 166 | err := s.mgr.SendMessage(ctxT, m.user, text) |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 167 | if err != nil { |
| 168 | glog.Warningf("Attempting redelivery of %v after error: %v...", m, err) |
Sergiusz Bazanski | 83e2690 | 2020-01-23 14:18:25 +0100 | [diff] [blame] | 169 | err = s.mgr.SendMessage(ctx, m.user, text) |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 170 | glog.Errorf("Redelivery of %v failed: %v...", m, err) |
| 171 | } |
Sergiusz Bazanski | 83e2690 | 2020-01-23 14:18:25 +0100 | [diff] [blame] | 172 | cancel() |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 173 | |
| 174 | case n := <-s.ircLog: |
Michal Zagorski | 5b1aa13 | 2020-03-01 17:05:05 +0100 | [diff] [blame] | 175 | glog.V(4).Infof("bridge/irc/debug4: Get message from irc: %s", n.Message) |
Sergiusz Bazanski | a885488 | 2020-01-05 00:34:38 +0100 | [diff] [blame] | 176 | // 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 | } |