From 518435fcd332c61a5b71abc445479a58e8e77549 Mon Sep 17 00:00:00 2001 From: Bhunter <180028024+bhunter234@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:11:51 +0100 Subject: [PATCH] refactor: replace kv usage with boltdb and update related configurations --- cmd/run.go | 270 +++++++++-------------- go.mod | 25 ++- go.sum | 92 ++++++-- internal/auth/auth.go | 6 +- internal/cache/cache.go | 10 +- internal/config/config.go | 280 ++++++++++++++++++------ internal/database/database.go | 22 +- internal/database/database_util.go | 12 +- internal/duration/duration.go | 11 +- internal/duration/duration_test.go | 2 +- internal/kv/bolt.go | 76 ------- internal/kv/key.go | 9 - internal/kv/kv.go | 32 --- internal/kv/session.go | 41 ---- internal/pool/pool.go | 3 +- internal/reader/tg_multi_reader_test.go | 142 ------------ internal/tgc/bolt.go | 36 +++ internal/tgc/connect.go | 78 ------- internal/tgc/helpers.go | 8 +- internal/tgc/tgc.go | 20 +- main.go | 3 +- pkg/cron/cron.go | 4 +- pkg/services/api_service.go | 12 +- pkg/services/file.go | 2 +- pkg/services/upload.go | 2 +- pkg/services/user.go | 57 +++-- 26 files changed, 525 insertions(+), 730 deletions(-) delete mode 100644 internal/kv/bolt.go delete mode 100644 internal/kv/key.go delete mode 100644 internal/kv/kv.go delete mode 100644 internal/kv/session.go delete mode 100644 internal/reader/tg_multi_reader_test.go create mode 100644 internal/tgc/bolt.go delete mode 100644 internal/tgc/connect.go diff --git a/cmd/run.go b/cmd/run.go index 75ae8c2..3eecf79 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -5,24 +5,15 @@ import ( "fmt" "net" "net/http" - "os" - "os/signal" - "path/filepath" - "reflect" "regexp" "strings" - "syscall" "time" - "unicode" "github.com/go-chi/chi/v5" chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-co-op/gocron" - "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" "github.com/tgdrive/teldrive/internal/api" "github.com/tgdrive/teldrive/internal/appcontext" "github.com/tgdrive/teldrive/internal/auth" @@ -31,12 +22,11 @@ import ( "github.com/tgdrive/teldrive/internal/config" "github.com/tgdrive/teldrive/internal/database" "github.com/tgdrive/teldrive/internal/duration" - "github.com/tgdrive/teldrive/internal/kv" "github.com/tgdrive/teldrive/internal/logging" "github.com/tgdrive/teldrive/internal/middleware" "github.com/tgdrive/teldrive/internal/tgc" - "github.com/tgdrive/teldrive/internal/utils" "github.com/tgdrive/teldrive/ui" + "go.etcd.io/bbolt" "github.com/tgdrive/teldrive/pkg/cron" "github.com/tgdrive/teldrive/pkg/services" @@ -46,85 +36,95 @@ import ( ) func NewRun() *cobra.Command { - config := config.Config{} - runCmd := &cobra.Command{ + var cfg config.ServerCmdConfig + cmd := &cobra.Command{ Use: "run", Short: "Start Teldrive Server", Run: func(cmd *cobra.Command, args []string) { - runApplication(&config) + runApplication(cmd.Context(), &cfg) }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return initViperConfig(cmd) + loader := config.NewConfigLoader() + if err := loader.InitializeConfig(cmd); err != nil { + return err + } + if err := loader.Load(&cfg); err != nil { + return err + } + if err := checkRequiredRunFlags(&cfg); err != nil { + return err + } + return nil }, } - - runCmd.Flags().StringP("config", "c", "", "Config file path (default $HOME/.teldrive/config.toml)") - runCmd.Flags().IntVarP(&config.Server.Port, "server-port", "p", 8080, "Server port") - duration.DurationVar(runCmd.Flags(), &config.Server.GracefulShutdown, "server-graceful-shutdown", 10*time.Second, "Server graceful shutdown timeout") - runCmd.Flags().BoolVar(&config.Server.EnablePprof, "server-enable-pprof", false, "Enable Pprof Profiling") - duration.DurationVar(runCmd.Flags(), &config.Server.ReadTimeout, "server-read-timeout", 1*time.Hour, "Server read timeout") - duration.DurationVar(runCmd.Flags(), &config.Server.WriteTimeout, "server-write-timeout", 1*time.Hour, "Server write timeout") - - runCmd.Flags().BoolVar(&config.CronJobs.Enable, "cronjobs-enable", true, "Run cron jobs") - duration.DurationVar(runCmd.Flags(), &config.CronJobs.CleanFilesInterval, "cronjobs-clean-files-interval", 1*time.Hour, "Clean files interval") - duration.DurationVar(runCmd.Flags(), &config.CronJobs.CleanUploadsInterval, "cronjobs-clean-uploads-interval", 12*time.Hour, "Clean uploads interval") - duration.DurationVar(runCmd.Flags(), &config.CronJobs.FolderSizeInterval, "cronjobs-folder-size-interval", 2*time.Hour, "Folder size update interval") - - runCmd.Flags().IntVar(&config.Cache.MaxSize, "cache-max-size", 10*1024*1024, "Max Cache max size (memory)") - runCmd.Flags().StringVar(&config.Cache.RedisAddr, "cache-redis-addr", "", "Redis address") - runCmd.Flags().StringVar(&config.Cache.RedisPass, "cache-redis-pass", "", "Redis password") - - runCmd.Flags().IntVarP(&config.Log.Level, "log-level", "", -1, "Logging level") - runCmd.Flags().StringVar(&config.Log.File, "log-file", "", "Logging file path") - runCmd.Flags().BoolVar(&config.Log.Development, "log-development", false, "Enable development mode") - - runCmd.Flags().StringVar(&config.JWT.Secret, "jwt-secret", "", "JWT secret key") - duration.DurationVar(runCmd.Flags(), &config.JWT.SessionTime, "jwt-session-time", (30*24)*time.Hour, "JWT session duration") - runCmd.Flags().StringSliceVar(&config.JWT.AllowedUsers, "jwt-allowed-users", []string{}, "Allowed users") - - runCmd.Flags().StringVar(&config.DB.DataSource, "db-data-source", "", "Database connection string") - runCmd.Flags().IntVar(&config.DB.LogLevel, "db-log-level", 1, "Database log level") - runCmd.Flags().BoolVar(&config.DB.PrepareStmt, "db-prepare-stmt", true, "Enable prepared statements") - runCmd.Flags().BoolVar(&config.DB.Pool.Enable, "db-pool-enable", true, "Enable database pool") - runCmd.Flags().IntVar(&config.DB.Pool.MaxIdleConnections, "db-pool-max-open-connections", 25, "Database max open connections") - runCmd.Flags().IntVar(&config.DB.Pool.MaxIdleConnections, "db-pool-max-idle-connections", 25, "Database max idle connections") - duration.DurationVar(runCmd.Flags(), &config.DB.Pool.MaxLifetime, "db-pool-max-lifetime", 10*time.Minute, "Database max connection lifetime") - - runCmd.Flags().IntVar(&config.TG.AppId, "tg-app-id", 0, "Telegram app ID") - runCmd.Flags().StringVar(&config.TG.AppHash, "tg-app-hash", "", "Telegram app hash") - runCmd.Flags().StringVar(&config.TG.SessionFile, "tg-session-file", "", "Bot session file path") - runCmd.Flags().BoolVar(&config.TG.RateLimit, "tg-rate-limit", true, "Enable rate limiting for telegram client") - runCmd.Flags().IntVar(&config.TG.RateBurst, "tg-rate-burst", 5, "Limiting burst for telegram client") - runCmd.Flags().IntVar(&config.TG.Rate, "tg-rate", 100, "Limiting rate for telegram client") - runCmd.Flags().StringVar(&config.TG.DeviceModel, "tg-device-model", - "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0", "Device model") - runCmd.Flags().StringVar(&config.TG.SystemVersion, "tg-system-version", "Win32", "System version") - runCmd.Flags().StringVar(&config.TG.AppVersion, "tg-app-version", "4.6.3 K", "App version") - runCmd.Flags().StringVar(&config.TG.LangCode, "tg-lang-code", "en", "Language code") - runCmd.Flags().StringVar(&config.TG.SystemLangCode, "tg-system-lang-code", "en-US", "System language code") - runCmd.Flags().StringVar(&config.TG.LangPack, "tg-lang-pack", "webk", "Language pack") - runCmd.Flags().StringVar(&config.TG.Proxy, "tg-proxy", "", "HTTP OR SOCKS5 proxy URL") - runCmd.Flags().BoolVar(&config.TG.DisableStreamBots, "tg-disable-stream-bots", false, "Disable Stream bots") - runCmd.Flags().BoolVar(&config.TG.Ntp, "tg-ntp", false, "Use NTP server time") - runCmd.Flags().BoolVar(&config.TG.EnableLogging, "tg-enable-logging", false, "Enable telegram client logging") - runCmd.Flags().StringVar(&config.TG.Uploads.EncryptionKey, "tg-uploads-encryption-key", "", "Uploads encryption key") - runCmd.Flags().IntVar(&config.TG.Uploads.Threads, "tg-uploads-threads", 8, "Uploads threads") - runCmd.Flags().IntVar(&config.TG.Uploads.MaxRetries, "tg-uploads-max-retries", 10, "Uploads Retries") - runCmd.Flags().Int64Var(&config.TG.PoolSize, "tg-pool-size", 8, "Telegram Session pool size") - duration.DurationVar(runCmd.Flags(), &config.TG.ReconnectTimeout, "tg-reconnect-timeout", 5*time.Minute, "Reconnect Timeout") - duration.DurationVar(runCmd.Flags(), &config.TG.Uploads.Retention, "tg-uploads-retention", (24*7)*time.Hour, "Uploads retention duration") - duration.DurationVar(runCmd.Flags(), &config.TG.BgBotsCheckInterval, "tg-bg-bots-check-interval", 4*time.Hour, "Interval for checking Idle background bots") - runCmd.Flags().IntVar(&config.TG.Stream.MultiThreads, "tg-stream-multi-threads", 0, "Stream multi-threads") - runCmd.Flags().IntVar(&config.TG.Stream.Buffers, "tg-stream-buffers", 8, "No of Stream buffers") - duration.DurationVar(runCmd.Flags(), &config.TG.Stream.ChunkTimeout, "tg-stream-chunk-timeout", 20*time.Second, "Chunk Fetch Timeout") - runCmd.MarkFlagRequired("tg-app-id") - runCmd.MarkFlagRequired("tg-app-hash") - runCmd.MarkFlagRequired("db-data-source") - runCmd.MarkFlagRequired("jwt-secret") - - return runCmd + addServerFlags(cmd, &cfg) + return cmd } +func addServerFlags(cmd *cobra.Command, cfg *config.ServerCmdConfig) { + + flags := cmd.Flags() + + config.AddCommonFlags(flags, cfg) + + // Server config + flags.IntVarP(&cfg.Server.Port, "server-port", "p", 8080, "Server port") + duration.DurationVar(flags, &cfg.Server.GracefulShutdown, "server-graceful-shutdown", 10*time.Second, "Server graceful shutdown timeout") + flags.BoolVar(&cfg.Server.EnablePprof, "server-enable-pprof", false, "Enable Pprof Profiling") + duration.DurationVar(flags, &cfg.Server.ReadTimeout, "server-read-timeout", 1*time.Hour, "Server read timeout") + duration.DurationVar(flags, &cfg.Server.WriteTimeout, "server-write-timeout", 1*time.Hour, "Server write timeout") + + // CronJobs config + flags.BoolVar(&cfg.CronJobs.Enable, "cronjobs-enable", true, "Run cron jobs") + duration.DurationVar(flags, &cfg.CronJobs.CleanFilesInterval, "cronjobs-clean-files-interval", 1*time.Hour, "Clean files interval") + duration.DurationVar(flags, &cfg.CronJobs.CleanUploadsInterval, "cronjobs-clean-uploads-interval", 12*time.Hour, "Clean uploads interval") + duration.DurationVar(flags, &cfg.CronJobs.FolderSizeInterval, "cronjobs-folder-size-interval", 2*time.Hour, "Folder size update interval") + + // Cache config + flags.IntVar(&cfg.Cache.MaxSize, "cache-max-size", 10*1024*1024, "Max Cache max size (memory)") + flags.StringVar(&cfg.Cache.RedisAddr, "cache-redis-addr", "", "Redis address") + flags.StringVar(&cfg.Cache.RedisPass, "cache-redis-pass", "", "Redis password") + + // JWT config + flags.StringVar(&cfg.JWT.Secret, "jwt-secret", "", "JWT secret key") + duration.DurationVar(flags, &cfg.JWT.SessionTime, "jwt-session-time", (30*24)*time.Hour, "JWT session duration") + flags.StringSliceVar(&cfg.JWT.AllowedUsers, "jwt-allowed-users", []string{}, "Allowed users") + + // Telegram Uploads config + flags.StringVar(&cfg.TG.Uploads.EncryptionKey, "tg-uploads-encryption-key", "", "Uploads encryption key") + flags.IntVar(&cfg.TG.Uploads.Threads, "tg-uploads-threads", 8, "Uploads threads") + flags.IntVar(&cfg.TG.Uploads.MaxRetries, "tg-uploads-max-retries", 10, "Uploads Retries") + duration.DurationVar(flags, &cfg.TG.ReconnectTimeout, "tg-reconnect-timeout", 5*time.Minute, "Reconnect Timeout") + duration.DurationVar(flags, &cfg.TG.Uploads.Retention, "tg-uploads-retention", (24*7)*time.Hour, "Uploads retention duration") + flags.IntVar(&cfg.TG.Stream.MultiThreads, "tg-stream-multi-threads", 0, "Stream multi-threads") + flags.IntVar(&cfg.TG.Stream.Buffers, "tg-stream-buffers", 8, "No of Stream buffers") + duration.DurationVar(flags, &cfg.TG.Stream.ChunkTimeout, "tg-stream-chunk-timeout", 20*time.Second, "Chunk Fetch Timeout") + +} + +func checkRequiredRunFlags(cfg *config.ServerCmdConfig) error { + var missingFields []string + + if cfg.DB.DataSource == "" { + missingFields = append(missingFields, "db-data-source") + } + if cfg.JWT.Secret == "" { + missingFields = append(missingFields, "jwt-secret") + } + if cfg.TG.AppHash == "" { + missingFields = append(missingFields, "tg-app-hash") + } + if cfg.TG.AppId == 0 { + missingFields = append(missingFields, "tg-app-id") + } + + if len(missingFields) > 0 { + return fmt.Errorf("required configuration values not set: %s", strings.Join(missingFields, ", ")) + } + + return nil +} + func findAvailablePort(startPort int) (int, error) { for port := startPort; port < startPort+100; port++ { addr := fmt.Sprintf(":%d", port) @@ -138,21 +138,16 @@ func findAvailablePort(startPort int) (int, error) { return 0, fmt.Errorf("no available ports found between %d and %d", startPort, startPort+100) } -func runApplication(conf *config.Config) { +func runApplication(ctx context.Context, conf *config.ServerCmdConfig) { logging.SetConfig(&logging.Config{ Level: zapcore.Level(conf.Log.Level), Development: conf.Log.Development, FilePath: conf.Log.File, }) - ctx, cancel := context.WithCancel(context.Background()) - lg := logging.DefaultLogger().Sugar() - defer func() { - logging.DefaultLogger().Sync() - cancel() - }() + defer lg.Sync() port, err := findAvailablePort(conf.Server.Port) if err != nil { @@ -165,23 +160,29 @@ func runApplication(conf *config.Config) { scheduler := gocron.NewScheduler(time.UTC) - cacher := cache.NewCache(ctx, conf) + cacher := cache.NewCache(ctx, &conf.Cache) - db, err := database.NewDatabase(conf, lg) + db, err := database.NewDatabase(&conf.DB, lg) if err != nil { lg.Fatalw("failed to create database", "err", err) } - kv := kv.NewBoltKV(conf) + err = database.MigrateDB(db) + + if err != nil { + lg.Fatalw("failed to migrate database", "err", err) + } + + boltDb, err := tgc.NewBoltDB(conf.TG.SessionFile) + + if err != nil { + lg.Fatalw("failed to create bolt db", "err", err) + } worker := tgc.NewBotWorker() - srv := setupServer(conf, db, cacher, kv, worker) - - stop := make(chan os.Signal, 1) - - signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + srv := setupServer(conf, db, cacher, boltDb, worker) cron.StartCronJobs(scheduler, db, conf) @@ -192,7 +193,7 @@ func runApplication(conf *config.Config) { } }() - <-stop + <-ctx.Done() lg.Info("Shutting down server...") @@ -209,13 +210,13 @@ func runApplication(conf *config.Config) { lg.Info("Server stopped") } -func setupServer(cfg *config.Config, db *gorm.DB, cache cache.Cacher, kv kv.KV, worker *tgc.BotWorker) *http.Server { +func setupServer(cfg *config.ServerCmdConfig, db *gorm.DB, cache cache.Cacher, boltdb *bbolt.DB, worker *tgc.BotWorker) *http.Server { lg := logging.DefaultLogger() - apiSrv := services.NewApiService(db, cfg, cache, kv, worker) + apiSrv := services.NewApiService(db, cfg, cache, boltdb, worker) - srv, err := api.NewServer(apiSrv, auth.NewSecurityHandler(db, cache, cfg)) + srv, err := api.NewServer(apiSrv, auth.NewSecurityHandler(db, cache, &cfg.JWT)) if err != nil { lg.Fatal("failed to create server", zap.Error(err)) @@ -254,72 +255,3 @@ func setupServer(cfg *config.Config, db *gorm.DB, cache cache.Cacher, kv kv.KV, IdleTimeout: 60 * time.Second, } } - -func initViperConfig(cmd *cobra.Command) error { - - viper.SetConfigType("toml") - - cfgFile := cmd.Flags().Lookup("config").Value.String() - - if cfgFile != "" { - viper.SetConfigFile(cfgFile) - } else { - home, _ := homedir.Dir() - viper.AddConfigPath(filepath.Join(home, ".teldrive")) - viper.AddConfigPath(".") - viper.AddConfigPath(utils.ExecutableDir()) - viper.SetConfigName("config") - } - - viper.SetEnvPrefix("teldrive") - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - viper.AutomaticEnv() - viper.ReadInConfig() - bindFlags(cmd.Flags(), "", reflect.ValueOf(config.Config{})) - return nil - -} -func bindFlags(flags *pflag.FlagSet, prefix string, v reflect.Value) { - t := v.Type() - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - for i := range t.NumField() { - field := t.Field(i) - switch field.Type.Kind() { - case reflect.Struct: - bindFlags(flags, fmt.Sprintf("%s.%s", prefix, strings.ToLower(field.Name)), v.Field(i)) - default: - newPrefix := prefix[1:] - newName := modifyFlag(field.Name) - configName := fmt.Sprintf("%s.%s", newPrefix, newName) - flag := flags.Lookup(fmt.Sprintf("%s-%s", strings.ReplaceAll(newPrefix, ".", "-"), newName)) - if !flag.Changed && viper.IsSet(configName) { - confVal := viper.Get(configName) - if field.Type.Kind() == reflect.Slice { - sliceValue, ok := confVal.([]interface{}) - if ok { - for _, v := range sliceValue { - flag.Value.Set(fmt.Sprintf("%v", v)) - } - } - } else { - flags.Set(flag.Name, fmt.Sprintf("%v", confVal)) - } - } - } - } -} - -func modifyFlag(s string) string { - var result []rune - - for i, c := range s { - if i > 0 && unicode.IsUpper(c) { - result = append(result, '-') - } - result = append(result, unicode.ToLower(c)) - } - - return string(result) -} diff --git a/go.mod b/go.mod index 4b24f23..116c0ae 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,10 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/gotd/contrib v0.21.0 - github.com/gotd/td v0.116.0 + github.com/gotd/td v0.117.0 github.com/iyear/connectproxy v0.1.1 github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/mapstructure v1.5.0 github.com/ogen-go/ogen v1.8.1 github.com/redis/go-redis/v9 v9.7.0 github.com/spf13/cobra v1.8.1 @@ -22,7 +23,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 go.etcd.io/bbolt v1.3.11 go.uber.org/zap v1.27.0 - golang.org/x/time v0.8.0 + golang.org/x/time v0.9.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/datatypes v1.2.5 gorm.io/driver/postgres v1.5.11 @@ -46,23 +47,23 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/magiconair/properties v1.8.9 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/sagikazarmark/locafero v0.6.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect + go.opentelemetry.io/otel/metric v1.33.0 // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/tools v0.28.0 // indirect + golang.org/x/tools v0.29.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gorm.io/driver/mysql v1.5.7 // indirect @@ -85,17 +86,17 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/pressly/goose/v3 v3.24.0 + github.com/pressly/goose/v3 v3.24.1 github.com/segmentio/asm v1.2.0 // indirect github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.33.0 // indirect go.opentelemetry.io/otel/trace v1.33.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 - golang.org/x/crypto v0.31.0 - golang.org/x/net v0.33.0 + golang.org/x/crypto v0.32.0 + golang.org/x/net v0.34.0 golang.org/x/sync v0.10.0 - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect diff --git a/go.sum b/go.sum index 0212f13..134b84d 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,14 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/WinterYukky/gorm-extra-clause-plugin v0.3.0 h1:fQfTkxoRso6mlm7eOfBIk94aqamJeqxCEfU+MyLWhgo= github.com/WinterYukky/gorm-extra-clause-plugin v0.3.0/go.mod h1:GFT8TzxeeGKYXNU/65PsiN2+zNHigm9HjybnbL1T7eg= github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -15,6 +19,18 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA= +github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M= @@ -37,6 +53,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= @@ -54,15 +72,23 @@ github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38= github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ= github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I= github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -76,8 +102,8 @@ github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk= github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0= github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ= github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ= -github.com/gotd/td v0.116.0 h1:MSZZ4lrfn6wDSYyVSKLvFDtBzBOmb9jv0MUBw/PiIgg= -github.com/gotd/td v0.116.0/go.mod h1:G65yEk83sFJU9s0xowP3dDtvXsTp0tmSrv5NfgI+ZY0= +github.com/gotd/td v0.117.0 h1:Z6vU5thb5DW/I1s0sLSeSfA/QWvwszx6SxHhEEYJiU8= +github.com/gotd/td v0.117.0/go.mod h1:jf1Zf1ViTN+H1x8dhDTCBHOYY/2E/40HsyOsohxqXYA= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= @@ -112,9 +138,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= @@ -127,6 +152,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ogen-go/ogen v1.8.1 h1:7TZ+oIeLkcBiyl0qu0fHPrFUrGWDj3Fi/zKSWg2i2Tg= @@ -134,11 +161,21 @@ github.com/ogen-go/ogen v1.8.1/go.mod h1:2ShRm6u/nXUHuwdVKv2SeaG8enBKPKAE3kSbHww github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pressly/goose/v3 v3.24.0 h1:sFbNms7Bd++2VMq6HSgDHDLWa7kHz1qXzPb3ZIU72VU= -github.com/pressly/goose/v3 v3.24.0/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug= +github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4LY= +github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -150,8 +187,8 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= @@ -160,8 +197,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= @@ -188,8 +225,12 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -201,27 +242,30 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f9bd861..fba943b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -93,7 +93,7 @@ func GetSessionByHash(db *gorm.DB, cache cache.Cacher, hash string) (*models.Ses type securityHandler struct { db *gorm.DB cache cache.Cacher - cfg *config.Config + cfg *config.JWTConfig } func (s *securityHandler) HandleApiKeyAuth(ctx context.Context, operationName api.OperationName, t api.ApiKeyAuth) (context.Context, error) { @@ -105,14 +105,14 @@ func (s *securityHandler) HandleBearerAuth(ctx context.Context, operationName ap } func (s *securityHandler) handleAuth(ctx context.Context, token string) (context.Context, error) { - claims, err := VerifyUser(s.db, s.cache, s.cfg.JWT.Secret, token) + claims, err := VerifyUser(s.db, s.cache, s.cfg.Secret, token) if err != nil { return nil, &ogenerrors.SecurityError{Err: err} } return context.WithValue(ctx, authKey, claims), nil } -func NewSecurityHandler(db *gorm.DB, cache cache.Cacher, cfg *config.Config) api.SecurityHandler { +func NewSecurityHandler(db *gorm.DB, cache cache.Cacher, cfg *config.JWTConfig) api.SecurityHandler { return &securityHandler{db: db, cache: cache, cfg: cfg} } diff --git a/internal/cache/cache.go b/internal/cache/cache.go index ea06f7c..d5f54a1 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -23,14 +23,14 @@ type MemoryCache struct { mu sync.RWMutex } -func NewCache(ctx context.Context, conf *config.Config) Cacher { +func NewCache(ctx context.Context, conf *config.CacheConfig) Cacher { var cacher Cacher - if conf.Cache.RedisAddr == "" { - cacher = NewMemoryCache(conf.Cache.MaxSize) + if conf.RedisAddr == "" { + cacher = NewMemoryCache(conf.MaxSize) } else { cacher = NewRedisCache(ctx, redis.NewClient(&redis.Options{ - Addr: conf.Cache.RedisAddr, - Password: conf.Cache.RedisPass, + Addr: conf.RedisAddr, + Password: conf.RedisPass, })) } return cacher diff --git a/internal/config/config.go b/internal/config/config.go index 00fb949..b1c1405 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,91 +1,231 @@ package config import ( + "fmt" + "path/filepath" + "reflect" + "strings" "time" + + "github.com/mitchellh/go-homedir" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/tgdrive/teldrive/internal/duration" ) -type Config struct { - Server ServerConfig - Log LoggingConfig - JWT JWTConfig - DB DBConfig - TG TGConfig - CronJobs CronJobConfig - Cache struct { - MaxSize int - RedisAddr string - RedisPass string - } -} - type ServerConfig struct { - Port int - GracefulShutdown time.Duration - EnablePprof bool - ReadTimeout time.Duration - WriteTimeout time.Duration + Port int `mapstructure:"port"` + GracefulShutdown time.Duration `mapstructure:"graceful-shutdown"` + EnablePprof bool `mapstructure:"enable-pprof"` + ReadTimeout time.Duration `mapstructure:"read-timeout"` + WriteTimeout time.Duration `mapstructure:"write-timeout"` } -type CronJobConfig struct { - Enable bool - CleanFilesInterval time.Duration - CleanUploadsInterval time.Duration - FolderSizeInterval time.Duration -} - -type TGConfig struct { - AppId int - AppHash string - RateLimit bool - RateBurst int - Rate int - DeviceModel string - SystemVersion string - AppVersion string - LangCode string - SystemLangCode string - LangPack string - Ntp bool - SessionFile string - DisableStreamBots bool - BgBotsCheckInterval time.Duration - Proxy string - ReconnectTimeout time.Duration - PoolSize int64 - EnableLogging bool - Uploads struct { - EncryptionKey string - Threads int - MaxRetries int - Retention time.Duration - } - Stream struct { - MultiThreads int - Buffers int - ChunkTimeout time.Duration - } +type CacheConfig struct { + MaxSize int `mapstructure:"max-size"` + RedisAddr string `mapstructure:"redis-addr"` + RedisPass string `mapstructure:"redis-pass"` } type LoggingConfig struct { - Level int - Development bool - File string + Level int `mapstructure:"level"` + Development bool `mapstructure:"development"` + File string `mapstructure:"file"` } type JWTConfig struct { - Secret string - SessionTime time.Duration - AllowedUsers []string + Secret string `mapstructure:"secret"` + SessionTime time.Duration `mapstructure:"session-time"` + AllowedUsers []string `mapstructure:"allowed-users"` } type DBConfig struct { - DataSource string - PrepareStmt bool - LogLevel int + DataSource string `mapstructure:"data-source"` + PrepareStmt bool `mapstructure:"prepare-stmt"` + LogLevel int `mapstructure:"log-level"` Pool struct { - Enable bool - MaxOpenConnections int - MaxIdleConnections int - MaxLifetime time.Duration + Enable bool `mapstructure:"enable"` + MaxOpenConnections int `mapstructure:"max-open-connections"` + MaxIdleConnections int `mapstructure:"max-idle-connections"` + MaxLifetime time.Duration `mapstructure:"max-lifetime"` + } `mapstructure:"pool"` +} + +type CronJobConfig struct { + Enable bool `mapstructure:"enable"` + CleanFilesInterval time.Duration `mapstructure:"clean-files-interval"` + CleanUploadsInterval time.Duration `mapstructure:"clean-uploads-interval"` + FolderSizeInterval time.Duration `mapstructure:"folder-size-interval"` +} + +type TGConfig struct { + AppId int `mapstructure:"app-id"` + AppHash string `mapstructure:"app-hash"` + RateLimit bool `mapstructure:"rate-limit"` + RateBurst int `mapstructure:"rate-burst"` + Rate int `mapstructure:"rate"` + UserName string `mapstructure:"user-name"` + DeviceModel string `mapstructure:"device-model"` + SystemVersion string `mapstructure:"system-version"` + AppVersion string `mapstructure:"app-version"` + LangCode string `mapstructure:"lang-code"` + SystemLangCode string `mapstructure:"system-lang-code"` + LangPack string `mapstructure:"lang-pack"` + Ntp bool `mapstructure:"ntp"` + SessionFile string `mapstructure:"session-file"` + DisableStreamBots bool `mapstructure:"disable-stream-bots"` + Proxy string `mapstructure:"proxy"` + ReconnectTimeout time.Duration `mapstructure:"reconnect-timeout"` + PoolSize int64 `mapstructure:"pool-size"` + EnableLogging bool `mapstructure:"enable-logging"` + Uploads struct { + EncryptionKey string `mapstructure:"encryption-key"` + Threads int `mapstructure:"threads"` + MaxRetries int `mapstructure:"max-retries"` + Retention time.Duration `mapstructure:"retention"` + } `mapstructure:"uploads"` + Stream struct { + MultiThreads int `mapstructure:"multi-threads"` + Buffers int `mapstructure:"buffers"` + ChunkTimeout time.Duration `mapstructure:"chunk-timeout"` + } `mapstructure:"stream"` +} + +type ServerCmdConfig struct { + Server ServerConfig `mapstructure:"server"` + Log LoggingConfig `mapstructure:"log"` + JWT JWTConfig `mapstructure:"jwt"` + DB DBConfig `mapstructure:"db"` + TG TGConfig `mapstructure:"tg"` + CronJobs CronJobConfig `mapstructure:"cronjobs"` + Cache CacheConfig `mapstructure:"cache"` +} + +type MigrateCmdConfig struct { + DB DBConfig `mapstructure:"db"` + Log LoggingConfig `mapstructure:"log"` +} + +type ConfigLoader struct { + v *viper.Viper +} + +func NewConfigLoader() *ConfigLoader { + return &ConfigLoader{ + v: viper.New(), } } + +func StringToDurationHook() mapstructure.DecodeHookFunc { + return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + + if t != reflect.TypeOf(time.Duration(0)) { + return data, nil + } + + str, ok := data.(string) + if !ok { + return data, nil + } + return duration.ParseDuration(str) + } +} + +func (cl *ConfigLoader) InitializeConfig(cmd *cobra.Command) error { + cl.v.SetConfigType("toml") + + cfgFile := cmd.Flags().Lookup("config").Value.String() + + if cfgFile != "" { + cl.v.SetConfigFile(cfgFile) + } else { + home, err := homedir.Dir() + if err != nil { + return fmt.Errorf("error getting home directory: %v", err) + } + cl.v.AddConfigPath(filepath.Join(home, ".teldrive")) + cl.v.AddConfigPath(".") + cl.v.SetConfigName("config") + } + + cl.v.SetEnvPrefix("teldrive") + cl.v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + cl.v.AutomaticEnv() + + if err := cl.v.BindPFlags(cmd.Flags()); err != nil { + return fmt.Errorf("error binding flags: %v", err) + } + + if err := cl.v.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return fmt.Errorf("error reading config file: %v", err) + } + } + + return nil +} + +func (cl *ConfigLoader) Load(cfg interface{}) error { + config := &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + StringToDurationHook(), + ), + WeaklyTypedInput: true, + Result: cfg, + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return fmt.Errorf("failed to create decoder: %v", err) + } + + if err := decoder.Decode(cl.v.AllSettings()); err != nil { + return fmt.Errorf("failed to decode config: %v", err) + } + + return nil +} + +func AddCommonFlags(flags *pflag.FlagSet, config *ServerCmdConfig) { + + flags.StringP("config", "c", "", "Config file path (default $HOME/.teldrive/config.toml)") + + // Log config + flags.IntVarP(&config.Log.Level, "log-level", "", -1, "Logging level") + flags.StringVar(&config.Log.File, "log-file", "", "Logging file path") + flags.BoolVar(&config.Log.Development, "log-development", false, "Enable development mode") + + // DB config + flags.StringVar(&config.DB.DataSource, "db-data-source", "", "Database connection string") + flags.IntVar(&config.DB.LogLevel, "db-log-level", 1, "Database log level") + flags.BoolVar(&config.DB.PrepareStmt, "db-prepare-stmt", true, "Enable prepared statements") + flags.BoolVar(&config.DB.Pool.Enable, "db-pool-enable", true, "Enable database pool") + flags.IntVar(&config.DB.Pool.MaxIdleConnections, "db-pool-max-open-connections", 25, "Database max open connections") + flags.IntVar(&config.DB.Pool.MaxIdleConnections, "db-pool-max-idle-connections", 25, "Database max idle connections") + duration.DurationVar(flags, &config.DB.Pool.MaxLifetime, "db-pool-max-lifetime", 10*time.Minute, "Database max connection lifetime") + + // Telegram config + flags.IntVar(&config.TG.AppId, "tg-app-id", 0, "Telegram app ID") + flags.StringVar(&config.TG.AppHash, "tg-app-hash", "", "Telegram app hash") + flags.StringVar(&config.TG.SessionFile, "tg-session-file", "", "Bot session file path") + flags.BoolVar(&config.TG.RateLimit, "tg-rate-limit", true, "Enable rate limiting for telegram client") + flags.IntVar(&config.TG.RateBurst, "tg-rate-burst", 5, "Limiting burst for telegram client") + flags.IntVar(&config.TG.Rate, "tg-rate", 100, "Limiting rate for telegram client") + flags.StringVar(&config.TG.DeviceModel, "tg-device-model", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0", "Device model") + flags.StringVar(&config.TG.SystemVersion, "tg-system-version", "Win32", "System version") + flags.StringVar(&config.TG.AppVersion, "tg-app-version", "4.6.3 K", "App version") + flags.StringVar(&config.TG.LangCode, "tg-lang-code", "en", "Language code") + flags.StringVar(&config.TG.SystemLangCode, "tg-system-lang-code", "en-US", "System language code") + flags.StringVar(&config.TG.LangPack, "tg-lang-pack", "webk", "Language pack") + flags.StringVar(&config.TG.Proxy, "tg-proxy", "", "HTTP OR SOCKS5 proxy URL") + flags.BoolVar(&config.TG.DisableStreamBots, "tg-disable-stream-bots", false, "Disable Stream bots") + flags.BoolVar(&config.TG.Ntp, "tg-ntp", false, "Use NTP server time") + flags.BoolVar(&config.TG.EnableLogging, "tg-enable-logging", false, "Enable telegram client logging") + flags.Int64Var(&config.TG.PoolSize, "tg-pool-size", 8, "Telegram Session pool size") +} diff --git a/internal/database/database.go b/internal/database/database.go index a63ea62..090363f 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -13,16 +13,16 @@ import ( "gorm.io/gorm/schema" ) -func NewDatabase(cfg *config.Config, lg *zap.SugaredLogger) (*gorm.DB, error) { +func NewDatabase(cfg *config.DBConfig, lg *zap.SugaredLogger) (*gorm.DB, error) { var ( db *gorm.DB err error - logger = NewLogger(lg, time.Second, true, zapcore.Level(cfg.DB.LogLevel)) + logger = NewLogger(lg, time.Second, true, zapcore.Level(cfg.LogLevel)) ) for i := 0; i <= 5; i++ { db, err = gorm.Open(postgres.New(postgres.Config{ - DSN: cfg.DB.DataSource, - PreferSimpleProtocol: !cfg.DB.PrepareStmt, + DSN: cfg.DataSource, + PreferSimpleProtocol: !cfg.PrepareStmt, }), &gorm.Config{ Logger: logger, NamingStrategy: schema.NamingStrategy{ @@ -46,20 +46,14 @@ func NewDatabase(cfg *config.Config, lg *zap.SugaredLogger) (*gorm.DB, error) { db.Use(extraClausePlugin.New()) - if cfg.DB.Pool.Enable { + if cfg.Pool.Enable { rawDB, err := db.DB() if err != nil { return nil, err } - rawDB.SetMaxOpenConns(cfg.DB.Pool.MaxOpenConnections) - rawDB.SetMaxIdleConns(cfg.DB.Pool.MaxIdleConnections) - rawDB.SetConnMaxLifetime(cfg.DB.Pool.MaxLifetime) - } - - sqlDb, _ := db.DB() - err = migrateDB(sqlDb) - if err != nil { - lg.Fatalf("database: %v", err) + rawDB.SetMaxOpenConns(cfg.Pool.MaxOpenConnections) + rawDB.SetMaxIdleConns(cfg.Pool.MaxIdleConnections) + rawDB.SetConnMaxLifetime(cfg.Pool.MaxLifetime) } return db, nil diff --git a/internal/database/database_util.go b/internal/database/database_util.go index ef6160d..aea2271 100644 --- a/internal/database/database_util.go +++ b/internal/database/database_util.go @@ -1,7 +1,6 @@ package database import ( - "database/sql" "embed" "errors" "fmt" @@ -44,7 +43,7 @@ func NewTestDatabase(tb testing.TB, migration bool) *gorm.DB { sqlDB.SetConnMaxIdleTime(10 * time.Minute) if migration { - migrateDB(sqlDB) + MigrateDB(db) } return db @@ -69,14 +68,15 @@ func DeleteRecordAll(_ testing.TB, db *gorm.DB, tableWhereClauses []string) erro return nil } -func migrateDB(db *sql.DB) error { +func MigrateDB(db *gorm.DB) error { + sqlDb, _ := db.DB() goose.SetBaseFS(embedMigrations) if err := goose.SetDialect("postgres"); err != nil { - return fmt.Errorf("failed run migrate: %w", err) + return err } - if err := goose.Up(db, "migrations"); err != nil { - return fmt.Errorf("failed run migrate: %w", err) + if err := goose.Up(sqlDb, "migrations"); err != nil { + return err } return nil } diff --git a/internal/duration/duration.go b/internal/duration/duration.go index bd1fbcb..5c40a78 100644 --- a/internal/duration/duration.go +++ b/internal/duration/duration.go @@ -27,7 +27,7 @@ func (d *Duration) String() string { } func (d *Duration) Set(s string) error { - v, err := parseDuration(s) + v, err := ParseDuration(s) *d = Duration(v) return err } @@ -89,10 +89,17 @@ func DurationVar(f *pflag.FlagSet, p *time.Duration, name string, value time.Dur f.VarP(newDurationValue(value, p), name, "", usage) } -func parseDuration(age string) (time.Duration, error) { +func ParseDuration(age string) (time.Duration, error) { return parseDurationFromNow(age) } +func (d *Duration) UnmarshalText(text []byte) error { + if err := d.Set(string(text)); err != nil { + return err + } + return nil +} + func (d *Duration) Type() string { return "Duration" } diff --git a/internal/duration/duration_test.go b/internal/duration/duration_test.go index b418b17..0341939 100644 --- a/internal/duration/duration_test.go +++ b/internal/duration/duration_test.go @@ -3,6 +3,6 @@ package duration import "testing" func TestDate(t *testing.T) { - res, _ := parseDuration("15h2m10s") + res, _ := ParseDuration("15h2m10s") _ = res } diff --git a/internal/kv/bolt.go b/internal/kv/bolt.go deleted file mode 100644 index e0ceab7..0000000 --- a/internal/kv/bolt.go +++ /dev/null @@ -1,76 +0,0 @@ -package kv - -import ( - "os" - "path/filepath" - "time" - - "github.com/mitchellh/go-homedir" - "github.com/tgdrive/teldrive/internal/config" - "github.com/tgdrive/teldrive/internal/utils" - "go.etcd.io/bbolt" -) - -type Bolt struct { - bucket []byte - db *bbolt.DB -} - -func (b *Bolt) Get(key string) ([]byte, error) { - var val []byte - - if err := b.db.View(func(tx *bbolt.Tx) error { - val = tx.Bucket(b.bucket).Get([]byte(key)) - return nil - }); err != nil { - return nil, err - } - - if val == nil { - return nil, ErrNotFound - } - return val, nil -} - -func (b *Bolt) Set(key string, val []byte) error { - return b.db.Update(func(tx *bbolt.Tx) error { - return tx.Bucket(b.bucket).Put([]byte(key), val) - }) -} - -func (b *Bolt) Delete(key string) error { - return b.db.Update(func(tx *bbolt.Tx) error { - return tx.Bucket(b.bucket).Delete([]byte(key)) - }) -} - -func NewBoltKV(cnf *config.Config) KV { - - sessionFile := cnf.TG.SessionFile - if sessionFile == "" { - dir, err := homedir.Dir() - if err != nil { - dir = utils.ExecutableDir() - } else { - dir = filepath.Join(dir, ".teldrive") - err := os.Mkdir(dir, 0755) - if err != nil && !os.IsExist(err) { - dir = utils.ExecutableDir() - } - } - sessionFile = filepath.Join(dir, "session.db") - } - boltDB, err := bbolt.Open(sessionFile, 0666, &bbolt.Options{ - Timeout: time.Second, - NoGrowSync: false, - }) - if err != nil { - panic(err) - } - kv, err := New(Options{Bucket: "teldrive", DB: boltDB}) - if err != nil { - panic(err) - } - - return kv -} diff --git a/internal/kv/key.go b/internal/kv/key.go deleted file mode 100644 index 4b5393b..0000000 --- a/internal/kv/key.go +++ /dev/null @@ -1,9 +0,0 @@ -package kv - -import ( - "strings" -) - -func Key(indexes ...string) string { - return strings.Join(indexes, ":") -} diff --git a/internal/kv/kv.go b/internal/kv/kv.go deleted file mode 100644 index a542973..0000000 --- a/internal/kv/kv.go +++ /dev/null @@ -1,32 +0,0 @@ -package kv - -import ( - "errors" - - "go.etcd.io/bbolt" -) - -var ErrNotFound = errors.New("key not found") - -type KV interface { - Get(key string) ([]byte, error) - Set(key string, value []byte) error - Delete(key string) error -} - -type Options struct { - Bucket string - DB *bbolt.DB -} - -func New(opts Options) (KV, error) { - - if err := opts.DB.Update(func(tx *bbolt.Tx) error { - _, err := tx.CreateBucketIfNotExists([]byte(opts.Bucket)) - return err - }); err != nil { - return nil, err - } - - return &Bolt{db: opts.DB, bucket: []byte(opts.Bucket)}, nil -} diff --git a/internal/kv/session.go b/internal/kv/session.go deleted file mode 100644 index 3e46620..0000000 --- a/internal/kv/session.go +++ /dev/null @@ -1,41 +0,0 @@ -package kv - -import ( - "context" - "errors" - "sync" - - "github.com/gotd/td/telegram" -) - -type Session struct { - kv KV - key string - mu sync.Mutex -} - -func NewSession(kv KV, key string) telegram.SessionStorage { - return &Session{kv: kv, key: key} -} - -func (s *Session) LoadSession(_ context.Context) ([]byte, error) { - s.mu.Lock() - defer s.mu.Unlock() - - b, err := s.kv.Get(s.key) - if err != nil { - if errors.Is(err, ErrNotFound) { - return nil, nil - } - return nil, err - } - data := make([]byte, len(b)) - copy(data, b) - return data, nil -} - -func (s *Session) StoreSession(_ context.Context, data []byte) error { - s.mu.Lock() - defer s.mu.Unlock() - return s.kv.Set(s.key, data) -} diff --git a/internal/pool/pool.go b/internal/pool/pool.go index 67e2aa8..10724b6 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -87,10 +87,11 @@ func (p *pool) Default(ctx context.Context) *tg.Client { return p.Client(ctx, p.current()) } -func (p *pool) Close() (err error) { +func (p *pool) Close() error { if p.close != nil { return p.close() } + return nil } diff --git a/internal/reader/tg_multi_reader_test.go b/internal/reader/tg_multi_reader_test.go deleted file mode 100644 index fc1e6b3..0000000 --- a/internal/reader/tg_multi_reader_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package reader - -import ( - "context" - "crypto/rand" - "io" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/tgdrive/teldrive/internal/config" -) - -type testChunkSource struct { - buffer []byte -} - -func (m *testChunkSource) Chunk(ctx context.Context, offset int64, limit int64) ([]byte, error) { - return m.buffer[offset : offset+limit], nil -} - -func (m *testChunkSource) ChunkSize(start, end int64) int64 { - return 1 -} - -type testChunkSourceTimeout struct { - buffer []byte -} - -func (m *testChunkSourceTimeout) Chunk(ctx context.Context, offset int64, limit int64) ([]byte, error) { - if offset == 8 { - time.Sleep(2 * time.Second) - } - return m.buffer[offset : offset+limit], nil -} - -func (m *testChunkSourceTimeout) ChunkSize(start, end int64) int64 { - return 1 -} - -type TestSuite struct { - suite.Suite - config *config.TGConfig -} - -func (suite *TestSuite) SetupTest() { - suite.config = &config.TGConfig{Stream: struct { - MultiThreads int - Buffers int - ChunkTimeout time.Duration - }{MultiThreads: 8, Buffers: 10, ChunkTimeout: 1 * time.Second}} -} - -func (suite *TestSuite) TestFullRead() { - ctx := context.Background() - start := int64(0) - end := int64(99) - data := make([]byte, 100) - rand.Read(data) - chunkSrc := &testChunkSource{buffer: data} - reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc) - assert.NoError(suite.T(), err) - test_data, err := io.ReadAll(reader) - assert.Equal(suite.T(), nil, err) - assert.Equal(suite.T(), data[start:end+1], test_data) -} - -func (suite *TestSuite) TestPartialRead() { - ctx := context.Background() - start := int64(0) - end := int64(65) - data := make([]byte, 100) - rand.Read(data) - chunkSrc := &testChunkSource{buffer: data} - reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc) - assert.NoError(suite.T(), err) - test_data, err := io.ReadAll(reader) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), data[start:end+1], test_data) -} - -func (suite *TestSuite) TestTimeout() { - ctx := context.Background() - start := int64(0) - end := int64(65) - data := make([]byte, 100) - rand.Read(data) - chunkSrc := &testChunkSourceTimeout{buffer: data} - reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc) - assert.NoError(suite.T(), err) - test_data, err := io.ReadAll(reader) - assert.Greater(suite.T(), len(test_data), 0) - assert.Equal(suite.T(), err, ErrStreamAbandoned) -} - -func (suite *TestSuite) TestClose() { - ctx := context.Background() - start := int64(0) - end := int64(65) - data := make([]byte, 100) - rand.Read(data) - chunkSrc := &testChunkSource{buffer: data} - reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc) - assert.NoError(suite.T(), err) - _, err = io.ReadAll(reader) - assert.NoError(suite.T(), err) - assert.NoError(suite.T(), reader.Close()) -} - -func (suite *TestSuite) TestCancellation() { - ctx, cancel := context.WithCancel(context.Background()) - start := int64(0) - end := int64(65) - data := make([]byte, 100) - rand.Read(data) - chunkSrc := &testChunkSource{buffer: data} - reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc) - assert.NoError(suite.T(), err) - cancel() - _, err = io.ReadAll(reader) - assert.Equal(suite.T(), err, context.Canceled) - assert.Equal(suite.T(), len(reader.bufferChan), 0) -} - -func (suite *TestSuite) TestCancellationWithTimeout() { - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - _ = cancel - start := int64(0) - end := int64(65) - data := make([]byte, 100) - rand.Read(data) - chunkSrc := &testChunkSourceTimeout{buffer: data} - reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc) - assert.NoError(suite.T(), err) - _, err = io.ReadAll(reader) - assert.Equal(suite.T(), err, context.DeadlineExceeded) - assert.Equal(suite.T(), len(reader.bufferChan), 0) -} -func Test(t *testing.T) { - suite.Run(t, new(TestSuite)) -} diff --git a/internal/tgc/bolt.go b/internal/tgc/bolt.go new file mode 100644 index 0000000..7e2ba07 --- /dev/null +++ b/internal/tgc/bolt.go @@ -0,0 +1,36 @@ +package tgc + +import ( + "os" + "path/filepath" + "time" + + "github.com/mitchellh/go-homedir" + "github.com/tgdrive/teldrive/internal/utils" + "go.etcd.io/bbolt" +) + +func NewBoltDB(sessionFile string) (*bbolt.DB, error) { + if sessionFile == "" { + dir, err := homedir.Dir() + if err != nil { + dir = utils.ExecutableDir() + } else { + dir = filepath.Join(dir, ".teldrive") + err := os.Mkdir(dir, 0755) + if err != nil && !os.IsExist(err) { + dir = utils.ExecutableDir() + } + } + sessionFile = filepath.Join(dir, "session.db") + } + db, err := bbolt.Open(sessionFile, 0666, &bbolt.Options{ + Timeout: time.Second, + NoGrowSync: false, + }) + if err != nil { + return nil, err + } + return db, nil + +} diff --git a/internal/tgc/connect.go b/internal/tgc/connect.go deleted file mode 100644 index cf03f80..0000000 --- a/internal/tgc/connect.go +++ /dev/null @@ -1,78 +0,0 @@ -package tgc - -import ( - "context" - "errors" - - "github.com/gotd/td/telegram" -) - -type StopFunc func() error - -type connectOptions struct { - ctx context.Context - token string -} - -type Option interface { - apply(o *connectOptions) -} - -type fnOption func(o *connectOptions) - -func (f fnOption) apply(o *connectOptions) { - f(o) -} - -func WithContext(ctx context.Context) Option { - return fnOption(func(o *connectOptions) { - o.ctx = ctx - }) -} - -func WithBotToken(token string) Option { - return fnOption(func(o *connectOptions) { - o.token = token - }) -} - -func Connect(client *telegram.Client, options ...Option) (StopFunc, error) { - opt := &connectOptions{ - ctx: context.Background(), - } - for _, o := range options { - o.apply(opt) - } - - ctx, cancel := context.WithCancel(opt.ctx) - - errC := make(chan error, 1) - initDone := make(chan struct{}) - go func() { - defer close(errC) - errC <- RunWithAuth(ctx, client, opt.token, func(ctx context.Context) error { - close(initDone) - <-ctx.Done() - if errors.Is(ctx.Err(), context.Canceled) { - return nil - } - return ctx.Err() - }) - }() - - select { - case <-ctx.Done(): // context canceled - cancel() - return func() error { return nil }, ctx.Err() - case err := <-errC: // startup timeout - cancel() - return func() error { return nil }, err - case <-initDone: // init done - } - - stopFn := func() error { - cancel() - return <-errC - } - return stopFn, nil -} diff --git a/internal/tgc/helpers.go b/internal/tgc/helpers.go index 32d0c71..3acc3f6 100644 --- a/internal/tgc/helpers.go +++ b/internal/tgc/helpers.go @@ -12,8 +12,8 @@ import ( "github.com/gotd/td/telegram" "github.com/gotd/td/tg" "github.com/tgdrive/teldrive/internal/config" - "github.com/tgdrive/teldrive/internal/kv" "github.com/tgdrive/teldrive/pkg/types" + "go.etcd.io/bbolt" "golang.org/x/sync/errgroup" ) @@ -100,7 +100,7 @@ func GetMessages(ctx context.Context, client *tg.Client, ids []int, channelId in return nil, err } - batchSize := 200 + batchSize := 100 batchCount := int(math.Ceil(float64(len(ids)) / float64(batchSize))) @@ -184,10 +184,10 @@ func GetMediaContent(ctx context.Context, client *tg.Client, location tg.InputFi return buff, nil } -func GetBotInfo(ctx context.Context, KV kv.KV, config *config.TGConfig, token string) (*types.BotInfo, error) { +func GetBotInfo(ctx context.Context, boltdb *bbolt.DB, config *config.TGConfig, token string) (*types.BotInfo, error) { var user *tg.User middlewares := NewMiddleware(config, WithFloodWait(), WithRateLimit()) - client, _ := BotClient(ctx, KV, config, token, middlewares...) + client, _ := BotClient(ctx, boltdb, config, token, middlewares...) err := RunWithAuth(ctx, client, token, func(ctx context.Context) error { user, _ = client.Self(ctx) return nil diff --git a/internal/tgc/tgc.go b/internal/tgc/tgc.go index 7f17981..61f48c1 100644 --- a/internal/tgc/tgc.go +++ b/internal/tgc/tgc.go @@ -2,10 +2,12 @@ package tgc import ( "context" + "strings" "time" "github.com/cenkalti/backoff/v4" "github.com/go-faster/errors" + tgbbolt "github.com/gotd/contrib/bbolt" "github.com/gotd/contrib/clock" "github.com/gotd/contrib/middleware/floodwait" "github.com/gotd/contrib/middleware/ratelimit" @@ -13,17 +15,21 @@ import ( "github.com/gotd/td/telegram" "github.com/gotd/td/telegram/dcs" "github.com/tgdrive/teldrive/internal/config" - "github.com/tgdrive/teldrive/internal/kv" "github.com/tgdrive/teldrive/internal/logging" "github.com/tgdrive/teldrive/internal/recovery" "github.com/tgdrive/teldrive/internal/retry" "github.com/tgdrive/teldrive/internal/utils" + "go.etcd.io/bbolt" "go.uber.org/zap" "golang.org/x/net/proxy" "golang.org/x/time/rate" ) -func New(ctx context.Context, config *config.TGConfig, handler telegram.UpdateHandler, storage session.Storage, middlewares ...telegram.Middleware) (*telegram.Client, error) { +func sessionKey(indexes ...string) string { + return strings.Join(indexes, ":") +} + +func newClient(ctx context.Context, config *config.TGConfig, handler telegram.UpdateHandler, storage session.Storage, middlewares ...telegram.Middleware) (*telegram.Client, error) { var dialer dcs.DialFunc = proxy.Direct.DialContext if config.Proxy != "" { @@ -80,7 +86,7 @@ func NoAuthClient(ctx context.Context, config *config.TGConfig, handler telegram floodwait.NewSimpleWaiter(), } middlewares = append(middlewares, ratelimit.New(rate.Every(time.Millisecond*100), 5)) - return New(ctx, config, handler, storage, middlewares...) + return newClient(ctx, config, handler, storage, middlewares...) } func AuthClient(ctx context.Context, config *config.TGConfig, sessionStr string, middlewares ...telegram.Middleware) (*telegram.Client, error) { @@ -98,14 +104,14 @@ func AuthClient(ctx context.Context, config *config.TGConfig, sessionStr string, if err := loader.Save(context.TODO(), data); err != nil { return nil, err } - return New(ctx, config, nil, storage, middlewares...) + return newClient(ctx, config, nil, storage, middlewares...) } -func BotClient(ctx context.Context, KV kv.KV, config *config.TGConfig, token string, middlewares ...telegram.Middleware) (*telegram.Client, error) { +func BotClient(ctx context.Context, boltdb *bbolt.DB, config *config.TGConfig, token string, middlewares ...telegram.Middleware) (*telegram.Client, error) { - storage := kv.NewSession(KV, kv.Key("botsession", token)) + storage := tgbbolt.NewSessionStorage(boltdb, sessionKey("botsession", token), []byte("teldrive")) - return New(ctx, config, nil, storage, middlewares...) + return newClient(ctx, config, nil, storage, middlewares...) } diff --git a/main.go b/main.go index e96f61f..a1394dd 100644 --- a/main.go +++ b/main.go @@ -4,12 +4,13 @@ import ( "context" "os" "os/signal" + "syscall" "github.com/tgdrive/teldrive/cmd" ) func main() { - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() if err := cmd.New().ExecuteContext(ctx); err != nil { diff --git a/pkg/cron/cron.go b/pkg/cron/cron.go index 51eb17d..8fd47af 100644 --- a/pkg/cron/cron.go +++ b/pkg/cron/cron.go @@ -37,11 +37,11 @@ type UploadResult struct { type CronService struct { db *gorm.DB - cnf *config.Config + cnf *config.ServerCmdConfig logger *zap.SugaredLogger } -func StartCronJobs(scheduler *gocron.Scheduler, db *gorm.DB, cnf *config.Config) { +func StartCronJobs(scheduler *gocron.Scheduler, db *gorm.DB, cnf *config.ServerCmdConfig) { if !cnf.CronJobs.Enable { return } diff --git a/pkg/services/api_service.go b/pkg/services/api_service.go index 23f9c19..cef12f1 100644 --- a/pkg/services/api_service.go +++ b/pkg/services/api_service.go @@ -7,12 +7,12 @@ import ( "github.com/go-faster/errors" "github.com/gotd/td/telegram" "github.com/ogen-go/ogen/ogenerrors" + "go.etcd.io/bbolt" ht "github.com/ogen-go/ogen/http" "github.com/tgdrive/teldrive/internal/api" "github.com/tgdrive/teldrive/internal/cache" "github.com/tgdrive/teldrive/internal/config" - "github.com/tgdrive/teldrive/internal/kv" "github.com/tgdrive/teldrive/internal/tgc" "github.com/tgdrive/teldrive/internal/version" "gorm.io/gorm" @@ -20,9 +20,9 @@ import ( type apiService struct { db *gorm.DB - cnf *config.Config + cnf *config.ServerCmdConfig cache cache.Cacher - kv kv.KV + boltdb *bbolt.DB worker *tgc.BotWorker middlewares []telegram.Middleware } @@ -53,11 +53,11 @@ func (a *apiService) NewError(ctx context.Context, err error) *api.ErrorStatusCo } func NewApiService(db *gorm.DB, - cnf *config.Config, + cnf *config.ServerCmdConfig, cache cache.Cacher, - kv kv.KV, + boltdb *bbolt.DB, worker *tgc.BotWorker) *apiService { - return &apiService{db: db, cnf: cnf, cache: cache, kv: kv, worker: worker, + return &apiService{db: db, cnf: cnf, cache: cache, boltdb: boltdb, worker: worker, middlewares: tgc.NewMiddleware(&cnf.TG, tgc.WithFloodWait(), tgc.WithRateLimit())} } diff --git a/pkg/services/file.go b/pkg/services/file.go index 437de2e..c6c60b8 100644 --- a/pkg/services/file.go +++ b/pkg/services/file.go @@ -765,7 +765,7 @@ func (e *extendedService) FilesStream(w http.ResponseWriter, r *http.Request, fi token, _ = e.api.worker.Next(file.ChannelId.Value) - client, err = tgc.BotClient(ctx, e.api.kv, &e.api.cnf.TG, token, middlewares...) + client, err = tgc.BotClient(ctx, e.api.boltdb, &e.api.cnf.TG, token, middlewares...) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/pkg/services/upload.go b/pkg/services/upload.go index 64e23b5..de0922f 100644 --- a/pkg/services/upload.go +++ b/pkg/services/upload.go @@ -120,7 +120,7 @@ func (a *apiService) UploadsUpload(ctx context.Context, req *api.UploadsUploadRe } else { a.worker.Set(tokens, channelId) token, index = a.worker.Next(channelId) - client, err = tgc.BotClient(ctx, a.kv, &a.cnf.TG, token) + client, err = tgc.BotClient(ctx, a.boltdb, &a.cnf.TG, token) if err != nil { return nil, err diff --git a/pkg/services/user.go b/pkg/services/user.go index cab4edb..8d76674 100644 --- a/pkg/services/user.go +++ b/pkg/services/user.go @@ -21,6 +21,8 @@ import ( "github.com/tgdrive/teldrive/pkg/types" "golang.org/x/sync/errgroup" + tgbbolt "github.com/gotd/contrib/bbolt" + "github.com/gotd/contrib/storage" "gorm.io/gorm/clause" ) @@ -44,36 +46,28 @@ func (a *apiService) UsersAddBots(ctx context.Context, req *api.AddBots) error { } func (a *apiService) UsersListChannels(ctx context.Context) ([]api.Channel, error) { - _, session := auth.GetUser(ctx) - client, err := tgc.AuthClient(ctx, &a.cnf.TG, session, a.middlewares...) - if err != nil { - return nil, &apiError{err: err} - } - if client == nil { - return nil, &apiError{err: errors.New("failed to initialise tg client")} - } + + userID, _ := auth.GetUser(ctx) channels := make(map[int64]*api.Channel) - client.Run(ctx, func(ctx context.Context) error { + peerStorage := tgbbolt.NewPeerStorage(a.boltdb, []byte(fmt.Sprintf("peers:%d", userID))) - dialogs, _ := query.GetDialogs(client.API()).BatchSize(100).Collect(ctx) - - for _, dialog := range dialogs { - if !dialog.Deleted() { - for _, channel := range dialog.Entities.Channels() { - _, exists := channels[channel.ID] - if !exists && channel.AdminRights.AddAdmins { - channels[channel.ID] = &api.Channel{ChannelId: channel.ID, ChannelName: channel.Title} - } - } + iter, err := peerStorage.Iterate(ctx) + if err != nil { + return []api.Channel{}, nil + } + for iter.Next(ctx) { + peer := iter.Value() + if peer.Channel != nil && peer.Channel.AdminRights.AddAdmins { + _, exists := channels[peer.Channel.ID] + if !exists { + channels[peer.Channel.ID] = &api.Channel{ChannelId: peer.Channel.ID, ChannelName: peer.Channel.Title} } } - return nil - }) + } res := []api.Channel{} - for _, channel := range channels { res = append(res, *channel) @@ -84,6 +78,23 @@ func (a *apiService) UsersListChannels(ctx context.Context) ([]api.Channel, erro return res, nil } +func (a *apiService) UsersSyncChannels(ctx context.Context) error { + userId, session := auth.GetUser(ctx) + peerStorage := tgbbolt.NewPeerStorage(a.boltdb, []byte(fmt.Sprintf("peers:%d", userId))) + collector := storage.CollectPeers(peerStorage) + client, err := tgc.AuthClient(ctx, &a.cnf.TG, session, a.middlewares...) + if err != nil { + return &apiError{err: err} + } + err = client.Run(ctx, func(ctx context.Context) error { + return collector.Dialogs(ctx, query.GetDialogs(client.API()).Iter()) + }) + if err != nil { + return &apiError{err: err} + } + return nil +} + func (a *apiService) UsersListSessions(ctx context.Context) ([]api.UserSession, error) { userId, userSession := auth.GetUser(ctx) @@ -287,7 +298,7 @@ func (a *apiService) addBots(c context.Context, client *telegram.Client, userId for _, token := range botsTokens { g.Go(func() error { - info, err := tgc.GetBotInfo(c, a.kv, &a.cnf.TG, token) + info, err := tgc.GetBotInfo(c, a.boltdb, &a.cnf.TG, token) if err != nil { return err }