199 lines
5.5 KiB
Go
199 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"runtime/debug"
|
|
"time"
|
|
|
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
|
"github.com/joho/godotenv"
|
|
"github.com/spf13/afero"
|
|
|
|
"github.com/zakirullin/files.md/server"
|
|
"github.com/zakirullin/files.md/server/config"
|
|
"github.com/zakirullin/files.md/server/db"
|
|
"github.com/zakirullin/files.md/server/fs"
|
|
"github.com/zakirullin/files.md/server/pkg/tg"
|
|
"github.com/zakirullin/files.md/server/sync"
|
|
"github.com/zakirullin/files.md/server/userconfig"
|
|
)
|
|
|
|
func main() {
|
|
if err := godotenv.Load(); err != nil {
|
|
fmt.Println("No .env file found, relying on process environment")
|
|
}
|
|
if err := config.LoadBotConfig(); err != nil {
|
|
panic(fmt.Sprintf("Error loading cfg: %s\n", err))
|
|
}
|
|
// Save all renames and deletes to an append-only log.
|
|
fs.LogRename = sync.LogRename
|
|
fs.LogDelete = sync.LogDelete
|
|
|
|
// Launch the HTTP server, it does two things:
|
|
// - Serves the PWA app
|
|
// - Provides the API for synchronization (if API_URL is not empty)
|
|
go sync.Serve(
|
|
config.ServerCfg.APIHost(),
|
|
config.ServerCfg.AppHost(),
|
|
config.ServerCfg.ServerCertDir,
|
|
config.ServerCfg.ServerLogFile,
|
|
)
|
|
|
|
// Telegram bot is optional - server can run as web-only.
|
|
api, err := tgbotapi.NewBotAPI(config.ServerCfg.BotAPIToken)
|
|
if err != nil {
|
|
fmt.Println("No Telegram bot token found, running web and api server only")
|
|
select {} // block forever
|
|
}
|
|
telegram := tg.NewTG(api)
|
|
|
|
// If today or inbox was changed in web app, we need to send the updated items to the bot.
|
|
sync.OnChatUpdate = func(userID int64) { updateBotHome(telegram, userID) }
|
|
|
|
// Due tasks scheduler
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
quit := make(chan struct{})
|
|
defer func(quit chan struct{}) {
|
|
close(quit)
|
|
}(quit)
|
|
go func(tg *tg.TG) {
|
|
fsBackend := afero.NewOsFs()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
err := server.MoveDueTasks(config.ServerCfg.StorageDir, config.ServerCfg.ConfigFilename, fsBackend, telegram)
|
|
if err != nil {
|
|
fmt.Printf("Worker's error: %s\n", err)
|
|
}
|
|
|
|
err = server.RemoveCompletedChecklistItems(config.ServerCfg.StorageDir, config.ServerCfg.ConfigFilename, fsBackend)
|
|
if err != nil {
|
|
fmt.Printf("Worker's error: %s\n", err)
|
|
}
|
|
case <-quit:
|
|
ticker.Stop()
|
|
return
|
|
}
|
|
}
|
|
}(telegram)
|
|
|
|
infolog := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
|
|
// Main bot loop.
|
|
// Loop through updates from Telegram and process them sequentially in separate per-user goroutine.
|
|
userChannels := make(map[int64]chan tgbotapi.Update)
|
|
tgConfig := tgbotapi.NewUpdate(0)
|
|
tgConfig.Timeout = 60 // TODO release, check if it's enough
|
|
updates := api.GetUpdatesChan(tgConfig)
|
|
for update := range updates {
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
slog.Error("Bot panic", "update", update, "err", r, "stacktrace", string(debug.Stack()))
|
|
}
|
|
}()
|
|
|
|
updJSON, _ := json.Marshal(update)
|
|
infolog.Info("Bot update", "update", string(updJSON))
|
|
|
|
var userID int64
|
|
upd := tg.NewTGUpd(update)
|
|
channelID, channelIDExists := upd.ChannelID()
|
|
if channelIDExists {
|
|
userID, err = telegram.ChannelCreatorID(channelID)
|
|
if err != nil {
|
|
slog.Error("Bot error: can't get channel creator ID", "upd", string(updJSON), "err", err)
|
|
}
|
|
} else {
|
|
userID = upd.UserID()
|
|
}
|
|
|
|
userCh, channelIDExists := userChannels[userID]
|
|
if !channelIDExists {
|
|
userCh = make(chan tgbotapi.Update, 100)
|
|
userChannels[userID] = userCh
|
|
go supervisor(userID, userCh, telegram)
|
|
}
|
|
|
|
userCh <- update
|
|
}()
|
|
}
|
|
}
|
|
|
|
// Runs per-user worker that listens for updates.
|
|
// Restarts infinitely upon panics.
|
|
func supervisor(userID int64, updates <-chan tgbotapi.Update, telegram *tg.TG) {
|
|
for {
|
|
func() {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
slog.Error("Bot panic", "userID", userID, "err", err, "stacktrace", string(debug.Stack()))
|
|
}
|
|
}()
|
|
processUserUpdates(userID, updates, telegram)
|
|
}()
|
|
time.Sleep(time.Second)
|
|
slog.Info("Restarting worker", "userID", userID)
|
|
}
|
|
}
|
|
|
|
func processUserUpdates(userID int64, updates <-chan tgbotapi.Update, telegram *tg.TG) {
|
|
for update := range updates {
|
|
upd := tg.NewTGUpd(update)
|
|
|
|
bot, err := newBot(telegram, userID)
|
|
if err != nil {
|
|
slog.Error("Bot error: can't create bot", "err", err)
|
|
return
|
|
}
|
|
|
|
if err := bot.Reply(upd); err != nil {
|
|
if errors.Is(err, tg.ErrFileTooBig) {
|
|
if _, sendErr := telegram.Send(userID, "This file is too big. Telegram allows bots to download files up to 20MB.", nil, ""); sendErr != nil {
|
|
slog.Error("Bot error: can't send file-too-big notice", "err", sendErr)
|
|
}
|
|
}
|
|
slog.Error("Bot error", "err", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newBot(telegram *tg.TG, userID int64) (*server.Bot, error) {
|
|
userFS, err := fs.NewUserFS(userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't create fs: %w", err)
|
|
}
|
|
err = userFS.CreateSystemDirs()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't create user dirs: %w", err)
|
|
}
|
|
|
|
confFilename := config.ServerCfg.ConfigFilename
|
|
userconf := userconfig.NewConfig(userFS, userID, confFilename)
|
|
err = userconf.CreateDefaultIfNotExists()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't create default user config: %w", err)
|
|
}
|
|
|
|
bot := server.NewBot(userID, telegram, userFS, db.NewDB(userID), userconf)
|
|
|
|
return bot, nil
|
|
}
|
|
|
|
func updateBotHome(telegram *tg.TG, userID int64) {
|
|
bot, err := newBot(telegram, userID)
|
|
if err != nil {
|
|
slog.Error("Bot error: can't create bot", "err", err)
|
|
return
|
|
}
|
|
|
|
err = bot.ShowHome(nil)
|
|
if err != nil {
|
|
slog.Error("Server error: can't update bot home", "userID", userID, "err", err)
|
|
}
|
|
}
|