initial commit

This commit is contained in:
mappu 2018-06-03 15:27:50 +12:00
commit 5c253575aa
6 changed files with 311 additions and 0 deletions

39
NTFConfig.go Normal file
View File

@ -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)
}

178
NTFServer.go Normal file
View File

@ -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
}

20
README.txt Normal file
View File

@ -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

24
main.go Normal file
View File

@ -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")
}

44
upstreamWorker.go Normal file
View File

@ -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
}
}
}
}

6
version.go Normal file
View File

@ -0,0 +1,6 @@
package main
const (
AppName = "nmdc-telegramfrontend"
AppVersion = "1.0.0"
)