package laniakea import ( "fmt" "os" "sort" "strings" "time" "git.nix13.pw/scuroneko/extypes" "git.nix13.pw/scuroneko/laniakea/tgapi" "git.nix13.pw/scuroneko/slog" ) type BotOpts struct { Token string Debug bool ErrorTemplate string Prefixes []string UpdateTypes []string LoggerBasePath string UseRequestLogger bool WriteToFile bool UseTestServer bool APIUrl string } func LoadOptsFromEnv() *BotOpts { return &BotOpts{ Token: os.Getenv("TG_TOKEN"), Debug: os.Getenv("DEBUG") == "true", ErrorTemplate: os.Getenv("ERROR_TEMPLATE"), Prefixes: LoadPrefixesFromEnv(), UpdateTypes: strings.Split(os.Getenv("UPDATE_TYPES"), ";"), UseRequestLogger: os.Getenv("USE_REQ_LOG") == "true", WriteToFile: os.Getenv("WRITE_TO_FILE") == "true", UseTestServer: os.Getenv("USE_TEST_SERVER") == "true", APIUrl: os.Getenv("API_URL"), } } func LoadPrefixesFromEnv() []string { prefixesS, exists := os.LookupEnv("PREFIXES") if !exists { return []string{"/"} } return strings.Split(prefixesS, ";") } type DbContext interface{} type Bot[T DbContext] struct { token string debug bool errorTemplate string logger *slog.Logger RequestLogger *slog.Logger plugins []Plugin[T] middlewares []Middleware[T] prefixes []string runners []Runner[T] api *tgapi.API uploader *tgapi.Uploader dbContext *T l10n *L10n dbWriterRequested extypes.Slice[*slog.Logger] updateOffset int updateTypes []tgapi.UpdateType updateQueue *extypes.Queue[*tgapi.Update] } func NewBot[T any](opts *BotOpts) *Bot[T] { updateQueue := extypes.CreateQueue[*tgapi.Update](512) apiOpts := tgapi.NewAPIOpts(opts.Token).SetAPIUrl(opts.APIUrl).UseTestServer(opts.UseTestServer) api := tgapi.NewAPI(apiOpts) uploader := tgapi.NewUploader(api) bot := &Bot[T]{ updateOffset: 0, errorTemplate: "%s", updateQueue: updateQueue, api: api, uploader: uploader, debug: opts.Debug, prefixes: opts.Prefixes, token: opts.Token, plugins: make([]Plugin[T], 0), updateTypes: make([]tgapi.UpdateType, 0), runners: make([]Runner[T], 0), dbWriterRequested: make([]*slog.Logger, 0), l10n: &L10n{}, } bot.dbWriterRequested = bot.dbWriterRequested.Push(api.GetLogger()).Push(uploader.GetLogger()) if len(opts.ErrorTemplate) > 0 { bot.errorTemplate = opts.ErrorTemplate } if len(opts.LoggerBasePath) == 0 { opts.LoggerBasePath = "./" } bot.initLoggers(opts) u, err := api.GetMe() if err != nil { _ = api.CloseApi() _ = uploader.Close() bot.logger.Fatal(err) } bot.logger.Infof("Authorized as %s\n", u.FirstName) return bot } func (bot *Bot[T]) Close() error { if err := bot.uploader.Close(); err != nil { bot.logger.Errorln(err) } if err := bot.api.CloseApi(); err != nil { bot.logger.Errorln(err) } if err := bot.RequestLogger.Close(); err != nil { bot.logger.Errorln(err) } if err := bot.logger.Close(); err != nil { return err } return nil } func (bot *Bot[T]) initLoggers(opts *BotOpts) { level := slog.FATAL if opts.Debug { level = slog.DEBUG } bot.logger = slog.CreateLogger().Level(level).Prefix("BOT") bot.logger.AddWriter(bot.logger.CreateJsonStdoutWriter()) if opts.WriteToFile { path := fmt.Sprintf("%s/main.log", strings.TrimRight(opts.LoggerBasePath, "/")) fileWriter, err := bot.logger.CreateTextFileWriter(path) if err != nil { bot.logger.Fatal(err) } bot.logger.AddWriter(fileWriter) } if opts.UseRequestLogger { bot.RequestLogger = slog.CreateLogger().Level(level).Prefix("REQUESTS") bot.RequestLogger.AddWriter(bot.RequestLogger.CreateJsonStdoutWriter()) if opts.WriteToFile { path := fmt.Sprintf("%s/requests.log", strings.TrimRight(opts.LoggerBasePath, "/")) fileWriter, err := bot.RequestLogger.CreateTextFileWriter(path) if err != nil { bot.logger.Fatal(err) } bot.RequestLogger.AddWriter(fileWriter) } } } func (bot *Bot[T]) GetUpdateOffset() int { return bot.updateOffset } func (bot *Bot[T]) SetUpdateOffset(offset int) { bot.updateOffset = offset } func (bot *Bot[T]) GetUpdateTypes() []tgapi.UpdateType { return bot.updateTypes } func (bot *Bot[T]) GetQueue() *extypes.Queue[*tgapi.Update] { return bot.updateQueue } func (bot *Bot[T]) GetLogger() *slog.Logger { return bot.logger } func (bot *Bot[T]) GetDBContext() *T { return bot.dbContext } func (bot *Bot[T]) L10n(lang, key string) string { return bot.l10n.Translate(lang, key) } func (bot *Bot[T]) AddDatabaseLogger(writer func(db *T) slog.LoggerWriter) *Bot[T] { w := writer(bot.dbContext) bot.logger.AddWriter(w) if bot.RequestLogger != nil { bot.RequestLogger.AddWriter(w) } for _, l := range bot.dbWriterRequested { l.AddWriter(w) } return bot } func (bot *Bot[T]) DatabaseContext(ctx *T) *Bot[T] { bot.dbContext = ctx return bot } func (bot *Bot[T]) UpdateTypes(t ...tgapi.UpdateType) *Bot[T] { bot.updateTypes = make([]tgapi.UpdateType, 0) bot.updateTypes = append(bot.updateTypes, t...) return bot } func (bot *Bot[T]) AddUpdateType(t ...tgapi.UpdateType) *Bot[T] { bot.updateTypes = append(bot.updateTypes, t...) return bot } func (bot *Bot[T]) AddPrefixes(prefixes ...string) *Bot[T] { bot.prefixes = append(bot.prefixes, prefixes...) return bot } func (bot *Bot[T]) ErrorTemplate(s string) *Bot[T] { bot.errorTemplate = s return bot } func (bot *Bot[T]) Debug(debug bool) *Bot[T] { bot.debug = debug return bot } func (bot *Bot[T]) AddPlugins(plugin ...*Plugin[T]) *Bot[T] { for _, p := range plugin { bot.plugins = append(bot.plugins, *p) bot.logger.Debugln(fmt.Sprintf("plugins with name \"%s\" registered", p.Name)) } return bot } func (bot *Bot[T]) AddMiddleware(middleware ...Middleware[T]) *Bot[T] { bot.middlewares = append(bot.middlewares, middleware...) for _, m := range middleware { bot.logger.Debugln(fmt.Sprintf("middleware with name \"%s\" registered", m.name)) } sort.Slice(bot.middlewares, func(i, j int) bool { first := bot.middlewares[i] second := bot.middlewares[j] if first.order == second.order { return first.name < second.name } return first.order < second.order }) return bot } func (bot *Bot[T]) AddRunner(runner Runner[T]) *Bot[T] { bot.runners = append(bot.runners, runner) bot.logger.Debugln(fmt.Sprintf("runner with name \"%s\" registered", runner.name)) return bot } func (bot *Bot[T]) AddL10n(l *L10n) *Bot[T] { bot.l10n = l return bot } func (bot *Bot[T]) Run() { if len(bot.prefixes) == 0 { bot.logger.Fatalln("no prefixes defined") return } if len(bot.plugins) == 0 { bot.logger.Fatalln("no plugins defined") return } bot.ExecRunners() bot.logger.Infoln("Bot running. Press CTRL+C to exit.") go func() { for { _, err := bot.Updates() if err != nil { bot.logger.Errorln(err) } } }() for { queue := bot.updateQueue if queue.IsEmpty() { time.Sleep(time.Millisecond * 25) continue } u := queue.Dequeue() if u == nil { bot.logger.Errorln("update is nil") continue } bot.handle(u) } }