2018-06-03 03:27:50 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2018-06-03 06:24:08 +00:00
|
|
|
"html"
|
2018-06-03 03:27:50 +00:00
|
|
|
"log"
|
2018-06-03 07:15:16 +00:00
|
|
|
"sort"
|
2018-06-03 04:39:45 +00:00
|
|
|
"strings"
|
2018-06-03 06:49:14 +00:00
|
|
|
"sync"
|
2018-06-03 04:39:45 +00:00
|
|
|
"time"
|
2018-06-03 03:27:50 +00:00
|
|
|
|
|
|
|
"code.ivysaur.me/libnmdc"
|
|
|
|
telegram "github.com/go-telegram-bot-api/telegram-bot-api"
|
|
|
|
)
|
|
|
|
|
2018-06-03 06:49:14 +00:00
|
|
|
const (
|
|
|
|
coalesceWorkerRescanEvery time.Duration = 10 * time.Second
|
|
|
|
coalesceWorkerExpireAfter time.Duration = 20 * time.Second
|
|
|
|
)
|
|
|
|
|
2018-06-03 03:27:50 +00:00
|
|
|
// NTFServer methods all run on the same thread, so no mutexes are needed for field access
|
|
|
|
type NTFServer struct {
|
2018-06-03 04:39:45 +00:00
|
|
|
bot *telegram.BotAPI
|
|
|
|
hubMessages chan upstreamMessage
|
|
|
|
chatName string
|
|
|
|
inviteLink string
|
|
|
|
configFile string
|
|
|
|
config NTFConfig
|
|
|
|
conns map[string]*libnmdc.HubConnection // hubnick -> hubconn
|
2018-06-03 06:51:59 +00:00
|
|
|
verbose bool
|
2018-06-03 06:49:14 +00:00
|
|
|
|
|
|
|
// Except the coalesce buffer, that requires a background worker.
|
|
|
|
coalesceBufferMut sync.Mutex
|
|
|
|
coalesceBuffer map[string]time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
func coalesceKey(hubNick, message string) string {
|
|
|
|
return hubNick + "\x00" + message
|
2018-06-03 03:27:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type upstreamMessage struct {
|
|
|
|
telegramUserId int64
|
2018-06-03 04:39:45 +00:00
|
|
|
hubNick string
|
2018-06-03 03:27:50 +00:00
|
|
|
evt libnmdc.HubEvent
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:51:59 +00:00
|
|
|
func NewNTFServer(configFile string, verbose bool) (*NTFServer, error) {
|
2018-06-03 03:27:50 +00:00
|
|
|
ret := NTFServer{
|
2018-06-03 06:49:14 +00:00
|
|
|
configFile: configFile,
|
|
|
|
hubMessages: make(chan upstreamMessage, 0),
|
|
|
|
conns: make(map[string]*libnmdc.HubConnection),
|
|
|
|
coalesceBuffer: make(map[string]time.Time),
|
2018-06-03 06:51:59 +00:00
|
|
|
verbose: verbose,
|
2018-06-03 03:27:50 +00:00
|
|
|
}
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
// Config
|
|
|
|
|
2018-06-03 03:27:50 +00:00
|
|
|
cfg, err := LoadConfig(configFile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
ret.config = cfg
|
|
|
|
|
2018-06-03 06:49:14 +00:00
|
|
|
// Coalesce background worker
|
|
|
|
go ret.coalesceWorker()
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
// Bot connection
|
|
|
|
|
2018-06-03 03:27:50 +00:00
|
|
|
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
|
|
|
|
|
2018-06-03 06:51:59 +00:00
|
|
|
if ret.verbose {
|
|
|
|
bot.Debug = true
|
|
|
|
}
|
2018-06-03 03:27:50 +00:00
|
|
|
|
|
|
|
log.Printf("Connected to telegram as '%s'", bot.Self.UserName)
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
// Groupchat properties
|
|
|
|
|
2018-06-03 03:27:50 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
// Spawn upstream connections for all pre-existing known users
|
2018-06-04 04:36:45 +00:00
|
|
|
launchedAny := false
|
2018-06-03 04:39:45 +00:00
|
|
|
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 {
|
2018-06-04 04:44:04 +00:00
|
|
|
log.Fatalf("Couldn't reconnect upstream for '%s': %s", hubNick, err.Error()) // fatal - inconsistent DB is the only possible cause
|
2018-06-03 04:39:45 +00:00
|
|
|
}
|
2018-06-04 04:36:45 +00:00
|
|
|
|
|
|
|
launchedAny = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// We have some bad edge cases if a telegram message comes in while the upstream
|
|
|
|
// connection is offline. Wait a bit after startup, to minimise failure cases
|
|
|
|
if launchedAny {
|
|
|
|
time.Sleep(5 * time.Second)
|
2018-06-03 03:27:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &ret, nil
|
|
|
|
}
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
// 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 {
|
2018-06-03 06:24:08 +00:00
|
|
|
if existingHubNick == hubUsername {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
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
|
|
|
|
}
|
2018-06-03 03:27:50 +00:00
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
log.Printf("Connecting to hub '%s' as user '%s'...", this.config.HubAddr, hubUsername)
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
hubUser := libnmdc.NewUserInfo(hubUsername)
|
|
|
|
hubUser.ClientTag = AppName
|
|
|
|
hubUser.ClientVersion = AppVersion
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
conn := libnmdc.ConnectAsync(
|
|
|
|
&libnmdc.HubConnectionOptions{
|
|
|
|
Address: libnmdc.HubAddress(this.config.HubAddr),
|
2018-06-03 06:24:08 +00:00
|
|
|
Self: hubUser,
|
2018-06-03 04:39:45 +00:00
|
|
|
},
|
|
|
|
upstreamChan,
|
|
|
|
)
|
|
|
|
|
|
|
|
// Stash conn so that we can refer to it / close it later
|
|
|
|
this.conns[hubUsername] = conn
|
|
|
|
|
|
|
|
return nil
|
2018-06-03 03:27:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
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)
|
|
|
|
}
|
2018-06-03 03:27:50 +00:00
|
|
|
}
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
return nil // UNREACHABLE
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:49:14 +00:00
|
|
|
func (this *NTFServer) coalesceWorker() {
|
|
|
|
for {
|
|
|
|
time.Sleep(coalesceWorkerRescanEvery)
|
|
|
|
|
|
|
|
deadLine := time.Now().Add(-coalesceWorkerExpireAfter)
|
|
|
|
|
|
|
|
this.coalesceBufferMut.Lock()
|
|
|
|
for k, v := range this.coalesceBuffer {
|
|
|
|
if v.Before(deadLine) {
|
|
|
|
delete(this.coalesceBuffer, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.coalesceBufferMut.Unlock()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Coalesce returns true if the message sticks to an existing one.
|
|
|
|
// It adds it into the coalesce buffer anyway.
|
|
|
|
func (this *NTFServer) Coalesce(hubNick, message string) bool {
|
|
|
|
ckey := coalesceKey(hubNick, message)
|
|
|
|
|
|
|
|
this.coalesceBufferMut.Lock()
|
|
|
|
if _, ok := this.coalesceBuffer[ckey]; ok {
|
|
|
|
this.coalesceBufferMut.Unlock()
|
|
|
|
return true // stuck to existing
|
|
|
|
} else {
|
|
|
|
this.coalesceBuffer[ckey] = time.Now()
|
|
|
|
this.coalesceBufferMut.Unlock()
|
|
|
|
return false // first we've seen it
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
func (this *NTFServer) HandleHubMessage(msg upstreamMessage) {
|
2018-06-03 06:51:59 +00:00
|
|
|
if this.verbose {
|
|
|
|
log.Printf("Hub: %#v", msg)
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:49:14 +00:00
|
|
|
// Coalesce from multiple connections
|
|
|
|
if this.Coalesce(msg.evt.Nick, msg.evt.Message) {
|
|
|
|
return // ignore - we heard this message already recently
|
|
|
|
}
|
|
|
|
|
|
|
|
// Display the message
|
2018-06-03 06:24:08 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
}
|
2018-06-03 03:27:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:51:59 +00:00
|
|
|
if this.verbose {
|
|
|
|
fmt.Printf("%#v\n", update.Message)
|
2018-06-03 03:27:50 +00:00
|
|
|
|
2018-06-03 06:51:59 +00:00
|
|
|
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
|
|
|
|
}
|
2018-06-03 03:27:50 +00:00
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
// kickAndDrop deregisters an account and kicks them from the group chat
|
|
|
|
func (this *NTFServer) kickAndDrop(telegramUserId int64) {
|
|
|
|
|
|
|
|
// 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: this.config.GroupChatID,
|
|
|
|
UserID: int(telegramUserId),
|
|
|
|
},
|
|
|
|
UntilDate: time.Now().Add(24 * time.Hour).Unix(),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Couldn't kick user from telegram group: %s", err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// HandleTelegramUserParted processes a user leaving the groupchat, without deregistering their account
|
2018-06-03 04:39:45 +00:00
|
|
|
func (this *NTFServer) HandleTelegramUserParted(telegramUserId int64, update telegram.Update) error {
|
|
|
|
delete(this.config.GroupChatMembers, telegramUserId)
|
|
|
|
return this.config.Save(this.configFile)
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
func (this *NTFServer) HandleTelegramUserJoined(telegramUserId int64, telegramDisplayName string, update telegram.Update) error {
|
2018-06-03 04:39:45 +00:00
|
|
|
// If known, spawn the upstream connection; if unknown, kick them
|
|
|
|
hubNick, ok := this.config.KnownUsers[telegramUserId]
|
|
|
|
if !ok {
|
2018-06-03 06:24:08 +00:00
|
|
|
log.Printf("Unexpected user '%s' (%d) joined the group chat, kicking...", telegramDisplayName, telegramUserId)
|
|
|
|
this.kickAndDrop(telegramUserId)
|
|
|
|
}
|
2018-06-03 04:39:45 +00:00
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
// Remember which users are currently joined
|
|
|
|
this.config.GroupChatMembers[telegramUserId] = telegramDisplayName
|
|
|
|
err := this.config.Save(this.configFile)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2018-06-03 04:39:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Spawn the upstream connection for this user
|
|
|
|
return this.LaunchUpstreamWorker(telegramUserId, hubNick)
|
|
|
|
}
|
|
|
|
|
2018-06-03 03:27:50 +00:00
|
|
|
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
|
2018-06-03 04:39:45 +00:00
|
|
|
for _, joinedUser := range *update.Message.NewChatMembers {
|
2018-06-03 06:24:08 +00:00
|
|
|
err := this.HandleTelegramUserJoined(int64(joinedUser.ID), joinedUser.String(), update)
|
2018-06-03 04:39:45 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Printf("Handling user join: %s", err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2018-06-03 03:27:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if update.Message.LeftChatMember != nil {
|
|
|
|
// User parted
|
|
|
|
// Close upstream NMDC connection for them
|
2018-06-03 06:24:08 +00:00
|
|
|
log.Printf("Telegram user '%s' (%d) leaving group chat", update.Message.LeftChatMember.String(), update.Message.LeftChatMember.ID)
|
2018-06-03 04:39:45 +00:00
|
|
|
return this.HandleTelegramUserParted(int64(update.Message.LeftChatMember.ID), update)
|
2018-06-03 03:27:50 +00:00
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
if len(update.Message.Text) > 0 {
|
|
|
|
// Actual chat message
|
2018-06-03 06:49:14 +00:00
|
|
|
// Find the responsible user to send it to the upstream hub, using their account
|
2018-06-03 06:24:08 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:49:14 +00:00
|
|
|
// Also add it to the coalesce buffer so that we don't replay it from someone else's NMDC connection
|
|
|
|
this.Coalesce(hubNick, update.Message.Text)
|
|
|
|
|
|
|
|
// Submit to NMDC
|
2018-06-04 04:43:49 +00:00
|
|
|
err := conn.SayPublic(update.Message.Text)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Failed to deliver message '%s': %s", update.Message.Text, err.Error())
|
|
|
|
this.GroupChatSayHTML(fmt.Sprintf("<i>Couldn't sync message '%s' because: %s</i>", html.EscapeString(update.Message.Text), html.EscapeString(err.Error())))
|
|
|
|
}
|
2018-06-03 06:24:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO probably a file/image upload???
|
|
|
|
// TODO support "editing messages" by re-sending them with a ** suffix
|
2018-06-03 03:27:50 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-06-03 03:27:50 +00:00
|
|
|
func (this *NTFServer) HandleDirectMessage(update telegram.Update) error {
|
|
|
|
|
|
|
|
// Registration workflow
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
userID := int64(update.Message.From.ID)
|
2018-06-03 06:24:08 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|
2018-06-03 04:39:45 +00:00
|
|
|
|
|
|
|
respond := func(str string) error {
|
2018-06-03 06:24:08 +00:00
|
|
|
return this.ReplyTelegramUser(userID, str, update.Message.MessageID)
|
2018-06-03 04:39:45 +00:00
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
// Find out the current status for this chat ID
|
|
|
|
hubNick, isKnown := this.config.KnownUsers[userID]
|
|
|
|
_, isInGroupChat := this.config.GroupChatMembers[userID]
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
// Handle the incoming request
|
|
|
|
|
|
|
|
msg := update.Message.Text
|
|
|
|
if strings.HasPrefix(msg, "/pm ") {
|
2018-06-03 06:24:08 +00:00
|
|
|
if !(isKnown && isInGroupChat) {
|
2018-06-03 04:39:45 +00:00
|
|
|
return respond("Can't send a native PM until you've joined.")
|
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
conn, ok := this.conns[hubNick]
|
|
|
|
if !ok {
|
|
|
|
return respond("Can't send a native PM (No upstream hub connection)")
|
|
|
|
}
|
2018-06-03 04:39:45 +00:00
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
parts := strings.SplitN(msg, " ", 3)
|
|
|
|
if len(parts) != 3 {
|
|
|
|
return respond("Expected format /pm [recipient] [message] - try again...")
|
2018-06-03 04:39:45 +00:00
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
if !conn.UserExists(parts[1]) {
|
|
|
|
return respond(fmt.Sprintf("Can't PM offline user '%s'", parts[1]))
|
|
|
|
}
|
|
|
|
|
2018-06-04 04:43:49 +00:00
|
|
|
err := conn.SayPrivate(parts[1], parts[2])
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Sending PM %s -> %s failed because: %s", hubNick, parts[1], err.Error())
|
|
|
|
return respond(fmt.Sprintf("Sending PM failed because: %s", err.Error()))
|
|
|
|
}
|
2018-06-03 06:24:08 +00:00
|
|
|
|
|
|
|
} else if strings.HasPrefix(msg, "/join ") {
|
2018-06-03 04:39:45 +00:00
|
|
|
requestedHubNick := msg[6:]
|
|
|
|
err := this.registerUser(userID, requestedHubNick)
|
|
|
|
if err != nil {
|
2018-06-03 06:24:08 +00:00
|
|
|
log.Printf("Failed to register user: %s", err.Error())
|
|
|
|
return respond(fmt.Sprintf("Couldn't allow registration because: %s", err.Error()))
|
2018-06-03 04:39:45 +00:00
|
|
|
}
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
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).")
|
|
|
|
}
|
2018-06-03 04:39:45 +00:00
|
|
|
|
2018-06-03 07:15:16 +00:00
|
|
|
} else if strings.HasPrefix(msg, "/userlist") {
|
|
|
|
conn, ok := this.conns[hubNick]
|
|
|
|
if !ok {
|
|
|
|
return respond("Can't get userlist for you (No upstream hub connection)")
|
|
|
|
}
|
|
|
|
|
|
|
|
usernames := make([]string, 0)
|
|
|
|
err := conn.Users(func(umap *map[string]libnmdc.UserInfo) error {
|
|
|
|
for k, _ := range *umap {
|
|
|
|
usernames = append(usernames, k)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Error retrieving userlist: %s", err.Error())
|
|
|
|
return respond("Can't get userlist for you (internal error)")
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(usernames)
|
|
|
|
return respond("Online users: " + strings.Join(usernames, " "))
|
|
|
|
|
|
|
|
} else if strings.HasPrefix(msg, "/whois ") {
|
|
|
|
target := msg[7:]
|
|
|
|
conn, ok := this.conns[hubNick]
|
|
|
|
if !ok {
|
|
|
|
return respond("Can't get user details for you (No upstream hub connection)")
|
|
|
|
}
|
|
|
|
|
|
|
|
var userinfo libnmdc.UserInfo
|
|
|
|
var exists bool
|
|
|
|
err := conn.Users(func(umap *map[string]libnmdc.UserInfo) error {
|
|
|
|
userinfo, exists = (*umap)[target]
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Error retrieving userlist: %s", err.Error())
|
|
|
|
return respond("Can't get user details for you (internal error)")
|
|
|
|
}
|
|
|
|
|
|
|
|
if !exists {
|
|
|
|
return respond(fmt.Sprintf("There is no online user '%s'.", target))
|
|
|
|
}
|
|
|
|
|
|
|
|
return respond(fmt.Sprintf("Description: %s\nShare size: %d\nClient: %s %s", userinfo.Description, userinfo.ShareSize, userinfo.ClientTag, userinfo.ClientVersion))
|
|
|
|
|
2018-06-03 04:39:45 +00:00
|
|
|
} else if strings.HasPrefix(msg, "/quit") {
|
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
this.kickAndDrop(userID)
|
|
|
|
return respond("Disconnected. You can register again by typing /help .")
|
2018-06-03 04:39:45 +00:00
|
|
|
|
2018-06-03 06:24:08 +00:00
|
|
|
} else { // e.g. /start or /help
|
2018-06-03 04:39:45 +00:00
|
|
|
// Help
|
|
|
|
helpMsg := `I am a bot that connects Telegram with ` + this.config.HubDescription + ".\n"
|
|
|
|
if isKnown {
|
2018-06-03 06:24:08 +00:00
|
|
|
helpMsg += "You are currently registered as: '" + hubNick + "'\n"
|
2018-06-03 04:39:45 +00:00
|
|
|
} else {
|
|
|
|
helpMsg += "You aren't connected yet.\n"
|
|
|
|
}
|
2018-06-03 06:24:08 +00:00
|
|
|
if isInGroupChat {
|
|
|
|
helpMsg += "You are currently in the groupchat.\n"
|
|
|
|
} else {
|
|
|
|
helpMsg += "You haven't joined the groupchat.\n"
|
|
|
|
}
|
2018-06-03 04:39:45 +00:00
|
|
|
|
|
|
|
helpMsg += `
|
|
|
|
|
|
|
|
Available commands:
|
|
|
|
/setup - Welcome message
|
2018-06-03 06:24:08 +00:00
|
|
|
/join [hubnick] - Register as 'hubnick' and join ` + this.config.HubDescription + `
|
|
|
|
/rejoin - Re-invite if you are already registered
|
2018-06-03 04:39:45 +00:00
|
|
|
/pm [recipient] [message] - Send a native PM (if connected)
|
2018-06-03 07:15:16 +00:00
|
|
|
/userlist - List native online users
|
|
|
|
/whois [hubnick] - Check if native user is online, and read their description
|
2018-06-03 06:24:08 +00:00
|
|
|
/quit - Unregister your nick and leave the group chat
|
2018-06-03 04:39:45 +00:00
|
|
|
`
|
|
|
|
|
|
|
|
return respond(helpMsg)
|
|
|
|
}
|
2018-06-03 03:27:50 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|