| package main |
| |
| import ( |
| "context" |
| "flag" |
| "fmt" |
| "strconv" |
| "strings" |
| "time" |
| |
| "code.hackerspace.pl/hscloud/go/mirko" |
| |
| tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" |
| "github.com/golang/glog" |
| |
| "code.hackerspace.pl/hscloud/personal/q3k/lelegram/irc" |
| ) |
| |
| func init() { |
| flag.Set("logtostderr", "true") |
| } |
| |
| var ( |
| flagTelegramToken string |
| flagTelegramChat string |
| flagTeleimgRoot string |
| flagIRCMaxConnections int |
| flagIRCServer string |
| flagIRCChannel string |
| flagIRCLogin string |
| ) |
| |
| // server is responsible for briding IRC and Telegram. |
| type server struct { |
| // groupId is the Telegram Group ID to bridge. |
| groupId int64 |
| tel *tgbotapi.BotAPI |
| mgr *irc.Manager |
| |
| // backlog from telegram |
| telLog chan *telegramPlain |
| // backlog from IRC |
| ircLog chan *irc.Notification |
| } |
| |
| // telegramPlain is a plaintext telegram message - ie. one that's ready to send |
| // to IRC, possibly in mutliple lines. |
| type telegramPlain struct { |
| // Telegram name that sent message - without '@'. |
| user string |
| // Plain text of message, possibly multiline. |
| text string |
| } |
| |
| func newServer(groupId int64, mgr *irc.Manager) (*server, error) { |
| tel, err := tgbotapi.NewBotAPI(flagTelegramToken) |
| if err != nil { |
| return nil, fmt.Errorf("when creating telegram bot: %v", err) |
| } |
| |
| glog.Infof("Authorized with Telegram as %q", tel.Self.UserName) |
| |
| return &server{ |
| groupId: groupId, |
| tel: tel, |
| mgr: mgr, |
| |
| telLog: make(chan *telegramPlain), |
| ircLog: make(chan *irc.Notification), |
| }, nil |
| } |
| |
| func main() { |
| flag.StringVar(&flagTelegramToken, "telegram_token", "", "Telegram Bot API Token") |
| 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") |
| flag.StringVar(&flagTeleimgRoot, "teleimg_root", "https://teleimg.hswaw.net/fileid/", "Root URL of teleimg file serving URL") |
| flag.IntVar(&flagIRCMaxConnections, "irc_max_connections", 10, "How many simulataneous connections can there be to IRC before they get recycled") |
| flag.StringVar(&flagIRCServer, "irc_server", "chat.freenode.net:6667", "The address (with port) of the IRC server to connect to") |
| flag.StringVar(&flagIRCChannel, "irc_channel", "", "The channel name (including hash(es)) to bridge") |
| flag.StringVar(&flagIRCLogin, "irc_login", "lelegram[t]", "The login of irc user used by bot") |
| flag.Parse() |
| |
| if flagTelegramToken == "" { |
| glog.Exitf("telegram_token must be set") |
| } |
| |
| if flagIRCChannel == "" { |
| glog.Exitf("irc_channel must be set") |
| } |
| if flagIRCLogin == "" { |
| flagIRCLogin = "lelegram" |
| } |
| glog.Infof("dabug: Backup login in IRC: %s", flagIRCLogin) |
| // Parse given group ID. |
| // If not set, start server in 'lame' mode, ie. one that will not actually |
| // perform any bridging, but will let you figure out the IDs of groups that |
| // this bot is part of. |
| var groupId int64 |
| if flagTelegramChat == "" { |
| glog.Warningf("telegram_chat NOT GIVEN, STARTING IN LAME MODE") |
| 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.") |
| } else { |
| g, err := strconv.ParseInt(flagTelegramChat, 10, 64) |
| if err != nil { |
| glog.Exitf("telegram_chat must be a number") |
| } |
| groupId = g |
| } |
| |
| m := mirko.New() |
| if err := m.Listen(); err != nil { |
| glog.Exitf("Listen(): %v", err) |
| } |
| |
| // https://tools.ietf.org/html/rfc2812#section-1.3 "Channel names are case insensitive" |
| mgr := irc.NewManager(flagIRCMaxConnections, flagIRCServer, strings.ToLower(flagIRCChannel), flagIRCLogin) |
| glog.V(4).Infof("telegram/debug4: Linking to group: %d", groupId) |
| s, err := newServer(groupId, mgr) |
| if err != nil { |
| glog.Exitf("newServer(): %v", err) |
| } |
| |
| if err := m.Serve(); err != nil { |
| glog.Exitf("Serve(): %v", err) |
| } |
| |
| ctx := m.Context() |
| |
| // Start IRC manager |
| go mgr.Run(ctx) |
| |
| // Start piping Telegram messages into telLog |
| go s.telegramLoop(ctx) |
| |
| // Start piping IRC messages into ircLog |
| mgr.Subscribe(s.ircLog) |
| |
| // Start message processing bridge (connecting telLog and ircLog) |
| go s.bridge(ctx) |
| |
| <-m.Done() |
| } |
| |
| // bridge connects telLog with ircLog, exchanging messages both ways and |
| // performing nick translation given an up-to-date nickmap. |
| func (s *server) bridge(ctx context.Context) { |
| nickmap := make(map[string]string) |
| for { |
| glog.V(32).Info("bridge/debug32: New element in queue") |
| select { |
| case <-ctx.Done(): |
| return |
| case m := <-s.telLog: |
| // Event from Telegram (message). Translate Telegram names into IRC names. |
| text := m.text |
| for t, i := range nickmap { |
| text = strings.ReplaceAll(text, "@"+t, i) |
| } |
| glog.Infof("telegram/info/%s: %v", m.user, text) |
| |
| // Attempt to route message to IRC twice. |
| // This blocks until success or failure, making sure the log stays |
| // totally ordered in the face of some of our IRC connections being |
| // dead/slow. |
| ctxT, cancel := context.WithTimeout(ctx, 15*time.Second) |
| err := s.mgr.SendMessage(ctxT, m.user, text) |
| if err != nil { |
| glog.Warningf("Attempting redelivery of %v after error: %v...", m, err) |
| err = s.mgr.SendMessage(ctx, m.user, text) |
| glog.Errorf("Redelivery of %v failed: %v...", m, err) |
| } |
| cancel() |
| |
| case n := <-s.ircLog: |
| glog.V(4).Infof("bridge/irc/debug4: Get message from irc: %s", n.Message) |
| // Notification from IRC (message or new nickmap) |
| switch { |
| case n.Nickmap != nil: |
| // Nicks on IRC changed. |
| for k, v := range *n.Nickmap { |
| nickmap[k] = v |
| } |
| glog.Infof("New nickmap: %v", nickmap) |
| |
| case n.Message != nil: |
| // New IRC message. Translate IRC names into Telegram names. |
| text := n.Message.Message |
| for t, i := range nickmap { |
| text = strings.ReplaceAll(text, i, "@"+t) |
| } |
| // And send message to Telegram. |
| msg := tgbotapi.NewMessage(s.groupId, fmt.Sprintf("<%s> %s", n.Message.Nick, text)) |
| s.tel.Send(msg) |
| } |
| } |
| } |
| } |