mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-10-31 23:40:11 +00:00
Harvey Tindall
e4a7172517
pins generated on the user page are assigned to that user, no other jellyifn user can verify them.
288 lines
8.4 KiB
Go
288 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
|
)
|
|
|
|
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 jfID, user := range t.app.storage.GetTelegram() {
|
|
if user.ChatID == upd.Message.Chat.ID {
|
|
user.Lang = sects[1]
|
|
t.app.storage.SetTelegramKey(jfID, 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) (ok bool) {
|
|
ok = false
|
|
for _, u := range t.app.storage.GetTelegram() {
|
|
if u.Username == username {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// DeleteVerifiedToken removes the token with the given PIN.
|
|
func (t *TelegramDaemon) DeleteVerifiedToken(pin string) {
|
|
delete(t.verifiedTokens, pin)
|
|
}
|