wip (2)
This commit is contained in:
parent
6a7a7a51f6
commit
9bfdc42187
14
NTFConfig.go
14
NTFConfig.go
@ -10,6 +10,7 @@ import (
|
||||
type NTFConfig struct {
|
||||
HubAddr string
|
||||
HubDescription string
|
||||
HubSecNick string // Nickname of Hub-Security to exclude (e.g. "PtokaX")
|
||||
BotAPIKey string
|
||||
GroupChatID int64
|
||||
|
||||
@ -18,6 +19,9 @@ type NTFConfig struct {
|
||||
|
||||
// Map of telegram users known to be in the group chat ==> telegram displayname
|
||||
GroupChatMembers map[int64]string
|
||||
|
||||
// Map of telegram users to their direct conversation ID with the bot
|
||||
DirectMessageChats map[int64]int64
|
||||
}
|
||||
|
||||
func LoadConfig(configFile string) (NTFConfig, error) {
|
||||
@ -32,6 +36,16 @@ func LoadConfig(configFile string) (NTFConfig, error) {
|
||||
return NTFConfig{}, err
|
||||
}
|
||||
|
||||
if ret.KnownUsers == nil {
|
||||
ret.KnownUsers = make(map[int64]string)
|
||||
}
|
||||
if ret.GroupChatMembers == nil {
|
||||
ret.GroupChatMembers = make(map[int64]string)
|
||||
}
|
||||
if ret.DirectMessageChats == nil {
|
||||
ret.DirectMessageChats = make(map[int64]int64)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
|
230
NTFServer.go
230
NTFServer.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
@ -32,6 +33,7 @@ func NewNTFServer(configFile string) (*NTFServer, error) {
|
||||
ret := NTFServer{
|
||||
configFile: configFile,
|
||||
hubMessages: make(chan upstreamMessage, 0),
|
||||
conns: make(map[string]*libnmdc.HubConnection),
|
||||
}
|
||||
|
||||
// Config
|
||||
@ -105,6 +107,10 @@ func NewNTFServer(configFile string) (*NTFServer, error) {
|
||||
// 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 {
|
||||
if existingHubNick == hubUsername {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Telegram account is already registered with hub nick '%s'", existingHubNick)
|
||||
}
|
||||
|
||||
@ -140,6 +146,8 @@ func (this *NTFServer) LaunchUpstreamWorker(telegramUserId int64, hubUsername st
|
||||
return fmt.Errorf("Duplicate hub connection for user '%s', abandoning", hubUsername)
|
||||
}
|
||||
|
||||
log.Printf("Connecting to hub '%s' as user '%s'...", this.config.HubAddr, 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
|
||||
@ -154,14 +162,14 @@ func (this *NTFServer) LaunchUpstreamWorker(telegramUserId int64, hubUsername st
|
||||
}
|
||||
}()
|
||||
|
||||
hubUser := libnmdc.NewUserInfo(hubUsername)
|
||||
hubUser.ClientTag = AppName
|
||||
hubUser.ClientVersion = AppVersion
|
||||
|
||||
conn := libnmdc.ConnectAsync(
|
||||
&libnmdc.HubConnectionOptions{
|
||||
Address: libnmdc.HubAddress(this.config.HubAddr),
|
||||
Self: &libnmdc.UserInfo{
|
||||
Nick: hubUsername,
|
||||
ClientTag: AppName,
|
||||
ClientVersion: AppVersion,
|
||||
},
|
||||
Self: hubUser,
|
||||
},
|
||||
upstreamChan,
|
||||
)
|
||||
@ -205,6 +213,36 @@ func (this *NTFServer) Run() error {
|
||||
}
|
||||
|
||||
func (this *NTFServer) HandleHubMessage(msg upstreamMessage) {
|
||||
switch msg.evt.EventType {
|
||||
|
||||
case libnmdc.EVENT_SYSTEM_MESSAGE_FROM_CONN, libnmdc.EVENT_CONNECTION_STATE_CHANGED:
|
||||
log.Printf("Hub(%s): * %s", msg.hubNick, msg.evt.Message)
|
||||
|
||||
case libnmdc.EVENT_PRIVATE:
|
||||
err := this.DirectMessageTelegramUser(msg.telegramUserId, fmt.Sprintf("PM from user '%s': %s", msg.evt.Nick, msg.evt.Message))
|
||||
if err != nil {
|
||||
log.Printf("Delivering PM to telegram user: %s", err.Error())
|
||||
}
|
||||
|
||||
case libnmdc.EVENT_PUBLIC:
|
||||
if msg.evt.Nick == this.config.HubSecNick {
|
||||
return // ignore
|
||||
}
|
||||
|
||||
// FIXME coalesce from multiple connections
|
||||
htmlMsg := "<b><" + html.EscapeString(msg.evt.Nick) + "></b> " + html.EscapeString(msg.evt.Message)
|
||||
err := this.GroupChatSayHTML(htmlMsg)
|
||||
if err != nil {
|
||||
log.Printf("Delivering public message to group chat: %s", err.Error())
|
||||
}
|
||||
|
||||
case libnmdc.EVENT_USER_JOINED, libnmdc.EVENT_USER_PART, libnmdc.EVENT_USER_UPDATED_INFO, libnmdc.EVENT_USERCOMMAND, libnmdc.EVENT_DEBUG_MESSAGE, libnmdc.EVENT_HUBNAME_CHANGED:
|
||||
// ignore
|
||||
|
||||
default:
|
||||
log.Printf("Hub(%s): Unhandled(%d): %s", msg.hubNick, msg.evt.EventType, msg.evt.Message)
|
||||
|
||||
}
|
||||
log.Printf("Hub: %#v", msg)
|
||||
}
|
||||
|
||||
@ -237,27 +275,60 @@ func (this *NTFServer) HandleMessage(update telegram.Update) {
|
||||
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)
|
||||
}
|
||||
// kickAndDrop deregisters an account and kicks them from the group chat
|
||||
func (this *NTFServer) kickAndDrop(telegramUserId int64) {
|
||||
|
||||
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 {
|
||||
// Hubnick
|
||||
if hubNick, ok := this.config.KnownUsers[telegramUserId]; ok {
|
||||
|
||||
// Close upstream connection (if any)
|
||||
if conn, ok := this.conns[hubNick]; ok {
|
||||
delete(this.conns, hubNick)
|
||||
|
||||
log.Printf("Disconnecting '%s' from hub", hubNick)
|
||||
conn.Disconnect()
|
||||
}
|
||||
|
||||
// Deregister from known users
|
||||
delete(this.config.KnownUsers, telegramUserId)
|
||||
err := this.config.Save(this.configFile)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't save changes when deregistering user: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Kick from telegram groupchat (if logged in)
|
||||
_, err := this.bot.KickChatMember(telegram.KickChatMemberConfig{
|
||||
ChatMemberConfig: telegram.ChatMemberConfig{
|
||||
ChatID: update.Message.Chat.ID,
|
||||
ChatID: this.config.GroupChatID,
|
||||
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())
|
||||
log.Printf("Couldn't kick user from telegram group: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// HandleTelegramUserParted processes a user leaving the groupchat, without deregistering their account
|
||||
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, telegramDisplayName string, update telegram.Update) error {
|
||||
// If known, spawn the upstream connection; if unknown, kick them
|
||||
hubNick, ok := this.config.KnownUsers[telegramUserId]
|
||||
if !ok {
|
||||
log.Printf("Unexpected user '%s' (%d) joined the group chat, kicking...", telegramDisplayName, telegramUserId)
|
||||
this.kickAndDrop(telegramUserId)
|
||||
}
|
||||
|
||||
return nil
|
||||
// Remember which users are currently joined
|
||||
this.config.GroupChatMembers[telegramUserId] = telegramDisplayName
|
||||
err := this.config.Save(this.configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Spawn the upstream connection for this user
|
||||
@ -272,7 +343,7 @@ func (this *NTFServer) HandleGroupMessage(update telegram.Update) error {
|
||||
// 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)
|
||||
err := this.HandleTelegramUserJoined(int64(joinedUser.ID), joinedUser.String(), update)
|
||||
if err != nil {
|
||||
log.Printf("Handling user join: %s", err.Error())
|
||||
}
|
||||
@ -283,77 +354,154 @@ func (this *NTFServer) HandleGroupMessage(update telegram.Update) error {
|
||||
if update.Message.LeftChatMember != nil {
|
||||
// User parted
|
||||
// Close upstream NMDC connection for them
|
||||
log.Printf("Telegram user '%s' (%d) leaving group chat", update.Message.LeftChatMember.String(), update.Message.LeftChatMember.ID)
|
||||
return this.HandleTelegramUserParted(int64(update.Message.LeftChatMember.ID), update)
|
||||
}
|
||||
|
||||
if len(update.Message.Text) > 0 {
|
||||
// Actual chat message
|
||||
// Find the responsible user and send it to the upstream hub using their account
|
||||
// TODO
|
||||
// Find the responsible user and send it to the upstream hub, using their account
|
||||
// FIXME also add it to the coalesce buffer so that we don't replay it from someone else's NMDC connection
|
||||
|
||||
userID := int64(update.Message.From.ID)
|
||||
hubNick, ok := this.config.KnownUsers[userID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't send public message for user '%d' unexpectedly missing hub nick!", userID)
|
||||
}
|
||||
|
||||
conn, ok := this.conns[hubNick]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't send public message for user '%d' (%s) unexpectedly missing upstream connection!", userID, hubNick)
|
||||
}
|
||||
|
||||
conn.SayPublic(update.Message.Text)
|
||||
}
|
||||
|
||||
// TODO probably a file/image upload???
|
||||
// TODO support "editing messages" by re-sending them with a ** suffix
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *NTFServer) ReplyTelegramUser(userID int64, str string, replyToMessageID int) error {
|
||||
chatId, ok := this.config.DirectMessageChats[userID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Can't send telegram message to user '%s': no DM chat known", userID)
|
||||
}
|
||||
|
||||
msg := telegram.NewMessage(chatId, str)
|
||||
if replyToMessageID != 0 {
|
||||
msg.ReplyToMessageID = replyToMessageID
|
||||
}
|
||||
_, err := this.bot.Send(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (this *NTFServer) DirectMessageTelegramUser(userID int64, str string) error {
|
||||
return this.ReplyTelegramUser(userID, str, 0)
|
||||
}
|
||||
|
||||
func (this *NTFServer) GroupChatSayHTML(str string) error {
|
||||
msg := telegram.NewMessage(this.config.GroupChatID, str)
|
||||
msg.ParseMode = telegram.ModeHTML
|
||||
_, err := this.bot.Send(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
// Stash the telegram user ID against this direct-message chat ID so that
|
||||
// we can always reply later on
|
||||
chatID := update.Message.Chat.ID
|
||||
if oldChatID, ok := this.config.DirectMessageChats[userID]; !ok || oldChatID != chatID {
|
||||
this.config.DirectMessageChats[userID] = chatID
|
||||
err := this.config.Save(this.configFile)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't save chat ID %d for user %d", chatID, 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
|
||||
return this.ReplyTelegramUser(userID, str, update.Message.MessageID)
|
||||
}
|
||||
|
||||
// Find out the current status for this chat ID
|
||||
hubNick, isKnown := this.config.KnownUsers[userID]
|
||||
_, isInGroupChat := this.config.GroupChatMembers[userID]
|
||||
|
||||
// Handle the incoming request
|
||||
|
||||
msg := update.Message.Text
|
||||
if strings.HasPrefix(msg, "/pm ") {
|
||||
if !isKnown {
|
||||
if !(isKnown && isInGroupChat) {
|
||||
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 + "'.")
|
||||
conn, ok := this.conns[hubNick]
|
||||
if !ok {
|
||||
return respond("Can't send a native PM (No upstream hub connection)")
|
||||
}
|
||||
|
||||
parts := strings.SplitN(msg, " ", 3)
|
||||
if len(parts) != 3 {
|
||||
return respond("Expected format /pm [recipient] [message] - try again...")
|
||||
}
|
||||
|
||||
if !conn.UserExists(parts[1]) {
|
||||
return respond(fmt.Sprintf("Can't PM offline user '%s'", parts[1]))
|
||||
}
|
||||
|
||||
conn.SayPrivate(parts[1], parts[2])
|
||||
|
||||
} else if strings.HasPrefix(msg, "/join ") {
|
||||
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()))
|
||||
log.Printf("Failed to register user: %s", err.Error())
|
||||
return respond(fmt.Sprintf("Couldn't allow registration because: %s", err.Error()))
|
||||
}
|
||||
|
||||
return respond(fmt.Sprintf("Hi '%s'! You can join %s at %s", requestedHubNick, this.config.HubDescription, this.inviteLink))
|
||||
return respond(fmt.Sprintf("Hi '%s'! You are now registered, and can join %s at %s", requestedHubNick, this.config.HubDescription, this.inviteLink))
|
||||
|
||||
} else if strings.HasPrefix(msg, "/rejoin") {
|
||||
if isKnown && !isInGroupChat {
|
||||
return respond(fmt.Sprintf("Welcome back '%s'! You can join %s at %s", hubNick, this.config.HubDescription, this.inviteLink))
|
||||
|
||||
} else {
|
||||
return respond("You are either already joined (try /quit first), or not yet registered (try /join first).")
|
||||
}
|
||||
|
||||
} else if strings.HasPrefix(msg, "/quit") {
|
||||
|
||||
// TODO
|
||||
return respond("Not yet implemented.")
|
||||
this.kickAndDrop(userID)
|
||||
return respond("Disconnected. You can register again by typing /help .")
|
||||
|
||||
} else { // e.g. /setup or /help
|
||||
} else { // e.g. /start 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"
|
||||
helpMsg += "You are currently registered as: '" + hubNick + "'\n"
|
||||
} else {
|
||||
helpMsg += "You aren't connected yet.\n"
|
||||
}
|
||||
if isInGroupChat {
|
||||
helpMsg += "You are currently in the groupchat.\n"
|
||||
} else {
|
||||
helpMsg += "You haven't joined the groupchat.\n"
|
||||
}
|
||||
|
||||
helpMsg += `
|
||||
|
||||
Available commands:
|
||||
/setup - Welcome message
|
||||
/join [hubnick] - Join ` + this.config.HubDescription + ` as user 'hubnick'
|
||||
/join [hubnick] - Register as 'hubnick' and join ` + this.config.HubDescription + `
|
||||
/rejoin - Re-invite if you are already registered
|
||||
/pm [recipient] [message] - Send a native PM (if connected)
|
||||
/quit - Leave the group chat
|
||||
/quit - Unregister your nick and leave the group chat
|
||||
`
|
||||
|
||||
return respond(helpMsg)
|
||||
|
29
README.txt
29
README.txt
@ -1,20 +1,35 @@
|
||||
A bot to synchronise an NMDC hub with a Telegram supergroup.
|
||||
A bot to synchronise chat between an NMDC/ADC hub and a Telegram supergroup.
|
||||
|
||||
## USAGE
|
||||
Tags: NMDC
|
||||
Written in Go
|
||||
|
||||
Create a new bot
|
||||
## FEATURES
|
||||
|
||||
- NMDC/ADC support
|
||||
- Exclude messages from hub-security nicks
|
||||
- Standalone binary
|
||||
|
||||
## SETUP
|
||||
|
||||
Create a new telegram bot
|
||||
- Use BotFather to create a new bot
|
||||
- Use BotFather to change its privacy mode for group chats
|
||||
- Use BotFather to disable its privacy mode for group chats
|
||||
|
||||
Create a group
|
||||
Create a telegram 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
|
||||
Handover to 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
|
||||
- Leave the group chat (long press on mobile, can't do it on desktop)
|
||||
- Run this bot with -GroupChatID for normal operation
|
||||
|
||||
## USAGE
|
||||
|
||||
Chat with the bot to enter/leave the synchronised channel.
|
||||
|
||||
Sometimes the telegram invite links can take a few minutes to activate, especially if there has been unusual activity (e.g. frequent join/parts)
|
||||
|
20
TODO.txt
20
TODO.txt
@ -1,19 +1,26 @@
|
||||
|
||||
[X] Menu for direct messages
|
||||
[X] Commands for new user registration
|
||||
[ ] If registered/online -- responding to PMs (always require prefix)
|
||||
[X] If registered/online -- responding to PMs (always require prefix)
|
||||
|
||||
[X] Open upstream connections
|
||||
[X] when registered users join the groupchat
|
||||
[X] open existing connections at app startup
|
||||
|
||||
[ ] Handle upstream messages
|
||||
[ ] Coalesce matches from multiple upstream connections?
|
||||
[ ] When recieving a PM - send directly to telegram user account
|
||||
[X] System messages
|
||||
[X] Public messages
|
||||
[ ] Coalesce matches from multiple upstream connections (timer?)
|
||||
[X] Exclude messages from hub-security (custom nick)
|
||||
[ ] Allow sending public messages to DC from inside the group chat
|
||||
[ ] Nick translation
|
||||
[X] Private messages
|
||||
[X] Send directly to telegram user account (even if they're telegramuser-on-dc - telegramuser-on-dc PMs i guess)
|
||||
[X] Allow replying to PMs with /pm [recipient] message
|
||||
|
||||
[ ] Graceful termination
|
||||
[ ] Ability to close nmdc connections
|
||||
[ ] Close nmdc connection && deregister user (with bot message) when user leaves groupchat
|
||||
[X] Graceful termination
|
||||
[X] Ability to close nmdc connections
|
||||
[X] Close nmdc connection && deregister user (with bot message) when user leaves groupchat
|
||||
|
||||
[X] Block unknown users from entering telegram channel
|
||||
|
||||
@ -21,5 +28,6 @@
|
||||
[ ] Convert telegram files/images/videos to raw URLs for NMDC
|
||||
[ ] Bot API does allow downloading - but (?authenticated)? need to re-host (either internally or upload to contented server)
|
||||
[ ] Contented support - convert known URLs with image mime type to telegram images
|
||||
[ ] Including private messages??? is that possible?
|
||||
|
||||
[ ] Publish
|
||||
|
Loading…
Reference in New Issue
Block a user