commit 5c253575aae6ed183e949649a057d38b3c0a1cf2 Author: mappu Date: Sun Jun 3 15:27:50 2018 +1200 initial commit diff --git a/NTFConfig.go b/NTFConfig.go new file mode 100644 index 0000000..30f7c9c --- /dev/null +++ b/NTFConfig.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "io/ioutil" +) + +type NTFConfig struct { + HubAddr string + BotAPIKey string + GroupChatID int64 + + // Map of telegram user IDs to NMDC nicks + KnownUsers map[int64]string +} + +func LoadConfig(configFile string) (NTFConfig, error) { + b, err := ioutil.ReadFile(configFile) + if err != nil { + return NTFConfig{}, err + } + + ret := NTFConfig{} + err = json.Unmarshal(b, &ret) + if err != nil { + return NTFConfig{}, err + } + + return ret, nil +} + +func (this *NTFConfig) Save(configFile string) error { + b, err := json.MarshalIndent(this, "", " ") + if err != nil { + return err + } + + return ioutil.WriteFile(configFile, b, 0644) +} diff --git a/NTFServer.go b/NTFServer.go new file mode 100644 index 0000000..33896fc --- /dev/null +++ b/NTFServer.go @@ -0,0 +1,178 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + + "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 + upstream chan upstreamMessage + chatName string + inviteLink string + configFile string + config NTFConfig +} + +type upstreamMessage struct { + telegramUserId int64 + evt libnmdc.HubEvent +} + +func NewNTFServer(configFile string) (*NTFServer, error) { + ret := NTFServer{ + configFile: configFile, + } + + cfg, err := LoadConfig(configFile) + if err != nil { + return nil, err + } + + ret.config = cfg + + 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) + + 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 known users + ret.upstream = make(chan upstreamMessage, 0) + for k, v := range ret.config.KnownUsers { + ret.LaunchUpstreamWorker(k, v) + } + + return &ret, nil +} + +func (this *NTFServer) LaunchUpstreamWorker(telegramUserId int64, hubUsername string) { + ctx := context.Background() + + // Open NMDC connection + go upstreamWorker() +} + +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 update := range updateChan { + this.HandleMessage(update) + } + + return nil // Update channel was closed +} + +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) 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 + } + + if update.Message.LeftChatMember != nil { + // User parted + // Close upstream NMDC connection for them + } + + // Parts: + /* + &tgbotapi.Message{ + MessageID:9, From:(*tgbotapi.User)(0xc420304000), Date:1527989178, Chat:(*tgbotapi.Chat)(0xc4201dc120), [...] + NewChatMembers:(*[]tgbotapi.User)(nil), + LeftChatMember:(*tgbotapi.User)(0xc420304050), + } + */ + + return nil +} + +func (this *NTFServer) HandleDirectMessage(update telegram.Update) error { + + // Registration workflow + // Find out the current status for this chat ID... etc. + + msg := telegram.NewMessage(update.Message.Chat.ID, "Hi user, join the group chat at "+this.inviteLink) + msg.ReplyToMessageID = update.Message.MessageID + this.bot.Send(msg) + + return nil +} diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..5065413 --- /dev/null +++ b/README.txt @@ -0,0 +1,20 @@ +A bot to synchronise an NMDC hub with a Telegram supergroup. + +## USAGE + +Create a new bot + - Use BotFather to create a new bot + - Use BotFather to change its privacy mode for group chats + +Create a group + - Manually create a group chat and add the bot to it + - Convert group chat to supergroup + - Grant bot to be an administrator (including ability to add more administrators) + - Settings > "Who can add members" > Only administrators + - Create an invite link + +Start nmdc-telegramfrontend + - Run this bot with no -GroupChatID, to learn the groupchat ID + - Post a test message in the group chat, to discover the groupchat ID + - Leave the group chat + - Run this bot with -GroupChatID for normal operation diff --git a/main.go b/main.go new file mode 100644 index 0000000..ec90907 --- /dev/null +++ b/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "flag" + "log" +) + +func main() { + + configFile := flag.String("ConfigFile", "config.json", "") + flag.Parse() + + svr, err := NewNTFServer(*configFile) + if err != nil { + log.Fatal(err.Error()) + } + + err = svr.Run() + if err != nil { + log.Fatal(err.Error()) + } + + log.Println("Server shutting down") +} diff --git a/upstreamWorker.go b/upstreamWorker.go new file mode 100644 index 0000000..6e27e64 --- /dev/null +++ b/upstreamWorker.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + + "code.ivysaur.me/libnmdc" +) + +// upstreamWorker handles an NMDC connection. +// It blocks on the current thread, caller can background it with 'go'. +func upstreamWorker(ctx context.Context, hubAddress, username string, responseChan chan<- libnmdc.HubEvent) { + + interiorChan := make(chan libnmdc.HubEvent, 0) + + conn := libnmdc.ConnectAsync( + &libnmdc.HubConnectionOptions{ + Address: libnmdc.HubAddress(hubAddress), + Self: &libnmdc.UserInfo{ + Nick: username, + ClientTag: AppName, + ClientVersion: AppVersion, + }, + }, + interiorChan, + ) + + for { + select { + case <-ctx.Done(): + conn.Disconnect() + ctx = nil // prevent hitting this case again repeatedly - closed channel reads immediately, nil channel blocks forever + + case evt := <-interiorChan: + responseChan <- evt // forward + + if evt.StateChange == libnmdc.CONNECTIONSTATE_DISCONNECTED && ctx.Err() != nil { + // We're done here + return + } + + } + + } +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..2342c8e --- /dev/null +++ b/version.go @@ -0,0 +1,6 @@ +package main + +const ( + AppName = "nmdc-telegramfrontend" + AppVersion = "1.0.0" +)