Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions app/doctor/connectivity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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
}

color.White(" Testing help.getConfig...")
_, err := client.API().HelpGetConfig(ctx)
if err != nil {
color.Red("[FAIL] Failed to access help.getConfig api: %v", err)
return
}
color.White(" Server configuration retrieved")

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")
}
97 changes: 97 additions & 0 deletions app/doctor/database.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package doctor

import (
"context"

"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"
)

func checkDatabaseIntegrity(ctx context.Context, opts Options) {
storage := opts.KV
if storage == nil {
color.Red("[FAIL] Storage not initialized")
return
}

hasIssues := false

// Check storage type
storageType := storage.Name()
color.White(" Storage type: %s", storageType)

storageConfig := viper.GetStringMapString(consts.FlagStorage)
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...")
namespaces, err := storage.Namespaces()
if err != nil {
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)
}

// Check current namespace has required keys
currentNS := viper.GetString(consts.FlagNamespace)
if currentNS != "" {
color.White(" Checking current namespace: %s", currentNS)

nsStorage, err := storage.Open(currentNS)
if err != nil {
color.Yellow("[WARN] Failed to open namespace: %v", err)
hasIssues = true
} else {
nsHasIssues := checkNamespaceKeys(ctx, nsStorage)
if nsHasIssues {
hasIssues = true
}
}
}

// 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 storage.Storage) bool {
hasIssues := false

if _, err := storage.Get(ctx, "session"); err == nil {
color.White(" - Session data: exist")
} else {
color.White(" - Session data: missing (not logged in)")
hasIssues = true
}

if data, err := storage.Get(ctx, key.App()); err == nil {
color.White(" - App config: %s", string(data))
} else {
color.White(" - App config: missing")
hasIssues = true
}

return hasIssues
}
118 changes: 118 additions & 0 deletions app/doctor/doctor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package doctor

import (
"context"

"github.com/fatih/color"
"github.com/gotd/td/telegram"

"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
}

type Checker interface {
Name() string
NeedClient() bool
Run(ctx context.Context, opts Options)
}

var checks = make([]Checker, 0)

func Register(checker Checker) {
checks = append(checks, checker)
}

func Run(ctx context.Context, opts Options) error {
color.Blue("=== TDL Doctor ===\n")

// Separate checks into client-dependent and client-independent
var clientIndependent []Checker
var clientDependent []Checker

for _, check := range checks {
if check.NeedClient() {
clientDependent = append(clientDependent, check)
} else {
clientIndependent = append(clientIndependent, 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.Run(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.Run(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.Run(ctx, opts)
}
}

color.Blue("\n=== Diagnosis Complete ===")
return nil
}

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,
}
}
52 changes: 52 additions & 0 deletions app/doctor/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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
}

// 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)
return
}

if !status.Authorized {
color.Yellow("[WARN] Not logged in. Please run 'tdl login' first.")
return
}

// Get user info
color.White(" Fetching user information...")
user, err := client.Self(ctx)
if err != nil {
color.Yellow("[Error] 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)
} else {
color.White(" Account: %s", name)
}
color.White(" User ID: %d", user.ID)
if user.Phone != "" {
color.White(" Phone: %s", user.Phone)
}

color.Green("[OK] Login status: Authorized")
}
Loading