From 96715225964fdbe8a9366a9e8a4bd53f41d28fa8 Mon Sep 17 00:00:00 2001 From: XMLHexagram Date: Sat, 8 Nov 2025 02:01:59 +0100 Subject: [PATCH 1/8] feat(cmd/doctor): init --- app/doctor/connectivity.go | 24 +++++++++ app/doctor/database.go | 108 +++++++++++++++++++++++++++++++++++++ app/doctor/doctor.go | 95 ++++++++++++++++++++++++++++++++ app/doctor/login.go | 47 ++++++++++++++++ app/doctor/ntp.go | 47 ++++++++++++++++ cmd/doctor.go | 44 +++++++++++++++ cmd/root.go | 2 +- core/storage/session.go | 6 +-- go.work.sum | 96 +++++++++++++++++++++++++++++++++ 9 files changed, 465 insertions(+), 4 deletions(-) create mode 100644 app/doctor/connectivity.go create mode 100644 app/doctor/database.go create mode 100644 app/doctor/doctor.go create mode 100644 app/doctor/login.go create mode 100644 app/doctor/ntp.go create mode 100644 cmd/doctor.go diff --git a/app/doctor/connectivity.go b/app/doctor/connectivity.go new file mode 100644 index 000000000..42748f782 --- /dev/null +++ b/app/doctor/connectivity.go @@ -0,0 +1,24 @@ +package doctor + +import ( + "context" + + "github.com/fatih/color" +) + +func checkConnectivity(ctx context.Context, opts Options) { + client := opts.Client + if client == nil { + color.Yellow(" [WARN] Client not provided, skipping connectivity check") + return + } + + // Simple ping to check connectivity + _, err := client.API().HelpGetConfig(ctx) + if err != nil { + color.Red(" [FAIL] Failed to connect to Telegram: %v", err) + return + } + + color.Green(" [OK] Successfully connected to Telegram server") +} diff --git a/app/doctor/database.go b/app/doctor/database.go new file mode 100644 index 000000000..5702576db --- /dev/null +++ b/app/doctor/database.go @@ -0,0 +1,108 @@ +package doctor + +import ( + "context" + "os" + + "github.com/fatih/color" + "github.com/spf13/viper" + + "github.com/iyear/tdl/core/storage" + "github.com/iyear/tdl/pkg/consts" + "github.com/iyear/tdl/pkg/key" +) + +func checkDatabaseIntegrity(ctx context.Context, opts Options) { + storage := opts.KV + if storage == nil { + color.Red(" [FAIL] Storage not initialized") + return + } + + // Check storage type + storageType := storage.Name() + color.White(" Storage type: %s", storageType) + + // Get storage configuration + storageConfig := viper.GetStringMapString(consts.FlagStorage) + + // Check if storage path exists and is accessible + path := storageConfig["path"] + if path == "" { + color.Yellow(" [WARN] Storage path not configured") + } else { + color.White(" Storage path: %s", path) + + // Check if path exists + if info, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + color.Yellow(" [WARN] Storage path does not exist yet (will be created on first use)") + } else { + color.Red(" [FAIL] Cannot access storage path: %v", err) + return + } + } else { + // Show basic info about the storage + if info.IsDir() { + color.White(" Storage is a directory") + } else { + color.White(" Storage is a file (size: %d bytes)", info.Size()) + } + } + } + + // Check namespaces using already-opened storage + // This is safe because we're using the storage instance that was opened by root command + namespaces, err := storage.Namespaces() + if err != nil { + color.Red(" [FAIL] Failed to list namespaces: %v", err) + return + } + + if len(namespaces) == 0 { + color.Yellow(" [WARN] No namespaces found in storage") + } else { + color.White(" Found %d namespace(s): %v", len(namespaces), namespaces) + } + + // Check current namespace has required keys + currentNS := viper.GetString(consts.FlagNamespace) + if currentNS != "" { + color.White(" Current namespace: %s", currentNS) + + nsStorage, err := storage.Open(currentNS) + if err != nil { + color.Yellow(" [WARN] Failed to open namespace '%s': %v", currentNS, err) + } else { + checkNamespaceKeys(ctx, nsStorage) + } + } + + color.Green(" [OK] Database integrity check passed") +} + +func checkNamespaceKeys(ctx context.Context, storage_ interface{}) { + type getter interface { + Get(ctx context.Context, key string) ([]byte, error) + } + + st, ok := storage_.(getter) + if !ok { + return + } + + // Check for session key (generated by keygen.New("session")) + a := storage.Session{} + if _, err := st.Get(ctx, a.Key()); err == nil { + color.White(" - Session data: present") + } else { + color.Yellow(" - Session data: missing (not logged in)") + } + + // Check for app key + if data, err := st.Get(ctx, key.App()); err == nil { + color.White(" - App config: present (%s)", string(data)) + } else { + color.Yellow(" - App config: missing") + } +} diff --git a/app/doctor/doctor.go b/app/doctor/doctor.go new file mode 100644 index 000000000..9e225a0de --- /dev/null +++ b/app/doctor/doctor.go @@ -0,0 +1,95 @@ +package doctor + +import ( + "context" + + "github.com/fatih/color" + "github.com/gotd/td/telegram" + + "github.com/iyear/tdl/pkg/kv" +) + +type Options struct { + KV kv.Storage + Client *telegram.Client +} + +// CheckFunc represents a diagnostic check function +type CheckFunc func(ctx context.Context, opts Options) + +// Check represents a registered diagnostic check +type Check struct { + Name string + Fn CheckFunc +} + +var ( + checks = make([]Check, 0) +) + +// Register registers a new diagnostic check +func Register(name string, fn CheckFunc) { + checks = append(checks, Check{ + Name: name, + Fn: fn, + }) +} + +// Run executes all registered diagnostic checks +func Run(ctx context.Context, opts Options) error { + color.Blue("=== TDL Doctor ===\n") + + // Separate checks into client-dependent and client-independent + var clientIndependent []Check + var clientDependent []Check + + for _, check := range checks { + if check.Name == "Checking database integrity" || check.Name == "Checking time synchronization" { + clientIndependent = append(clientIndependent, check) + } else { + clientDependent = append(clientDependent, check) + } + } + + // Run client-independent checks first + total := len(checks) + currentIndex := 0 + for _, check := range clientIndependent { + currentIndex++ + color.Cyan("\n[%d/%d] %s...", currentIndex, total, check.Name) + check.Fn(ctx, opts) + } + + // Run client-dependent checks within a single client.Run() + if len(clientDependent) > 0 && opts.Client != nil { + err := opts.Client.Run(ctx, func(ctx context.Context) error { + for _, check := range clientDependent { + currentIndex++ + color.Cyan("\n[%d/%d] %s...", currentIndex, total, check.Name) + check.Fn(ctx, opts) + } + return nil + }) + if err != nil { + color.Red("\n[FAIL] Client error: %v", err) + } + } else { + // Run checks without client + for _, check := range clientDependent { + currentIndex++ + color.Cyan("\n[%d/%d] %s...", currentIndex, total, check.Name) + check.Fn(ctx, opts) + } + } + + color.Green("\n=== Diagnosis Complete ===") + return nil +} + +// init registers all checks in order +func init() { + Register("Checking database integrity", checkDatabaseIntegrity) + Register("Checking Telegram server connectivity", checkConnectivity) + Register("Checking time synchronization", checkNTPTime) + Register("Checking login status", checkLoginStatus) +} diff --git a/app/doctor/login.go b/app/doctor/login.go new file mode 100644 index 000000000..3a0defdc6 --- /dev/null +++ b/app/doctor/login.go @@ -0,0 +1,47 @@ +package doctor + +import ( + "context" + "fmt" + + "github.com/fatih/color" +) + +func checkLoginStatus(ctx context.Context, opts Options) { + client := opts.Client + if client == nil { + color.Yellow(" [WARN] Client not provided, skipping login check") + return + } + + status, err := client.Auth().Status(ctx) + if err != nil { + color.Red(" [FAIL] Failed to check login status: %v", err) + return + } + + if !status.Authorized { + color.Yellow(" [WARN] Not logged in. Please run 'tdl login' first.") + return + } + + color.Green(" [OK] Login status: Authorized") + + // Get user info + user, err := client.Self(ctx) + if err != nil { + color.Yellow(" [WARN] Failed to get user info: %v", err) + return + } + + name := fmt.Sprintf("%s %s", user.FirstName, user.LastName) + if user.Username != "" { + color.White(" Account: %s (@%s)", name, user.Username) + } else { + color.White(" Account: %s", name) + } + color.White(" User ID: %d", user.ID) + if user.Phone != "" { + color.White(" Phone: %s", user.Phone) + } +} diff --git a/app/doctor/ntp.go b/app/doctor/ntp.go new file mode 100644 index 000000000..b142eef5d --- /dev/null +++ b/app/doctor/ntp.go @@ -0,0 +1,47 @@ +package doctor + +import ( + "context" + "time" + + "github.com/beevik/ntp" + "github.com/fatih/color" + "github.com/spf13/viper" + + "github.com/iyear/tdl/pkg/consts" +) + +func checkNTPTime(ctx context.Context, opts Options) { + ntpServer := viper.GetString(consts.FlagNTP) + if ntpServer == "" { + ntpServer = "pool.ntp.org" + } + + // Get NTP time + resp, err := ntp.Query(ntpServer) + if err != nil { + color.Red(" [FAIL] Failed to query NTP server (%s): %v", ntpServer, err) + return + } + + offset := resp.ClockOffset + localTime := time.Now() + ntpTime := localTime.Add(offset) + + color.White(" Local time: %s", localTime.Format("2006-01-02 15:04:05 MST")) + color.White(" NTP time: %s", ntpTime.Format("2006-01-02 15:04:05 MST")) + color.White(" Offset: %v", offset) + + // Warning if offset is too large + absOffset := offset + if absOffset < 0 { + absOffset = -absOffset + } + + if absOffset > 30*time.Second { + color.Yellow(" [WARN] Time offset is greater than 30 seconds. This may cause issues with Telegram.") + color.Yellow(" [WARN] Consider synchronizing your system time or using --ntp flag.") + } else { + color.Green(" [OK] Time synchronization is acceptable") + } +} diff --git a/cmd/doctor.go b/cmd/doctor.go new file mode 100644 index 000000000..630fea293 --- /dev/null +++ b/cmd/doctor.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "github.com/iyear/tdl/app/doctor" + "github.com/iyear/tdl/core/logctx" + "github.com/iyear/tdl/pkg/kv" + "github.com/iyear/tdl/pkg/tclient" +) + +func NewDoctor() *cobra.Command { + cmd := &cobra.Command{ + Use: "doctor", + Short: "Diagnose Telegram connection and configuration issues", + GroupID: groupTools.ID, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := logctx.Named(cmd.Context(), "doctor") + + // Get storage + storage := kv.From(ctx) + + // Create client options + o, err := tOptions(ctx) + if err != nil { + return err + } + + // Create client + client, err := tclient.New(ctx, o, false) + if err != nil { + return err + } + + // Run doctor + return doctor.Run(ctx, doctor.Options{ + KV: storage, + Client: client, + }) + }, + } + + return cmd +} diff --git a/cmd/root.go b/cmd/root.go index bd14a639a..43a9c26ac 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -139,7 +139,7 @@ func New() *cobra.Command { cmd.AddCommand(NewVersion(), NewLogin(), NewDownload(), NewForward(), NewChat(), NewUpload(), NewBackup(), NewRecover(), NewMigrate(), - NewGen(), NewExtension(em)) + NewGen(), NewDoctor(), NewExtension(em)) // append extension command to root exts, _ := em.List(context.Background(), false) diff --git a/core/storage/session.go b/core/storage/session.go index 5d6384cde..3d3c08e99 100644 --- a/core/storage/session.go +++ b/core/storage/session.go @@ -23,7 +23,7 @@ func (s *Session) LoadSession(ctx context.Context) ([]byte, error) { return nil, nil } - b, err := s.kv.Get(ctx, s.key()) + b, err := s.kv.Get(ctx, s.Key()) if err != nil { if errors.Is(err, ErrNotFound) { return nil, nil @@ -34,9 +34,9 @@ func (s *Session) LoadSession(ctx context.Context) ([]byte, error) { } func (s *Session) StoreSession(ctx context.Context, data []byte) error { - return s.kv.Set(ctx, s.key(), data) + return s.kv.Set(ctx, s.Key(), data) } -func (s *Session) key() string { +func (s *Session) Key() string { return keygen.New("session") } diff --git a/go.work.sum b/go.work.sum index 514a568f6..819d93ebd 100644 --- a/go.work.sum +++ b/go.work.sum @@ -24,8 +24,10 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU= github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY= +github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= @@ -52,6 +54,9 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP 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.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -68,10 +73,13 @@ github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkN github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= github.com/go-faster/sdk v0.22.0 h1:s/Pq3lJ00MrHMoEUzeAemPz7kXzfETw2iN6outyOHoY= github.com/go-faster/sdk v0.22.0/go.mod h1:UJWFlbuRJHmXJwl4JxStMbbIZtMAz4fxrD4CnuDXCIc= +github.com/go-faster/sdk v0.28.0/go.mod h1:Ts+Rd1B0ltePMxuuCwphkfPVtTIbJhV6jzsV46MVM5w= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-openapi/inflect v0.21.2 h1:0gClGlGcxifcJR56zwvhaOulnNgnhc4qTAkob5ObnSM= github.com/go-openapi/inflect v0.21.2/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= +github.com/go-openapi/inflect v0.21.3/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -91,6 +99,9 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gotd/getdoc v0.48.0 h1:oo0C/tNHOwZsuW5egYhrD4XLtlAnz2ubSfz6tdh0FEM= github.com/gotd/getdoc v0.48.0/go.mod h1:w5IDi4f2qAfBjk7CmzVKpTRU1/jGSZinwZ2Gxej1XvA= +github.com/gotd/getdoc v0.50.0/go.mod h1:7z7IrsCH+c0OEqVd127PV/Fy3jOej7Nlq+QrcUCQ8MQ= +github.com/gotd/td v0.133.0 h1:4wBM590McSUg6ooGjd+k4taAN8R50cyrOOkPV5cELa0= +github.com/gotd/td v0.133.0/go.mod h1:SEgKTinqT8UPQDbcvjqk2iflcUvmv4uKsIgpZKe/788= github.com/gotd/tl v0.4.0 h1:8k2z0drujiPyhpLDa9PRm/yU1Gwlfn3iUzeInPiXwMA= github.com/gotd/tl v0.4.0/go.mod h1:CMIcjPWFS4qxxJ+1Ce7U/ilbtPrkoVo/t8uhN5Y/D7c= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -119,10 +130,18 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/k0kubun/pp/v3 v3.4.1 h1:1WdFZDRRqe8UsR61N/2RoOZ3ziTEqgTPVqKrHeb779Y= github.com/k0kubun/pp/v3 v3.4.1/go.mod h1:+SiNiqKnBfw1Nkj82Lh5bIeKQOAkPy6Xw9CAZUZ8npI= +github.com/k0kubun/pp/v3 v3.5.0/go.mod h1:5lzno5ZZeEeTV/Ky6vs3g6d1U3WarDrH8k240vMtGro= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0= @@ -135,6 +154,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ogen-go/ogen v1.15.2/go.mod h1:bS+BP2cV7+IGjOM24znBmh+PrpZvYFXA7o3BNF4Hj2E= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM= @@ -155,15 +176,23 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= +github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= @@ -175,30 +204,95 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488 h1:3doPGa+Gg4snce233aCWnbZVFsyFMo/dR40KK/6skyE= golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0= google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY= @@ -212,3 +306,5 @@ google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 963726206d2ab0233330434c70bdeae8247f5d1c Mon Sep 17 00:00:00 2001 From: XMLHexagram Date: Sat, 8 Nov 2025 02:42:04 +0100 Subject: [PATCH 2/8] improve --- app/doctor/connectivity.go | 23 +++++++++-- app/doctor/database.go | 63 +++++++++++++++++++----------- app/doctor/doctor.go | 6 +-- app/doctor/login.go | 10 ++++- app/doctor/ntp.go | 80 ++++++++++++++++++++++++++++++++------ 5 files changed, 141 insertions(+), 41 deletions(-) diff --git a/app/doctor/connectivity.go b/app/doctor/connectivity.go index 42748f782..e0a97ac89 100644 --- a/app/doctor/connectivity.go +++ b/app/doctor/connectivity.go @@ -13,12 +13,29 @@ func checkConnectivity(ctx context.Context, opts Options) { return } - // Simple ping to check connectivity + color.White(" Testing help.getConfig...") _, err := client.API().HelpGetConfig(ctx) if err != nil { - color.Red(" [FAIL] Failed to connect to Telegram: %v", err) + color.Red(" [FAIL] Failed to access help.getConfig api: %v", err) return } + color.White(" Server configuration retrieved") - color.Green(" [OK] Successfully connected to Telegram server") + color.White(" Testing help.getNearestDc...") + nearestDc, err := client.API().HelpGetNearestDC(ctx) + if err != nil { + color.Red(" [FAIL] Failed to access help.getNearestDc api: %v", err) + return + } + color.White(" Nearest datacenter: DC%d (%s)", nearestDc.NearestDC, nearestDc.Country) + + color.White(" Testing langpack.getLanguages...") + _, err = client.API().LangpackGetLanguages(ctx, "") + if err != nil { + color.Red(" [FAIL] Failed to access langpack.getLanguage api: %v", err) + return + } + color.White(" Language pack accessible") + + color.Green(" [OK] Connectivity check completed successfully") } diff --git a/app/doctor/database.go b/app/doctor/database.go index 5702576db..15822a872 100644 --- a/app/doctor/database.go +++ b/app/doctor/database.go @@ -7,7 +7,6 @@ import ( "github.com/fatih/color" "github.com/spf13/viper" - "github.com/iyear/tdl/core/storage" "github.com/iyear/tdl/pkg/consts" "github.com/iyear/tdl/pkg/key" ) @@ -19,6 +18,8 @@ func checkDatabaseIntegrity(ctx context.Context, opts Options) { return } + hasIssues := false + // Check storage type storageType := storage.Name() color.White(" Storage type: %s", storageType) @@ -30,37 +31,41 @@ func checkDatabaseIntegrity(ctx context.Context, opts Options) { path := storageConfig["path"] if path == "" { color.Yellow(" [WARN] Storage path not configured") + hasIssues = true } else { color.White(" Storage path: %s", path) // Check if path exists if info, err := os.Stat(path); err != nil { if os.IsNotExist(err) { - color.Yellow(" [WARN] Storage path does not exist yet (will be created on first use)") + color.White(" Path status: does not exist yet (will be created on first use)") } else { - color.Red(" [FAIL] Cannot access storage path: %v", err) + color.Red(" Path error: cannot access: %v", err) + color.Red("[FAIL] Database integrity check failed") return } } else { // Show basic info about the storage if info.IsDir() { - color.White(" Storage is a directory") + color.White(" Path type: directory") } else { - color.White(" Storage is a file (size: %d bytes)", info.Size()) + color.White(" Path type: file (size: %d bytes)", info.Size()) } } } - // Check namespaces using already-opened storage - // This is safe because we're using the storage instance that was opened by root command + // Check namespaces + color.White(" Checking namespaces...") namespaces, err := storage.Namespaces() if err != nil { - color.Red(" [FAIL] Failed to list namespaces: %v", err) + color.Red(" Namespace error: %v", err) + color.Red(" [FAIL] Database integrity check failed") return } if len(namespaces) == 0 { color.Yellow(" [WARN] No namespaces found in storage") + hasIssues = true } else { color.White(" Found %d namespace(s): %v", len(namespaces), namespaces) } @@ -68,41 +73,55 @@ func checkDatabaseIntegrity(ctx context.Context, opts Options) { // Check current namespace has required keys currentNS := viper.GetString(consts.FlagNamespace) if currentNS != "" { - color.White(" Current namespace: %s", currentNS) + color.White(" Checking current namespace: %s", currentNS) nsStorage, err := storage.Open(currentNS) if err != nil { - color.Yellow(" [WARN] Failed to open namespace '%s': %v", currentNS, err) + color.Yellow(" [WARN] Failed to open namespace: %v", err) + hasIssues = true } else { - checkNamespaceKeys(ctx, nsStorage) + nsHasIssues := checkNamespaceKeys(ctx, nsStorage) + if nsHasIssues { + hasIssues = true + } } } - color.Green(" [OK] Database integrity check passed") + // Final status + if hasIssues { + color.Yellow(" [WARN] Database check completed with warnings") + } else { + color.Green(" [OK] Database integrity check passed") + } } -func checkNamespaceKeys(ctx context.Context, storage_ interface{}) { +func checkNamespaceKeys(ctx context.Context, storage interface{}) bool { type getter interface { Get(ctx context.Context, key string) ([]byte, error) } - st, ok := storage_.(getter) + st, ok := storage.(getter) if !ok { - return + return false } - // Check for session key (generated by keygen.New("session")) - a := storage.Session{} - if _, err := st.Get(ctx, a.Key()); err == nil { - color.White(" - Session data: present") + hasIssues := false + + // Check for session key + if _, err := st.Get(ctx, "session"); err == nil { + color.White(" - Session data: present") } else { - color.Yellow(" - Session data: missing (not logged in)") + color.White(" - Session data: missing (not logged in)") + hasIssues = true } // Check for app key if data, err := st.Get(ctx, key.App()); err == nil { - color.White(" - App config: present (%s)", string(data)) + color.White(" - App config: %s", string(data)) } else { - color.Yellow(" - App config: missing") + color.White(" - App config: missing") + hasIssues = true } + + return hasIssues } diff --git a/app/doctor/doctor.go b/app/doctor/doctor.go index 9e225a0de..cd5dcba57 100644 --- a/app/doctor/doctor.go +++ b/app/doctor/doctor.go @@ -82,14 +82,14 @@ func Run(ctx context.Context, opts Options) error { } } - color.Green("\n=== Diagnosis Complete ===") + color.Blue("\n=== Diagnosis Complete ===") return nil } // init registers all checks in order func init() { - Register("Checking database integrity", checkDatabaseIntegrity) - Register("Checking Telegram server connectivity", checkConnectivity) Register("Checking time synchronization", checkNTPTime) + Register("Checking Telegram server connectivity", checkConnectivity) + Register("Checking database integrity", checkDatabaseIntegrity) Register("Checking login status", checkLoginStatus) } diff --git a/app/doctor/login.go b/app/doctor/login.go index 3a0defdc6..9f0892a6e 100644 --- a/app/doctor/login.go +++ b/app/doctor/login.go @@ -14,6 +14,8 @@ func checkLoginStatus(ctx context.Context, opts Options) { return } + // Check authentication status + color.White(" Checking authentication status...") status, err := client.Auth().Status(ctx) if err != nil { color.Red(" [FAIL] Failed to check login status: %v", err) @@ -25,15 +27,16 @@ func checkLoginStatus(ctx context.Context, opts Options) { return } - color.Green(" [OK] Login status: Authorized") - // Get user info + color.White(" Fetching user information...") user, err := client.Self(ctx) if err != nil { color.Yellow(" [WARN] Failed to get user info: %v", err) + color.Red(" [Error] Login status: Authorized (but cannot fetch user details)") return } + // Display user information name := fmt.Sprintf("%s %s", user.FirstName, user.LastName) if user.Username != "" { color.White(" Account: %s (@%s)", name, user.Username) @@ -44,4 +47,7 @@ func checkLoginStatus(ctx context.Context, opts Options) { if user.Phone != "" { color.White(" Phone: %s", user.Phone) } + + // Final status + color.Green(" [OK] Login status: Authorized") } diff --git a/app/doctor/ntp.go b/app/doctor/ntp.go index b142eef5d..c45839ca1 100644 --- a/app/doctor/ntp.go +++ b/app/doctor/ntp.go @@ -2,6 +2,7 @@ package doctor import ( "context" + "runtime" "time" "github.com/beevik/ntp" @@ -11,36 +12,93 @@ import ( "github.com/iyear/tdl/pkg/consts" ) +func getDefaultNTPServers() []string { + switch runtime.GOOS { + case "darwin": // macOS + return []string{ + "time.apple.com", + "time.cloudflare.com", + "time.windows.com", + "time.google.com", + "pool.ntp.org", + } + case "windows": + return []string{ + "time.windows.com", + "time.cloudflare.com", + "time.apple.com", + "time.google.com", + "pool.ntp.org", + } + case "linux": + return []string{ + "ntp.ubuntu.com", + "time.cloudflare.com", + "time.google.com", + "pool.ntp.org", + } + default: + return []string{ + "time.cloudflare.com", + "time.google.com", + "pool.ntp.org", + } + } +} + func checkNTPTime(ctx context.Context, opts Options) { + var ntpServers []string + ntpServer := viper.GetString(consts.FlagNTP) - if ntpServer == "" { - ntpServer = "pool.ntp.org" + if ntpServer != "" { + ntpServers = []string{ntpServer} + } else { + ntpServers = getDefaultNTPServers() + } + + var resp *ntp.Response + var err error + var successServer string + + for _, server := range ntpServers { + color.White(" Querying NTP server: %s", server) + resp, err = ntp.Query(server) + if err != nil { + color.Yellow(" [WARN] Failed to query %s: %v", server, err) + continue + } + successServer = server + break } - // Get NTP time - resp, err := ntp.Query(ntpServer) if err != nil { - color.Red(" [FAIL] Failed to query NTP server (%s): %v", ntpServer, err) + color.Red(" [FAIL] Failed to query all NTP servers") return } + color.White(" Successfully connected to: %s", successServer) + offset := resp.ClockOffset localTime := time.Now() ntpTime := localTime.Add(offset) - color.White(" Local time: %s", localTime.Format("2006-01-02 15:04:05 MST")) - color.White(" NTP time: %s", ntpTime.Format("2006-01-02 15:04:05 MST")) + color.White(" Local time: %s", localTime.Format("2006-01-02 15:04:05.000000000 MST")) + color.White(" NTP time: %s", ntpTime.Format("2006-01-02 15:04:05.000000000 MST")) color.White(" Offset: %v", offset) - // Warning if offset is too large absOffset := offset if absOffset < 0 { absOffset = -absOffset } - if absOffset > 30*time.Second { - color.Yellow(" [WARN] Time offset is greater than 30 seconds. This may cause issues with Telegram.") - color.Yellow(" [WARN] Consider synchronizing your system time or using --ntp flag.") + if absOffset > time.Second && absOffset < 10*time.Second { + color.Yellow(" [WARN] Time offset is between 1 and 10 seconds") + color.White(" Your system time is slightly off, but should work normally") + color.White(" Consider synchronizing your system time for better accuracy") + } else if absOffset > 10*time.Second { + color.Yellow(" [WARN] Time offset is greater than 10 seconds") + color.Yellow(" This may cause issues with Telegram authentication") + color.Yellow(" Consider synchronizing your system time or using --ntp flag") } else { color.Green(" [OK] Time synchronization is acceptable") } From 9b45592e9b2ead787720fb97d8147fda4d19fd1a Mon Sep 17 00:00:00 2001 From: XMLHexagram Date: Sat, 8 Nov 2025 02:52:17 +0100 Subject: [PATCH 3/8] improve output format --- app/doctor/connectivity.go | 10 +++++----- app/doctor/database.go | 16 ++++++++-------- app/doctor/login.go | 13 ++++++------- app/doctor/ntp.go | 10 +++++----- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/app/doctor/connectivity.go b/app/doctor/connectivity.go index e0a97ac89..420945553 100644 --- a/app/doctor/connectivity.go +++ b/app/doctor/connectivity.go @@ -9,14 +9,14 @@ import ( func checkConnectivity(ctx context.Context, opts Options) { client := opts.Client if client == nil { - color.Yellow(" [WARN] Client not provided, skipping connectivity check") + color.Yellow("[WARN] Client not provided, skipping connectivity check") return } color.White(" Testing help.getConfig...") _, err := client.API().HelpGetConfig(ctx) if err != nil { - color.Red(" [FAIL] Failed to access help.getConfig api: %v", err) + color.Red("[FAIL] Failed to access help.getConfig api: %v", err) return } color.White(" Server configuration retrieved") @@ -24,7 +24,7 @@ func checkConnectivity(ctx context.Context, opts Options) { color.White(" Testing help.getNearestDc...") nearestDc, err := client.API().HelpGetNearestDC(ctx) if err != nil { - color.Red(" [FAIL] Failed to access help.getNearestDc api: %v", err) + color.Red("[FAIL] Failed to access help.getNearestDc api: %v", err) return } color.White(" Nearest datacenter: DC%d (%s)", nearestDc.NearestDC, nearestDc.Country) @@ -32,10 +32,10 @@ func checkConnectivity(ctx context.Context, opts Options) { color.White(" Testing langpack.getLanguages...") _, err = client.API().LangpackGetLanguages(ctx, "") if err != nil { - color.Red(" [FAIL] Failed to access langpack.getLanguage api: %v", err) + color.Red("[FAIL] Failed to access langpack.getLanguage api: %v", err) return } color.White(" Language pack accessible") - color.Green(" [OK] Connectivity check completed successfully") + color.Green("[OK] Connectivity check completed successfully") } diff --git a/app/doctor/database.go b/app/doctor/database.go index 15822a872..c299c77ff 100644 --- a/app/doctor/database.go +++ b/app/doctor/database.go @@ -14,7 +14,7 @@ import ( func checkDatabaseIntegrity(ctx context.Context, opts Options) { storage := opts.KV if storage == nil { - color.Red(" [FAIL] Storage not initialized") + color.Red("[FAIL] Storage not initialized") return } @@ -30,7 +30,7 @@ func checkDatabaseIntegrity(ctx context.Context, opts Options) { // Check if storage path exists and is accessible path := storageConfig["path"] if path == "" { - color.Yellow(" [WARN] Storage path not configured") + color.Yellow("[WARN] Storage path not configured") hasIssues = true } else { color.White(" Storage path: %s", path) @@ -59,12 +59,12 @@ func checkDatabaseIntegrity(ctx context.Context, opts Options) { namespaces, err := storage.Namespaces() if err != nil { color.Red(" Namespace error: %v", err) - color.Red(" [FAIL] Database integrity check failed") + color.Red("[FAIL] Database integrity check failed") return } if len(namespaces) == 0 { - color.Yellow(" [WARN] No namespaces found in storage") + color.Yellow("[WARN] No namespaces found in storage") hasIssues = true } else { color.White(" Found %d namespace(s): %v", len(namespaces), namespaces) @@ -77,7 +77,7 @@ func checkDatabaseIntegrity(ctx context.Context, opts Options) { nsStorage, err := storage.Open(currentNS) if err != nil { - color.Yellow(" [WARN] Failed to open namespace: %v", err) + color.Yellow("[WARN] Failed to open namespace: %v", err) hasIssues = true } else { nsHasIssues := checkNamespaceKeys(ctx, nsStorage) @@ -89,9 +89,9 @@ func checkDatabaseIntegrity(ctx context.Context, opts Options) { // Final status if hasIssues { - color.Yellow(" [WARN] Database check completed with warnings") + color.Yellow("[WARN] Database check completed with warnings") } else { - color.Green(" [OK] Database integrity check passed") + color.Green("[OK] Database integrity check passed") } } @@ -109,7 +109,7 @@ func checkNamespaceKeys(ctx context.Context, storage interface{}) bool { // Check for session key if _, err := st.Get(ctx, "session"); err == nil { - color.White(" - Session data: present") + color.White(" - Session data: exist") } else { color.White(" - Session data: missing (not logged in)") hasIssues = true diff --git a/app/doctor/login.go b/app/doctor/login.go index 9f0892a6e..fbb5dbfb6 100644 --- a/app/doctor/login.go +++ b/app/doctor/login.go @@ -10,7 +10,7 @@ import ( func checkLoginStatus(ctx context.Context, opts Options) { client := opts.Client if client == nil { - color.Yellow(" [WARN] Client not provided, skipping login check") + color.Yellow("[WARN] Client not provided, skipping login check") return } @@ -18,12 +18,12 @@ func checkLoginStatus(ctx context.Context, opts Options) { color.White(" Checking authentication status...") status, err := client.Auth().Status(ctx) if err != nil { - color.Red(" [FAIL] Failed to check login status: %v", err) + color.Red("[FAIL] Failed to check login status: %v", err) return } if !status.Authorized { - color.Yellow(" [WARN] Not logged in. Please run 'tdl login' first.") + color.Yellow("[WARN] Not logged in. Please run 'tdl login' first.") return } @@ -31,8 +31,8 @@ func checkLoginStatus(ctx context.Context, opts Options) { color.White(" Fetching user information...") user, err := client.Self(ctx) if err != nil { - color.Yellow(" [WARN] Failed to get user info: %v", err) - color.Red(" [Error] Login status: Authorized (but cannot fetch user details)") + color.Yellow("[Error] Failed to get user info: %v", err) + color.Red("[Error] Login status: Authorized (but cannot fetch user details)") return } @@ -48,6 +48,5 @@ func checkLoginStatus(ctx context.Context, opts Options) { color.White(" Phone: %s", user.Phone) } - // Final status - color.Green(" [OK] Login status: Authorized") + color.Green("[OK] Login status: Authorized") } diff --git a/app/doctor/ntp.go b/app/doctor/ntp.go index c45839ca1..7242b4945 100644 --- a/app/doctor/ntp.go +++ b/app/doctor/ntp.go @@ -64,7 +64,7 @@ func checkNTPTime(ctx context.Context, opts Options) { color.White(" Querying NTP server: %s", server) resp, err = ntp.Query(server) if err != nil { - color.Yellow(" [WARN] Failed to query %s: %v", server, err) + color.Yellow("[WARN] Failed to query %s: %v", server, err) continue } successServer = server @@ -72,7 +72,7 @@ func checkNTPTime(ctx context.Context, opts Options) { } if err != nil { - color.Red(" [FAIL] Failed to query all NTP servers") + color.Red("[FAIL] Failed to query all NTP servers") return } @@ -92,14 +92,14 @@ func checkNTPTime(ctx context.Context, opts Options) { } if absOffset > time.Second && absOffset < 10*time.Second { - color.Yellow(" [WARN] Time offset is between 1 and 10 seconds") + color.Yellow("[WARN] Time offset is between 1 and 10 seconds") color.White(" Your system time is slightly off, but should work normally") color.White(" Consider synchronizing your system time for better accuracy") } else if absOffset > 10*time.Second { - color.Yellow(" [WARN] Time offset is greater than 10 seconds") + color.Yellow("[WARN] Time offset is greater than 10 seconds") color.Yellow(" This may cause issues with Telegram authentication") color.Yellow(" Consider synchronizing your system time or using --ntp flag") } else { - color.Green(" [OK] Time synchronization is acceptable") + color.Green("[OK] Time synchronization is acceptable") } } From d1df20f3bbf55f23b44c044d49ffeff7712cf8bf Mon Sep 17 00:00:00 2001 From: XMLHexagram Date: Sat, 8 Nov 2025 03:01:12 +0100 Subject: [PATCH 4/8] fix: golangci-lint --- app/doctor/doctor.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/doctor/doctor.go b/app/doctor/doctor.go index cd5dcba57..a17bc83b5 100644 --- a/app/doctor/doctor.go +++ b/app/doctor/doctor.go @@ -23,9 +23,7 @@ type Check struct { Fn CheckFunc } -var ( - checks = make([]Check, 0) -) +var checks = make([]Check, 0) // Register registers a new diagnostic check func Register(name string, fn CheckFunc) { From 087046e04269f6cb95c558035a2294b7066b616c Mon Sep 17 00:00:00 2001 From: XMLHexagram Date: Sat, 8 Nov 2025 03:47:33 +0100 Subject: [PATCH 5/8] doc: add doctor cmd doc --- .../content/en/getting-started/quick-start.md | 10 ++ docs/content/en/guide/doctor.md | 91 +++++++++++++++++++ docs/content/en/more/troubleshooting.md | 23 ++++- .../content/zh/getting-started/quick-start.md | 10 ++ docs/content/zh/guide/doctor.md | 91 +++++++++++++++++++ docs/content/zh/more/troubleshooting.md | 22 ++++- 6 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 docs/content/en/guide/doctor.md create mode 100644 docs/content/zh/guide/doctor.md diff --git a/docs/content/en/getting-started/quick-start.md b/docs/content/en/getting-started/quick-start.md index 479ffef60..2419643aa 100644 --- a/docs/content/en/getting-started/quick-start.md +++ b/docs/content/en/getting-started/quick-start.md @@ -47,6 +47,16 @@ tdl login -T qr tdl login -T code {{< /command >}} +## Verify Setup + +After logging in, you can verify your setup is working correctly: + +{{< command >}} +tdl doctor +{{< /command >}} + +This will check your time synchronization, connectivity, database, and login status. See [Doctor](/guide/doctor) for more details. + ## Download We download media from Telegram official channel: diff --git a/docs/content/en/guide/doctor.md b/docs/content/en/guide/doctor.md new file mode 100644 index 000000000..e5dc140cc --- /dev/null +++ b/docs/content/en/guide/doctor.md @@ -0,0 +1,91 @@ +--- +title: "Doctor" +weight: 15 +--- + +# Doctor + +The `doctor` command helps you diagnose common issues with TDL by running a series of automated checks. + +## Usage + +{{< command >}} +tdl doctor +{{< /command >}} + +## What It Checks + +The doctor command performs the following diagnostic checks: + +### 1. Time Synchronization + +Checks if your system time is synchronized with NTP servers. Telegram requires accurate time for authentication. + +- Tests multiple NTP servers** with automatic fallback + +**Status indicators:** +- **OK**: Time offset < 1 second +- **WARN**: Time offset between 1-10 seconds (may work, but consider syncing) +- **WARN**: Time offset > 10 seconds (may cause authentication issues) + +### 2. Telegram Server Connectivity + +Tests connection to Telegram servers using unauthenticated API endpoints: + +- `help.getConfig` - Basic server configuration +- `help.getNearestDc` - Nearest datacenter location +- `langpack.getLanguages` - Language pack availability + +This verifies network connectivity and access to Telegram's API. + +### 3. Database Integrity + +Checks your local TDL storage: + +- Storage type and path +- Namespace availability +- Session data presence +- App configuration + +### 4. Login Status + +Verifies your authentication status and displays account information: + +- Authentication status +- Account name and username +- User ID and phone number + +## Custom NTP Server + +You can specify a custom NTP server using the `--ntp` flag: + +{{< command >}} +tdl doctor --ntp time.google.com +{{< /command >}} + +## Common Issues + +### Time Synchronization Failed + +If all NTP servers fail, check your network connection and firewall settings. NTP uses UDP port 123. + +### Connectivity Tests Failed + +If connectivity tests fail: +1. Check your internet connection +2. Verify firewall settings allow connections to Telegram +3. Try using a proxy or VPN if Telegram is blocked in your region + +### Database Issues + +If database checks show warnings: +- Missing session data means you need to login: `tdl login` +- Database errors may require clean all local storages and re-initialization + +### Not Authorized + +If you're not logged in, run the login command first: + +{{< command >}} +tdl login +{{< /command >}} diff --git a/docs/content/en/more/troubleshooting.md b/docs/content/en/more/troubleshooting.md index 8736f32d8..e058fd157 100644 --- a/docs/content/en/more/troubleshooting.md +++ b/docs/content/en/more/troubleshooting.md @@ -5,6 +5,22 @@ weight: 40 # Troubleshooting +## Diagnostic Tool + +Before diving into specific issues, try running the doctor command to automatically diagnose common problems: + +{{< command >}} +tdl doctor +{{< /command >}} + +The doctor command will check: +- Time synchronization with NTP servers +- Telegram server connectivity +- Database integrity +- Login status + +For more details, see [Doctor](/guide/doctor). + ## Best Practices How to minimize the risk of blocking? @@ -19,8 +35,11 @@ How to minimize the risk of blocking? #### Q: Why no response after entering the command? And why there is `msg_id too high` in the log? -**A:** Check if you need to use a proxy (use `proxy` flag); Check if your system's local time is correct (use `ntp` flag -or calibrate system time) +**A:** First, run `tdl doctor` to diagnose the issue automatically. The doctor command will check your time synchronization and connectivity. + +If the issue persists: +- Check if you need to use a proxy (use `--proxy` flag) +- Check if your system's local time is correct (use `--ntp` flag or calibrate system time) If that doesn't work, run again with `--debug` flag. Then file a new issue and paste your log in the issue. diff --git a/docs/content/zh/getting-started/quick-start.md b/docs/content/zh/getting-started/quick-start.md index f04c31d55..0be3b2ca0 100644 --- a/docs/content/zh/getting-started/quick-start.md +++ b/docs/content/zh/getting-started/quick-start.md @@ -45,6 +45,16 @@ tdl login -T qr tdl login -T code {{< /command >}} +## 验证设置 + +登录后,您可以验证设置是否正常工作: + +{{< command >}} +tdl doctor +{{< /command >}} + +这将检查您的时间同步、连通性、数据库和登录状态。更多详细信息请参阅 [诊断](/zh/guide/doctor)。 + ## 下载 我们从 Telegram 官方频道下载文件: diff --git a/docs/content/zh/guide/doctor.md b/docs/content/zh/guide/doctor.md new file mode 100644 index 000000000..fd904eeeb --- /dev/null +++ b/docs/content/zh/guide/doctor.md @@ -0,0 +1,91 @@ +--- +title: "诊断" +weight: 15 +--- + +# 诊断 + +`doctor` 命令通过运行一系列自动检查来帮助您诊断 TDL 的常见问题。 + +## 用法 + +{{< command >}} +tdl doctor +{{< /command >}} + +## 检查项目 + +诊断命令会执行以下检查: + +### 1. 时间同步 + +检查您的系统时间是否与 NTP 服务器同步。Telegram 需要准确的时间进行身份验证。 + +- 测试多个 NTP 服务器,自动故障转移 + +**状态指示:** +- **OK**: 时间偏移 < 1 秒 +- **WARN**: 时间偏移 1-10 秒之间(可能正常工作,但建议同步) +- **WARN**: 时间偏移 > 10 秒(可能导致认证问题) + +### 2. Telegram 服务器连通性 + +使用无需认证的 API 端点测试与 Telegram 服务器的连接: + +- `help.getConfig` - 基础服务器配置 +- `help.getNearestDc` - 最近的数据中心位置 +- `langpack.getLanguages` - 语言包可用性 + +这可以验证网络连接和对 Telegram API 的访问。 + +### 3. 数据库完整性 + +检查您的本地 TDL 存储: + +- 存储类型和路径 +- 命名空间可用性 +- 会话数据是否存在 +- 应用配置 + +### 4. 登录状态 + +验证您的身份验证状态并显示账户信息: + +- 认证状态 +- 账户名称和用户名 +- 用户 ID 和电话号码 + +## 自定义 NTP 服务器 + +您可以使用 `--ntp` 参数指定自定义的 NTP 服务器: + +{{< command >}} +tdl doctor --ntp time.google.com +{{< /command >}} + +## 常见问题 + +### 时间同步失败 + +如果所有 NTP 服务器都失败,请检查您的网络连接和防火墙设置。NTP 使用 UDP 端口 123。 + +### 连通性测试失败 + +如果连通性测试失败: +1. 检查您的互联网连接 +2. 验证防火墙设置是否允许连接到 Telegram +3. 如果您所在地区屏蔽了 Telegram,尝试使用代理或 VPN + +### 数据库问题 + +如果数据库检查显示警告: +- 缺少会话数据意味着您需要登录:`tdl login` +- 数据库错误可能需要清理所有本地存储并重新初始化 + +### 未授权 + +如果您尚未登录,请先运行登录命令: + +{{< command >}} +tdl login +{{< /command >}} diff --git a/docs/content/zh/more/troubleshooting.md b/docs/content/zh/more/troubleshooting.md index 733388e36..c98e2602a 100644 --- a/docs/content/zh/more/troubleshooting.md +++ b/docs/content/zh/more/troubleshooting.md @@ -5,6 +5,22 @@ weight: 40 # 疑难解答 +## 诊断工具 + +在深入研究具体问题之前,请尝试运行诊断命令来自动诊断常见问题: + +{{< command >}} +tdl doctor +{{< /command >}} + +诊断命令将检查: +- 与 NTP 服务器的时间同步 +- Telegram 服务器连通性 +- 数据库完整性 +- 登录状态 + +更多详细信息,请参阅 [诊断](/zh/guide/doctor)。 + ## 最佳实践 如何减小封号的风险? @@ -19,7 +35,11 @@ weight: 40 #### Q: 输入命令后为什么没有响应?日志中为什么出现 `msg_id too high`? -**A:** 检查是否需要使用代理(使用 `--proxy` 选项);检查您系统的本地时间是否正确(使用 `--ntp` 选项或校准系统时间) +**A:** 首先,运行 `tdl doctor` 自动诊断问题。诊断命令将检查您的时间同步和连通性。 + +如果问题仍然存在: +- 检查是否需要使用代理(使用 `--proxy` 选项) +- 检查您系统的本地时间是否正确(使用 `--ntp` 选项或校准系统时间) 如果仍然无法解决问题,请使用 `--debug` 标志重新运行。然后创建一个新的 ISSUE 并将日志粘贴到问题中。 From 5c7f71dd712cc1b93177eff9813846888e497795 Mon Sep 17 00:00:00 2001 From: XMLHexagram Date: Thu, 13 Nov 2025 01:53:06 +0100 Subject: [PATCH 6/8] refactor(doctor/reviews): extract checks to interface and add constants --- app/doctor/doctor.go | 89 ++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 32 deletions(-) diff --git a/app/doctor/doctor.go b/app/doctor/doctor.go index a17bc83b5..f8b0d910e 100644 --- a/app/doctor/doctor.go +++ b/app/doctor/doctor.go @@ -9,43 +9,50 @@ import ( "github.com/iyear/tdl/pkg/kv" ) +const ( + CheckNameTimeSync = "Checking time synchronization" + CheckNameConnectivity = "Checking Telegram server connectivity" + CheckNameDatabaseInteg = "Checking database integrity" + CheckNameLoginStatus = "Checking login status" +) + +// init registers all checks in order +func init() { + Register(newCheck(CheckNameTimeSync, false, checkNTPTime)) + Register(newCheck(CheckNameConnectivity, true, checkConnectivity)) + Register(newCheck(CheckNameDatabaseInteg, false, checkDatabaseIntegrity)) + Register(newCheck(CheckNameLoginStatus, true, checkLoginStatus)) +} + type Options struct { KV kv.Storage Client *telegram.Client } -// CheckFunc represents a diagnostic check function -type CheckFunc func(ctx context.Context, opts Options) - -// Check represents a registered diagnostic check -type Check struct { - Name string - Fn CheckFunc +type Checker interface { + Name() string + NeedClient() bool + Run(ctx context.Context, opts Options) } -var checks = make([]Check, 0) +var checks = make([]Checker, 0) -// Register registers a new diagnostic check -func Register(name string, fn CheckFunc) { - checks = append(checks, Check{ - Name: name, - Fn: fn, - }) +func Register(checker Checker) { + checks = append(checks, checker) } -// Run executes all registered diagnostic checks func Run(ctx context.Context, opts Options) error { color.Blue("=== TDL Doctor ===\n") // Separate checks into client-dependent and client-independent - var clientIndependent []Check - var clientDependent []Check + var clientIndependent []Checker + var clientDependent []Checker for _, check := range checks { - if check.Name == "Checking database integrity" || check.Name == "Checking time synchronization" { - clientIndependent = append(clientIndependent, check) - } else { + if check.NeedClient() { clientDependent = append(clientDependent, check) + } else { + clientIndependent = append(clientIndependent, check) } } @@ -54,8 +61,8 @@ func Run(ctx context.Context, opts Options) error { currentIndex := 0 for _, check := range clientIndependent { currentIndex++ - color.Cyan("\n[%d/%d] %s...", currentIndex, total, check.Name) - check.Fn(ctx, opts) + color.Cyan("\n[%d/%d] %s...", currentIndex, total, check.Name()) + check.Run(ctx, opts) } // Run client-dependent checks within a single client.Run() @@ -63,8 +70,8 @@ func Run(ctx context.Context, opts Options) error { err := opts.Client.Run(ctx, func(ctx context.Context) error { for _, check := range clientDependent { currentIndex++ - color.Cyan("\n[%d/%d] %s...", currentIndex, total, check.Name) - check.Fn(ctx, opts) + color.Cyan("\n[%d/%d] %s...", currentIndex, total, check.Name()) + check.Run(ctx, opts) } return nil }) @@ -75,8 +82,8 @@ func Run(ctx context.Context, opts Options) error { // Run checks without client for _, check := range clientDependent { currentIndex++ - color.Cyan("\n[%d/%d] %s...", currentIndex, total, check.Name) - check.Fn(ctx, opts) + color.Cyan("\n[%d/%d] %s...", currentIndex, total, check.Name()) + check.Run(ctx, opts) } } @@ -84,10 +91,28 @@ func Run(ctx context.Context, opts Options) error { return nil } -// init registers all checks in order -func init() { - Register("Checking time synchronization", checkNTPTime) - Register("Checking Telegram server connectivity", checkConnectivity) - Register("Checking database integrity", checkDatabaseIntegrity) - Register("Checking login status", checkLoginStatus) +type checkImpl struct { + name string + needClient bool + runFunc func(ctx context.Context, opts Options) +} + +func (c *checkImpl) Name() string { + return c.name +} + +func (c *checkImpl) NeedClient() bool { + return c.needClient +} + +func (c *checkImpl) Run(ctx context.Context, opts Options) { + c.runFunc(ctx, opts) +} + +func newCheck(name string, needClient bool, runFunc func(ctx context.Context, opts Options)) Checker { + return &checkImpl{ + name: name, + needClient: needClient, + runFunc: runFunc, + } } From c3a2f71a9c3f1c9eb2a423e7244342aff66f49d4 Mon Sep 17 00:00:00 2001 From: XMLHexagram Date: Thu, 13 Nov 2025 02:01:58 +0100 Subject: [PATCH 7/8] refactor(doctor/reviews): use kv.NewWithMap instead of accessing storage options directly --- app/doctor/database.go | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/app/doctor/database.go b/app/doctor/database.go index c299c77ff..313e9b0fd 100644 --- a/app/doctor/database.go +++ b/app/doctor/database.go @@ -2,13 +2,13 @@ package doctor import ( "context" - "os" "github.com/fatih/color" "github.com/spf13/viper" "github.com/iyear/tdl/pkg/consts" "github.com/iyear/tdl/pkg/key" + "github.com/iyear/tdl/pkg/kv" ) func checkDatabaseIntegrity(ctx context.Context, opts Options) { @@ -24,35 +24,15 @@ func checkDatabaseIntegrity(ctx context.Context, opts Options) { storageType := storage.Name() color.White(" Storage type: %s", storageType) - // Get storage configuration storageConfig := viper.GetStringMapString(consts.FlagStorage) - - // Check if storage path exists and is accessible - path := storageConfig["path"] - if path == "" { - color.Yellow("[WARN] Storage path not configured") - hasIssues = true - } else { - color.White(" Storage path: %s", path) - - // Check if path exists - if info, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - color.White(" Path status: does not exist yet (will be created on first use)") - } else { - color.Red(" Path error: cannot access: %v", err) - color.Red("[FAIL] Database integrity check failed") - return - } - } else { - // Show basic info about the storage - if info.IsDir() { - color.White(" Path type: directory") - } else { - color.White(" Path type: file (size: %d bytes)", info.Size()) - } - } + testStorage, err := kv.NewWithMap(storageConfig) + if err != nil { + color.Red(" Storage configuration error: %v", err) + color.Red("[FAIL] Database integrity check failed") + return } + testStorage.Close() + color.White(" Storage configuration: valid") // Check namespaces color.White(" Checking namespaces...") From d7fbe5f7d74aee93497dd1352d8fcfb2f1204ac5 Mon Sep 17 00:00:00 2001 From: XMLHexagram Date: Thu, 13 Nov 2025 02:12:17 +0100 Subject: [PATCH 8/8] refactor(doctor/reviews): use storage.Storage type instead of interface{} --- app/doctor/database.go | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/app/doctor/database.go b/app/doctor/database.go index 313e9b0fd..4ee975c89 100644 --- a/app/doctor/database.go +++ b/app/doctor/database.go @@ -6,6 +6,7 @@ import ( "github.com/fatih/color" "github.com/spf13/viper" + "github.com/iyear/tdl/core/storage" "github.com/iyear/tdl/pkg/consts" "github.com/iyear/tdl/pkg/key" "github.com/iyear/tdl/pkg/kv" @@ -75,28 +76,17 @@ func checkDatabaseIntegrity(ctx context.Context, opts Options) { } } -func checkNamespaceKeys(ctx context.Context, storage interface{}) bool { - type getter interface { - Get(ctx context.Context, key string) ([]byte, error) - } - - st, ok := storage.(getter) - if !ok { - return false - } - +func checkNamespaceKeys(ctx context.Context, storage storage.Storage) bool { hasIssues := false - // Check for session key - if _, err := st.Get(ctx, "session"); err == nil { + if _, err := storage.Get(ctx, "session"); err == nil { color.White(" - Session data: exist") } else { color.White(" - Session data: missing (not logged in)") hasIssues = true } - // Check for app key - if data, err := st.Get(ctx, key.App()); err == nil { + if data, err := storage.Get(ctx, key.App()); err == nil { color.White(" - App config: %s", string(data)) } else { color.White(" - App config: missing")