package main import ( "errors" "fmt" "log" "strings" "time" "code.ivysaur.me/libnmdc" telegram "github.com/go-telegram-bot-api/telegram-bot-api" ) // NTFServer methods all run on the same thread, so no mutexes are needed for field access type NTFServer struct { bot *telegram.BotAPI hubMessages chan upstreamMessage chatName string inviteLink string configFile string config NTFConfig conns map[string]*libnmdc.HubConnection // hubnick -> hubconn } type upstreamMessage struct { telegramUserId int64 hubNick string evt libnmdc.HubEvent } func NewNTFServer(configFile string) (*NTFServer, error) { ret := NTFServer{ configFile: configFile, hubMessages: make(chan upstreamMessage, 0), } // Config cfg, err := LoadConfig(configFile) if err != nil { return nil, err } ret.config = cfg // Bot connection if len(cfg.BotAPIKey) == 0 { return nil, errors.New("No bot API key supplied (register with BotFather first)") } bot, err := telegram.NewBotAPI(cfg.BotAPIKey) if err != nil { return nil, fmt.Errorf("Connecting to Telegram: %s", err.Error()) } ret.bot = bot bot.Debug = true log.Printf("Connected to telegram as '%s'", bot.Self.UserName) // Groupchat properties if ret.IsSetupMode() { log.Println("Group chat ID unknown, running in setup mode only - find the groupchat ID then re-run") } else { chatInfo, err := bot.GetChat(telegram.ChatConfig{ChatID: ret.config.GroupChatID}) if err != nil { return nil, fmt.Errorf("Couldn't get supergroup properties: %s", err.Error()) } inviteLink, err := bot.GetInviteLink(telegram.ChatConfig{ChatID: ret.config.GroupChatID}) if err != nil { return nil, fmt.Errorf("Couldn't get supergroup invite link: %s", err.Error()) } log.Printf("Group chat: %s", chatInfo.Title) log.Printf("Invite link: %s", inviteLink) ret.chatName = chatInfo.Title ret.inviteLink = inviteLink } // Spawn upstream connections for all pre-existing known users for telegramUserId, telegramDisplayName := range ret.config.GroupChatMembers { hubNick, ok := ret.config.KnownUsers[telegramUserId] if !ok { log.Fatalf("Chat member '%d' (%s) is missing a hub mapping!!!", telegramUserId, telegramDisplayName) // fatal - inconsistent DB } err := ret.LaunchUpstreamWorker(telegramUserId, hubNick) if err != nil { return nil, fmt.Errorf("Reconnecting upstream for '%s': %s", hubNick, err.Error()) // fatal } } return &ret, nil } // registerUser lodges a user mapping in the configuration file. // This allows them to join the group chat (unbanning them if necessary). // An actual NMDC connection will occur once the user joins for the first time. func (this *NTFServer) registerUser(telegramUserId int64, hubUsername string) error { if existingHubNick, ok := this.config.KnownUsers[telegramUserId]; ok { return fmt.Errorf("Telegram account is already registered with hub nick '%s'", existingHubNick) } for _, v := range this.config.KnownUsers { if v == hubUsername { return fmt.Errorf("Requested hub nick '%s' is already used by another member", hubUsername) } } this.config.KnownUsers[telegramUserId] = hubUsername err := this.config.Save(this.configFile) if err != nil { return err } // Unban from groupchat, if necessary // Ignore errors because the user might not have been banned _, err = this.bot.UnbanChatMember(telegram.ChatMemberConfig{ ChatID: this.config.GroupChatID, UserID: int(telegramUserId), }) if err != nil { log.Printf("Couldn't unban user '%s' from groupchat because: %s (assuming OK, continuing)", hubUsername, err.Error()) } return nil } // LaunchUpstreamWorker opens an NMDC connection. func (this *NTFServer) LaunchUpstreamWorker(telegramUserId int64, hubUsername string) error { if _, exists := this.conns[hubUsername]; exists { return fmt.Errorf("Duplicate hub connection for user '%s', abandoning", hubUsername) } // We want to use the async NMDC connection, but at the same time, provide // extra information in the channel. Wrap the upstream NMDC channel with one // of our own upstreamChan := make(chan libnmdc.HubEvent, 0) go func() { for msg := range upstreamChan { this.hubMessages <- upstreamMessage{ telegramUserId: telegramUserId, hubNick: hubUsername, evt: msg, } } }() conn := libnmdc.ConnectAsync( &libnmdc.HubConnectionOptions{ Address: libnmdc.HubAddress(this.config.HubAddr), Self: &libnmdc.UserInfo{ Nick: hubUsername, ClientTag: AppName, ClientVersion: AppVersion, }, }, upstreamChan, ) // Stash conn so that we can refer to it / close it later this.conns[hubUsername] = conn return nil } func (this *NTFServer) IsSetupMode() bool { return this.config.GroupChatID == 0 } func (this *NTFServer) Run() error { updateProps := telegram.NewUpdate(0) updateProps.Timeout = 60 // seconds updateChan, err := this.bot.GetUpdatesChan(updateProps) if err != nil { return err } for { select { case update, ok := <-updateChan: if !ok { log.Fatalf("Telegram update channel closed unexpectedly") } this.HandleMessage(update) case hubMsg, ok := <-this.hubMessages: if !ok { log.Fatalf("Upstream update channel closed unexpectedly") } this.HandleHubMessage(hubMsg) } } return nil // UNREACHABLE } func (this *NTFServer) HandleHubMessage(msg upstreamMessage) { log.Printf("Hub: %#v", msg) } func (this *NTFServer) HandleMessage(update telegram.Update) { if update.Message == nil { return } if this.IsSetupMode() { log.Printf("Message from '%s': '%s', chat ID '%d'\n", update.Message.From.UserName, update.Message.Text, update.Message.Chat.ID) } else if update.Message.Chat.ID == this.config.GroupChatID { err := this.HandleGroupMessage(update) if err != nil { log.Printf("Handling group message: %s", err.Error()) } } else if update.Message.Chat.IsPrivate() { err := this.HandleDirectMessage(update) if err != nil { log.Printf("Handling private message: %s", err.Error()) } } else { log.Printf("Message from unknown chat %d (not our supergroup, not a PM, ...)", update.Message.Chat.ID) } fmt.Printf("%#v\n", update.Message) log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) } func (this *NTFServer) HandleTelegramUserParted(telegramUserId int64, update telegram.Update) error { delete(this.config.GroupChatMembers, telegramUserId) return this.config.Save(this.configFile) } func (this *NTFServer) HandleTelegramUserJoined(telegramUserId int64, update telegram.Update) error { // If known, spawn the upstream connection; if unknown, kick them hubNick, ok := this.config.KnownUsers[telegramUserId] if !ok { _, err := this.bot.KickChatMember(telegram.KickChatMemberConfig{ ChatMemberConfig: telegram.ChatMemberConfig{ ChatID: update.Message.Chat.ID, UserID: int(telegramUserId), }, UntilDate: time.Now().Add(24 * time.Hour).Unix(), }) if err != nil { log.Printf("Couldn't kick unexpected chat member from group: %s", err.Error()) } return nil } // Spawn the upstream connection for this user return this.LaunchUpstreamWorker(telegramUserId, hubNick) } func (this *NTFServer) HandleGroupMessage(update telegram.Update) error { // Joins: ???? if update.Message.NewChatMembers != nil && len(*update.Message.NewChatMembers) > 0 { // Users joining // Ensure that they have a valid user mapping // Create upstream NMDC connection for them for _, joinedUser := range *update.Message.NewChatMembers { err := this.HandleTelegramUserJoined(int64(joinedUser.ID), update) if err != nil { log.Printf("Handling user join: %s", err.Error()) } } return nil } if update.Message.LeftChatMember != nil { // User parted // Close upstream NMDC connection for them return this.HandleTelegramUserParted(int64(update.Message.LeftChatMember.ID), update) } // Actual chat message // Find the responsible user and send it to the upstream hub using their account // TODO return nil } func (this *NTFServer) HandleDirectMessage(update telegram.Update) error { // Registration workflow // Find out the current status for this chat ID userID := int64(update.Message.From.ID) hubNick, isKnown := this.config.KnownUsers[userID] respond := func(str string) error { msg := telegram.NewMessage(update.Message.Chat.ID, str) msg.ReplyToMessageID = update.Message.MessageID _, err := this.bot.Send(msg) return err } // Handle the incoming request msg := update.Message.Text if strings.HasPrefix(msg, "/pm ") { if !isKnown { return respond("Can't send a native PM until you've joined.") } // TODO return respond("Not yet implemented.") } else if strings.HasPrefix(msg, "/join ") { if isKnown { return respond("You've already joined as '" + hubNick + "'.") } requestedHubNick := msg[6:] err := this.registerUser(userID, requestedHubNick) if err != nil { log.Printf("Failed to join user: %s", err.Error()) return respond(fmt.Sprintf("Couldn't allow join because: %s", err.Error())) } return respond(fmt.Sprintf("Hi '%s'! You can join %s at %s", requestedHubNick, this.config.HubDescription, this.inviteLink)) } else if strings.HasPrefix(msg, "/quit") { // TODO return respond("Not yet implemented.") } else { // e.g. /setup or /help // Help helpMsg := `I am a bot that connects Telegram with ` + this.config.HubDescription + ".\n" if isKnown { helpMsg += "You are currently connected as: '" + hubNick + "'\n" } else { helpMsg += "You aren't connected yet.\n" } helpMsg += ` Available commands: /setup - Welcome message /join [hubnick] - Join ` + this.config.HubDescription + ` as user 'hubnick' /pm [recipient] [message] - Send a native PM (if connected) /quit - Leave the group chat ` return respond(helpMsg) } return nil }