833be688ac
migrating to badger, with the badgerhold frontend. So far, done: * Announcements (small, for a quick test) * Discord/Telegram/Matrix/Email most interaction with badgerhold is done through the standard Get<x>/Get<x>Key/Set<x>Key/Delete<x>Key. UserExists functions have been added for email and matrix, and those and the original ones now use a query against the database rather than sifting through every record. I've tagged these searched fields as "index" for badgerhold, although this definitely isn't used yet, and i'm not entirely sure if it'll be useful. migrateToBadger is now in migrations.go, and a temporary config key "migrated_to_badger" has been added, although it isn't being used yet, migration is just running every time during development.
283 lines
8.4 KiB
Go
283 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
|
"github.com/timshannon/badgerhold/v4"
|
|
)
|
|
|
|
const (
|
|
VERIF_TOKEN_EXPIRY_SEC = 10 * 60
|
|
)
|
|
|
|
type TelegramVerifiedToken struct {
|
|
ChatID int64
|
|
Username string
|
|
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
|
|
}
|
|
|
|
// VerifToken stores details about a pending user verification token.
|
|
type VerifToken struct {
|
|
Expiry time.Time
|
|
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
|
|
}
|
|
|
|
type TelegramDaemon struct {
|
|
Stopped bool
|
|
ShutdownChannel chan string
|
|
bot *tg.BotAPI
|
|
username string
|
|
tokens map[string]VerifToken // Map of pins to tokens.
|
|
verifiedTokens map[string]TelegramVerifiedToken // Map of token pins to the responsible ChatID+Username.
|
|
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
|
|
link string
|
|
app *appContext
|
|
}
|
|
|
|
func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
|
token := app.config.Section("telegram").Key("token").String()
|
|
if token == "" {
|
|
return nil, fmt.Errorf("token was blank")
|
|
}
|
|
bot, err := tg.NewBotAPI(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
td := &TelegramDaemon{
|
|
ShutdownChannel: make(chan string),
|
|
bot: bot,
|
|
username: bot.Self.UserName,
|
|
tokens: map[string]VerifToken{},
|
|
verifiedTokens: map[string]TelegramVerifiedToken{},
|
|
languages: map[int64]string{},
|
|
link: "https://t.me/" + bot.Self.UserName,
|
|
app: app,
|
|
}
|
|
for _, user := range app.storage.GetTelegram() {
|
|
if user.Lang != "" {
|
|
td.languages[user.ChatID] = user.Lang
|
|
}
|
|
}
|
|
return td, nil
|
|
}
|
|
|
|
func genAuthToken() string {
|
|
rand.Seed(time.Now().UnixNano())
|
|
pin := make([]rune, 8)
|
|
for i := range pin {
|
|
if (i+1)%3 == 0 {
|
|
pin[i] = '-'
|
|
} else {
|
|
pin[i] = runes[rand.Intn(len(runes))]
|
|
}
|
|
}
|
|
return string(pin)
|
|
}
|
|
|
|
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
|
|
|
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
|
func (t *TelegramDaemon) NewAuthToken() string {
|
|
pin := genAuthToken()
|
|
t.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: ""}
|
|
return pin
|
|
}
|
|
|
|
// NewAssignedAuthToken generates an 8-character pin in the form "A1-2B-CD",
|
|
// and assigns it for access only with the given Jellyfin ID.
|
|
func (t *TelegramDaemon) NewAssignedAuthToken(id string) string {
|
|
pin := genAuthToken()
|
|
t.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: id}
|
|
return pin
|
|
}
|
|
|
|
func (t *TelegramDaemon) run() {
|
|
t.app.info.Println("Starting Telegram bot daemon")
|
|
u := tg.NewUpdate(0)
|
|
u.Timeout = 60
|
|
updates, err := t.bot.GetUpdatesChan(u)
|
|
if err != nil {
|
|
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
|
|
telegramEnabled = false
|
|
return
|
|
}
|
|
for {
|
|
var upd tg.Update
|
|
select {
|
|
case upd = <-updates:
|
|
if upd.Message == nil {
|
|
continue
|
|
}
|
|
sects := strings.Split(upd.Message.Text, " ")
|
|
if len(sects) == 0 {
|
|
continue
|
|
}
|
|
lang := t.app.storage.lang.chosenTelegramLang
|
|
storedLang, ok := t.languages[upd.Message.Chat.ID]
|
|
if !ok {
|
|
found := false
|
|
for code := range t.app.storage.lang.Telegram {
|
|
if code[:2] == upd.Message.From.LanguageCode {
|
|
lang = code
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
t.languages[upd.Message.Chat.ID] = lang
|
|
}
|
|
} else {
|
|
lang = storedLang
|
|
}
|
|
switch msg := sects[0]; msg {
|
|
case "/start":
|
|
t.commandStart(&upd, sects, lang)
|
|
continue
|
|
case "/lang":
|
|
t.commandLang(&upd, sects, lang)
|
|
continue
|
|
default:
|
|
t.commandPIN(&upd, sects, lang)
|
|
}
|
|
|
|
case <-t.ShutdownChannel:
|
|
t.bot.StopReceivingUpdates()
|
|
t.ShutdownChannel <- "Down"
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *TelegramDaemon) Reply(upd *tg.Update, content string) error {
|
|
msg := tg.NewMessage((*upd).Message.Chat.ID, content)
|
|
_, err := t.bot.Send(msg)
|
|
return err
|
|
}
|
|
|
|
func (t *TelegramDaemon) QuoteReply(upd *tg.Update, content string) error {
|
|
msg := tg.NewMessage((*upd).Message.Chat.ID, content)
|
|
msg.ReplyToMessageID = (*upd).Message.MessageID
|
|
_, err := t.bot.Send(msg)
|
|
return err
|
|
}
|
|
|
|
var escapedChars = []string{"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(", "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>", "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|", "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!"}
|
|
var escaper = strings.NewReplacer(escapedChars...)
|
|
|
|
// Send will send a telegram message to a list of chat IDs. message.text is used if no markdown is given.
|
|
func (t *TelegramDaemon) Send(message *Message, ID ...int64) error {
|
|
for _, id := range ID {
|
|
var msg tg.MessageConfig
|
|
if message.Markdown == "" {
|
|
msg = tg.NewMessage(id, message.Text)
|
|
} else {
|
|
text := escaper.Replace(message.Markdown)
|
|
msg = tg.NewMessage(id, text)
|
|
msg.ParseMode = "MarkdownV2"
|
|
}
|
|
_, err := t.bot.Send(msg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *TelegramDaemon) Shutdown() {
|
|
t.Stopped = true
|
|
t.ShutdownChannel <- "Down"
|
|
<-t.ShutdownChannel
|
|
close(t.ShutdownChannel)
|
|
}
|
|
|
|
func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang string) {
|
|
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
|
content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
|
|
err := t.Reply(upd, content)
|
|
if err != nil {
|
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
|
}
|
|
}
|
|
|
|
func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string) {
|
|
if len(sects) == 1 {
|
|
list := "/lang `<lang>`\n"
|
|
for code := range t.app.storage.lang.Telegram {
|
|
list += fmt.Sprintf("`%s`: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
|
|
}
|
|
err := t.Reply(upd, list)
|
|
if err != nil {
|
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
|
}
|
|
return
|
|
}
|
|
if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok {
|
|
t.languages[upd.Message.Chat.ID] = sects[1]
|
|
for _, user := range t.app.storage.GetTelegram() {
|
|
if user.ChatID == upd.Message.Chat.ID {
|
|
user.Lang = sects[1]
|
|
t.app.storage.SetTelegramKey(user.JellyfinID, user)
|
|
if err := t.app.storage.storeTelegramUsers(); err != nil {
|
|
t.app.err.Printf("Failed to store Telegram users: %v", err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string) {
|
|
token, ok := t.tokens[upd.Message.Text]
|
|
if !ok || time.Now().After(token.Expiry) {
|
|
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
|
|
if err != nil {
|
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
|
}
|
|
delete(t.tokens, upd.Message.Text)
|
|
return
|
|
}
|
|
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
|
|
if err != nil {
|
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
|
}
|
|
t.verifiedTokens[upd.Message.Text] = TelegramVerifiedToken{
|
|
ChatID: upd.Message.Chat.ID,
|
|
Username: upd.Message.Chat.UserName,
|
|
JellyfinID: token.JellyfinID,
|
|
}
|
|
delete(t.tokens, upd.Message.Text)
|
|
}
|
|
|
|
// TokenVerified returns whether or not a token with the given PIN has been verified, and the token itself.
|
|
func (t *TelegramDaemon) TokenVerified(pin string) (token TelegramVerifiedToken, ok bool) {
|
|
token, ok = t.verifiedTokens[pin]
|
|
// delete(t.verifiedTokens, pin)
|
|
return
|
|
}
|
|
|
|
// AssignedTokenVerified returns whether or not a token with the given PIN has been verified, and the token itself.
|
|
// Returns false if the given Jellyfin ID does not match the one in the token.
|
|
func (t *TelegramDaemon) AssignedTokenVerified(pin string, jfID string) (token TelegramVerifiedToken, ok bool) {
|
|
token, ok = t.verifiedTokens[pin]
|
|
if ok && token.JellyfinID != jfID {
|
|
ok = false
|
|
}
|
|
// delete(t.verifiedTokens, pin)
|
|
return
|
|
}
|
|
|
|
// UserExists returns whether or not a user with the given username exists.
|
|
func (t *TelegramDaemon) UserExists(username string) bool {
|
|
c, err := t.app.storage.db.Count(&TelegramUser{}, badgerhold.Where("Username").Eq(username))
|
|
return err != nil || c > 0
|
|
}
|
|
|
|
// DeleteVerifiedToken removes the token with the given PIN.
|
|
func (t *TelegramDaemon) DeleteVerifiedToken(pin string) {
|
|
delete(t.verifiedTokens, pin)
|
|
}
|