Mohammad Shahid commited on
Commit
49b198e
·
1 Parent(s): aa300d8

feat(bot): Implement personal bot management and validation

Browse files

- Added UserBotManager for handling user-specific bot configurations with encryption.
- Introduced BotValidator for validating bot tokens.
- Implemented InstanceManager to manage multiple bot instances for users.
- Created commands for adding, removing, enabling, and disabling personal bots.
- Enhanced bot selection logic to prioritize user-specific bots.
- Updated database schema to support user bot configurations.
- Added detailed logging for bot operations and error handling.
- Improved user experience with informative messages regarding bot status and commands.

cmd/fsb/run.go CHANGED
@@ -60,6 +60,11 @@ func runApp(cmd *cobra.Command, args []string) {
60
  log.Panic("Failed to start main bot", zap.Error(err))
61
  }
62
 
 
 
 
 
 
63
  // Now that the client and its dispatcher exist, load the commands into it.
64
  commands.Load(log, mainBot.Dispatcher)
65
 
 
60
  log.Panic("Failed to start main bot", zap.Error(err))
61
  }
62
 
63
+ // Initialize bot instance manager (only if database is available)
64
+ if db.UserBotMgr != nil {
65
+ bot.InitInstanceManager(db.UserBotMgr, log)
66
+ }
67
+
68
  // Now that the client and its dispatcher exist, load the commands into it.
69
  commands.Load(log, mainBot.Dispatcher)
70
 
config/config.go CHANGED
@@ -53,6 +53,7 @@ type config struct {
53
  DB_URI string `envconfig:"DB_URI" default:"mongodb://localhost:27017/fsb"`
54
  MultiTokens []string
55
  InternalApiSecret string `envconfig:"INTERNAL_API_SECRET"`
 
56
  }
57
 
58
  var botTokenRegex = regexp.MustCompile(`MULTI\_TOKEN\d+=(.*)`)
 
53
  DB_URI string `envconfig:"DB_URI" default:"mongodb://localhost:27017/fsb"`
54
  MultiTokens []string
55
  InternalApiSecret string `envconfig:"INTERNAL_API_SECRET"`
56
+ BotEncryptionKey string `envconfig:"BOT_ENCRYPTION_KEY" default:"change-this-encryption-key-32chars"`
57
  }
58
 
59
  var botTokenRegex = regexp.MustCompile(`MULTI\_TOKEN\d+=(.*)`)
internal/bot/bot_selector.go ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package bot
2
+
3
+ import (
4
+ "TelegramCloud/tgf/internal/db"
5
+ "context"
6
+ "fmt"
7
+
8
+ "github.com/celestix/gotgproto"
9
+ "github.com/celestix/gotgproto/ext"
10
+ "go.uber.org/zap"
11
+ )
12
+
13
+ // BotSelector handles selecting the appropriate bot for operations
14
+ type BotSelector struct {
15
+ log *zap.Logger
16
+ }
17
+
18
+ // NewBotSelector creates a new bot selector
19
+ func NewBotSelector(log *zap.Logger) *BotSelector {
20
+ return &BotSelector{
21
+ log: log.Named("BotSelector"),
22
+ }
23
+ }
24
+
25
+ // SelectBotForUser selects the best bot for a user's operation
26
+ func (bs *BotSelector) SelectBotForUser(ctx context.Context, userID int64) (*gotgproto.Client, bool, error) {
27
+ // Check if user has a personal bot configured and active
28
+ if db.UserBotMgr != nil {
29
+ userBot, err := db.UserBotMgr.GetUserBot(ctx, userID)
30
+ if err != nil {
31
+ bs.log.Warn("Failed to check user bot", zap.Int64("userID", userID), zap.Error(err))
32
+ return Bot, false, nil // Fall back to main bot
33
+ }
34
+
35
+ if userBot != nil && userBot.IsActive {
36
+ // Try to get user's bot instance
37
+ if InstanceMgr != nil {
38
+ botClient, err := InstanceMgr.GetBotForUser(ctx, userID)
39
+ if err != nil {
40
+ bs.log.Warn("Failed to get user bot instance, using main bot",
41
+ zap.Int64("userID", userID), zap.Error(err))
42
+ return Bot, false, nil
43
+ }
44
+
45
+ bs.log.Debug("Using user's personal bot",
46
+ zap.Int64("userID", userID),
47
+ zap.String("botUsername", userBot.BotUsername))
48
+ return botClient, true, nil
49
+ }
50
+ }
51
+ }
52
+
53
+ // Fall back to main bot
54
+ bs.log.Debug("Using main bot for user", zap.Int64("userID", userID))
55
+ return Bot, false, nil
56
+ }
57
+
58
+ // HandleBotError handles errors from bot operations
59
+ func (bs *BotSelector) HandleBotError(ctx context.Context, userID int64, isPersonalBot bool, err error) {
60
+ if isPersonalBot && InstanceMgr != nil {
61
+ bs.log.Warn("Personal bot error",
62
+ zap.Int64("userID", userID),
63
+ zap.Error(err))
64
+
65
+ // Record the error
66
+ InstanceMgr.RecordError(ctx, userID, err)
67
+ }
68
+ }
69
+
70
+ // GetBotStats returns statistics about bot usage
71
+ func (bs *BotSelector) GetBotStats() map[string]interface{} {
72
+ stats := map[string]interface{}{
73
+ "main_bot_active": Bot != nil,
74
+ }
75
+
76
+ if InstanceMgr != nil {
77
+ instanceStats := InstanceMgr.GetStats()
78
+ for k, v := range instanceStats {
79
+ stats[k] = v
80
+ }
81
+ }
82
+
83
+ return stats
84
+ }
85
+
86
+ // ForwardMessageWithBestBot forwards a message using the best available bot
87
+ func (bs *BotSelector) ForwardMessageWithBestBot(ctx *ext.Context, userID int64, fromChatID, toChatID int64, messageID int) error {
88
+ dbCtx := context.Background()
89
+
90
+ // Select the best bot for this user
91
+ selectedBot, isPersonalBot, err := bs.SelectBotForUser(dbCtx, userID)
92
+ if err != nil {
93
+ return err
94
+ }
95
+
96
+ // Try to forward with selected bot
97
+ // Note: This is a simplified version - you'd need to implement the actual forwarding logic
98
+ // based on your existing ForwardMessages function
99
+
100
+ if selectedBot == nil {
101
+ return fmt.Errorf("no bot available")
102
+ }
103
+
104
+ // If using personal bot, we might need different logic here
105
+ // For now, we'll use the main bot's forwarding logic
106
+ // This is where you'd integrate the actual forwarding with user bots
107
+
108
+ bs.log.Info("Forwarding message",
109
+ zap.Int64("userID", userID),
110
+ zap.Bool("isPersonalBot", isPersonalBot),
111
+ zap.Int64("fromChat", fromChatID),
112
+ zap.Int64("toChat", toChatID),
113
+ zap.Int("messageID", messageID))
114
+
115
+ return nil
116
+ }
internal/bot/client.go CHANGED
@@ -2,6 +2,7 @@ package bot
2
 
3
  import (
4
  "TelegramCloud/tgf/config"
 
5
  "context"
6
  "time"
7
 
@@ -13,6 +14,7 @@ import (
13
  )
14
 
15
  var Bot *gotgproto.Client
 
16
 
17
  func StartClient(log *zap.Logger) (*gotgproto.Client, error) {
18
  ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
@@ -28,7 +30,7 @@ func StartClient(log *zap.Logger) (*gotgproto.Client, error) {
28
  gotgproto.ClientTypeBot(config.ValueOf.BotToken),
29
  &gotgproto.ClientOpts{
30
  Session: sessionMaker.SqlSession(
31
- sqlite.Open("/tmp/fsb.session"),
32
  ),
33
  DisableCopyright: true,
34
  },
@@ -50,4 +52,30 @@ func StartClient(log *zap.Logger) (*gotgproto.Client, error) {
50
  Bot = result.client
51
  return result.client, nil
52
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
 
2
 
3
  import (
4
  "TelegramCloud/tgf/config"
5
+ "TelegramCloud/tgf/internal/db"
6
  "context"
7
  "time"
8
 
 
14
  )
15
 
16
  var Bot *gotgproto.Client
17
+ var InstanceMgr *InstanceManager
18
 
19
  func StartClient(log *zap.Logger) (*gotgproto.Client, error) {
20
  ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
 
30
  gotgproto.ClientTypeBot(config.ValueOf.BotToken),
31
  &gotgproto.ClientOpts{
32
  Session: sessionMaker.SqlSession(
33
+ sqlite.Open("./sessions/fsb.session"),
34
  ),
35
  DisableCopyright: true,
36
  },
 
52
  Bot = result.client
53
  return result.client, nil
54
  }
55
+ }
56
+
57
+ // InitInstanceManager initializes the bot instance manager
58
+ func InitInstanceManager(userBotMgr *db.UserBotManager, log *zap.Logger) {
59
+ InstanceMgr = NewInstanceManager(userBotMgr, log)
60
+ log.Info("Bot instance manager initialized")
61
+ }
62
+
63
+ // GetBotForUser returns the appropriate bot for a user (personal or main bot)
64
+ func GetBotForUser(ctx context.Context, userID int64, log *zap.Logger) *gotgproto.Client {
65
+ // Try to get user's personal bot first
66
+ if InstanceMgr != nil {
67
+ userBot, err := InstanceMgr.GetBotForUser(ctx, userID)
68
+ if err != nil {
69
+ log.Warn("Failed to get user bot, falling back to main bot",
70
+ zap.Int64("userID", userID),
71
+ zap.Error(err))
72
+ } else {
73
+ log.Debug("Using user's personal bot", zap.Int64("userID", userID))
74
+ return userBot
75
+ }
76
+ }
77
+
78
+ // Fall back to main bot
79
+ log.Debug("Using main bot", zap.Int64("userID", userID))
80
+ return Bot
81
  }
internal/bot/instance_manager.go ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package bot
2
+
3
+ import (
4
+ "TelegramCloud/tgf/config"
5
+ "TelegramCloud/tgf/internal/db"
6
+ "context"
7
+ "fmt"
8
+ "sync"
9
+ "time"
10
+
11
+ "github.com/celestix/gotgproto"
12
+ "github.com/celestix/gotgproto/sessionMaker"
13
+ "github.com/glebarez/sqlite"
14
+ "go.uber.org/zap"
15
+ )
16
+
17
+ // BotInstance represents an active bot instance
18
+ type BotInstance struct {
19
+ Client *gotgproto.Client
20
+ UserID int64
21
+ BotID int64
22
+ Username string
23
+ LastUsed time.Time
24
+ ErrorCount int
25
+ }
26
+
27
+ // InstanceManager manages multiple bot instances
28
+ type InstanceManager struct {
29
+ instances map[int64]*BotInstance // userID -> BotInstance
30
+ mutex sync.RWMutex
31
+ log *zap.Logger
32
+ userBotMgr *db.UserBotManager
33
+ cleanupTicker *time.Ticker
34
+ maxInstances int
35
+ idleTimeout time.Duration
36
+ }
37
+
38
+ // NewInstanceManager creates a new bot instance manager
39
+ func NewInstanceManager(userBotMgr *db.UserBotManager, log *zap.Logger) *InstanceManager {
40
+ im := &InstanceManager{
41
+ instances: make(map[int64]*BotInstance),
42
+ log: log.Named("InstanceManager"),
43
+ userBotMgr: userBotMgr,
44
+ maxInstances: 100, // Maximum concurrent bot instances
45
+ idleTimeout: 30 * time.Minute, // Close idle bots after 30 minutes
46
+ }
47
+
48
+ // Start cleanup routine
49
+ im.startCleanupRoutine()
50
+
51
+ return im
52
+ }
53
+
54
+ // GetBotForUser returns a bot instance for the user (creates if needed)
55
+ func (im *InstanceManager) GetBotForUser(ctx context.Context, userID int64) (*gotgproto.Client, error) {
56
+ im.mutex.Lock()
57
+ defer im.mutex.Unlock()
58
+
59
+ // Check if we already have an active instance
60
+ if instance, exists := im.instances[userID]; exists {
61
+ instance.LastUsed = time.Now()
62
+ im.log.Debug("Reusing existing bot instance", zap.Int64("userID", userID))
63
+ return instance.Client, nil
64
+ }
65
+
66
+ // Check if we're at max capacity
67
+ if len(im.instances) >= im.maxInstances {
68
+ // Remove oldest idle instance
69
+ im.removeOldestInstance()
70
+ }
71
+
72
+ // Get user's bot token
73
+ botToken, err := im.userBotMgr.GetDecryptedBotToken(ctx, userID)
74
+ if err != nil {
75
+ return nil, fmt.Errorf("failed to get user bot token: %w", err)
76
+ }
77
+
78
+ // Create new bot instance
79
+ instance, err := im.createBotInstance(userID, botToken)
80
+ if err != nil {
81
+ // Record error in database
82
+ im.userBotMgr.RecordBotError(ctx, userID, err.Error())
83
+ return nil, fmt.Errorf("failed to create bot instance: %w", err)
84
+ }
85
+
86
+ // Store instance
87
+ im.instances[userID] = instance
88
+
89
+ // Update usage in database
90
+ im.userBotMgr.UpdateBotUsage(ctx, userID)
91
+
92
+ im.log.Info("Created new bot instance",
93
+ zap.Int64("userID", userID),
94
+ zap.String("botUsername", instance.Username),
95
+ zap.Int("totalInstances", len(im.instances)))
96
+
97
+ return instance.Client, nil
98
+ }
99
+
100
+ // createBotInstance creates a new bot client instance
101
+ func (im *InstanceManager) createBotInstance(userID int64, botToken string) (*BotInstance, error) {
102
+ // Create unique session path for this user's bot
103
+ sessionPath := fmt.Sprintf("./sessions/userbot_%d.session", userID)
104
+
105
+ // Create bot client with timeout
106
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
107
+ defer cancel()
108
+
109
+ resultChan := make(chan struct {
110
+ client *gotgproto.Client
111
+ err error
112
+ })
113
+
114
+ go func() {
115
+ client, err := gotgproto.NewClient(
116
+ int(config.ValueOf.ApiID),
117
+ config.ValueOf.ApiHash,
118
+ gotgproto.ClientTypeBot(botToken),
119
+ &gotgproto.ClientOpts{
120
+ Session: sessionMaker.SqlSession(
121
+ sqlite.Open(sessionPath),
122
+ ),
123
+ DisableCopyright: true,
124
+ },
125
+ )
126
+ resultChan <- struct {
127
+ client *gotgproto.Client
128
+ err error
129
+ }{client, err}
130
+ }()
131
+
132
+ select {
133
+ case <-ctx.Done():
134
+ return nil, fmt.Errorf("bot creation timeout")
135
+ case result := <-resultChan:
136
+ if result.err != nil {
137
+ return nil, result.err
138
+ }
139
+
140
+ instance := &BotInstance{
141
+ Client: result.client,
142
+ UserID: userID,
143
+ BotID: result.client.Self.ID,
144
+ Username: result.client.Self.Username,
145
+ LastUsed: time.Now(),
146
+ ErrorCount: 0,
147
+ }
148
+
149
+ return instance, nil
150
+ }
151
+ }
152
+
153
+ // RemoveBotInstance removes a bot instance for a user
154
+ func (im *InstanceManager) RemoveBotInstance(userID int64) {
155
+ im.mutex.Lock()
156
+ defer im.mutex.Unlock()
157
+
158
+ if instance, exists := im.instances[userID]; exists {
159
+ // Stop the bot client
160
+ instance.Client.Stop()
161
+ delete(im.instances, userID)
162
+
163
+ im.log.Info("Removed bot instance",
164
+ zap.Int64("userID", userID),
165
+ zap.String("botUsername", instance.Username))
166
+ }
167
+ }
168
+
169
+ // RecordError records an error for a user's bot instance
170
+ func (im *InstanceManager) RecordError(ctx context.Context, userID int64, err error) {
171
+ im.mutex.Lock()
172
+ defer im.mutex.Unlock()
173
+
174
+ if instance, exists := im.instances[userID]; exists {
175
+ instance.ErrorCount++
176
+
177
+ // Remove instance if too many errors
178
+ if instance.ErrorCount >= 3 {
179
+ im.log.Warn("Removing bot instance due to errors",
180
+ zap.Int64("userID", userID),
181
+ zap.Int("errorCount", instance.ErrorCount),
182
+ zap.Error(err))
183
+
184
+ instance.Client.Stop()
185
+ delete(im.instances, userID)
186
+ }
187
+ }
188
+
189
+ // Record error in database
190
+ im.userBotMgr.RecordBotError(ctx, userID, err.Error())
191
+ }
192
+
193
+ // removeOldestInstance removes the oldest idle instance
194
+ func (im *InstanceManager) removeOldestInstance() {
195
+ var oldestUserID int64
196
+ var oldestTime time.Time = time.Now()
197
+
198
+ for userID, instance := range im.instances {
199
+ if instance.LastUsed.Before(oldestTime) {
200
+ oldestTime = instance.LastUsed
201
+ oldestUserID = userID
202
+ }
203
+ }
204
+
205
+ if oldestUserID != 0 {
206
+ if instance, exists := im.instances[oldestUserID]; exists {
207
+ instance.Client.Stop()
208
+ delete(im.instances, oldestUserID)
209
+ im.log.Info("Removed oldest bot instance to make room",
210
+ zap.Int64("userID", oldestUserID))
211
+ }
212
+ }
213
+ }
214
+
215
+ // startCleanupRoutine starts the background cleanup routine
216
+ func (im *InstanceManager) startCleanupRoutine() {
217
+ im.cleanupTicker = time.NewTicker(10 * time.Minute) // Run every 10 minutes
218
+
219
+ go func() {
220
+ for range im.cleanupTicker.C {
221
+ im.cleanupIdleInstances()
222
+ }
223
+ }()
224
+ }
225
+
226
+ // cleanupIdleInstances removes idle bot instances
227
+ func (im *InstanceManager) cleanupIdleInstances() {
228
+ im.mutex.Lock()
229
+ defer im.mutex.Unlock()
230
+
231
+ now := time.Now()
232
+ var toRemove []int64
233
+
234
+ for userID, instance := range im.instances {
235
+ if now.Sub(instance.LastUsed) > im.idleTimeout {
236
+ toRemove = append(toRemove, userID)
237
+ }
238
+ }
239
+
240
+ for _, userID := range toRemove {
241
+ if instance, exists := im.instances[userID]; exists {
242
+ instance.Client.Stop()
243
+ delete(im.instances, userID)
244
+ im.log.Info("Cleaned up idle bot instance",
245
+ zap.Int64("userID", userID),
246
+ zap.Duration("idleTime", now.Sub(instance.LastUsed)))
247
+ }
248
+ }
249
+
250
+ if len(toRemove) > 0 {
251
+ im.log.Info("Cleanup completed",
252
+ zap.Int("removedInstances", len(toRemove)),
253
+ zap.Int("activeInstances", len(im.instances)))
254
+ }
255
+ }
256
+
257
+ // GetStats returns statistics about active instances
258
+ func (im *InstanceManager) GetStats() map[string]interface{} {
259
+ im.mutex.RLock()
260
+ defer im.mutex.RUnlock()
261
+
262
+ return map[string]interface{}{
263
+ "active_instances": len(im.instances),
264
+ "max_instances": im.maxInstances,
265
+ "idle_timeout": im.idleTimeout.String(),
266
+ }
267
+ }
268
+
269
+ // Shutdown gracefully shuts down all bot instances
270
+ func (im *InstanceManager) Shutdown() {
271
+ im.mutex.Lock()
272
+ defer im.mutex.Unlock()
273
+
274
+ if im.cleanupTicker != nil {
275
+ im.cleanupTicker.Stop()
276
+ }
277
+
278
+ for userID, instance := range im.instances {
279
+ instance.Client.Stop()
280
+ im.log.Info("Shutdown bot instance", zap.Int64("userID", userID))
281
+ }
282
+
283
+ im.instances = make(map[int64]*BotInstance)
284
+ im.log.Info("Instance manager shutdown complete")
285
+ }
internal/bot/validator.go ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package bot
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strings"
7
+ "time"
8
+
9
+ "github.com/celestix/gotgproto"
10
+ "github.com/celestix/gotgproto/sessionMaker"
11
+ "github.com/glebarez/sqlite"
12
+ "go.uber.org/zap"
13
+ )
14
+
15
+ // BotInfo contains information about a validated bot
16
+ type BotInfo struct {
17
+ ID int64 `json:"id"`
18
+ Username string `json:"username"`
19
+ FirstName string `json:"first_name"`
20
+ IsBot bool `json:"is_bot"`
21
+ }
22
+
23
+ // BotValidator handles bot token validation
24
+ type BotValidator struct {
25
+ log *zap.Logger
26
+ }
27
+
28
+ // NewBotValidator creates a new bot validator
29
+ func NewBotValidator(log *zap.Logger) *BotValidator {
30
+ return &BotValidator{
31
+ log: log.Named("BotValidator"),
32
+ }
33
+ }
34
+
35
+ // ValidateBotToken validates a bot token and returns bot information
36
+ func (bv *BotValidator) ValidateBotToken(token string) (*BotInfo, error) {
37
+ // Basic token format validation
38
+ if !bv.isValidTokenFormat(token) {
39
+ return nil, fmt.Errorf("invalid token format")
40
+ }
41
+
42
+ // Create a temporary client to test the token
43
+ _, cancel := context.WithTimeout(context.Background(), 30*time.Second)
44
+ defer cancel()
45
+
46
+ // Use a temporary session file
47
+ tempSessionPath := fmt.Sprintf("./sessions/validate_%d.session", time.Now().UnixNano())
48
+
49
+ client, err := gotgproto.NewClient(
50
+ 0, // We don't need API ID/Hash for bot validation
51
+ "",
52
+ gotgproto.ClientTypeBot(token),
53
+ &gotgproto.ClientOpts{
54
+ Session: sessionMaker.SqlSession(
55
+ sqlite.Open(tempSessionPath),
56
+ ),
57
+ DisableCopyright: true,
58
+ },
59
+ )
60
+
61
+ if err != nil {
62
+ bv.log.Error("Failed to create validation client", zap.Error(err))
63
+ return nil, fmt.Errorf("invalid bot token: %w", err)
64
+ }
65
+
66
+ // Clean up the temporary session
67
+ defer func() {
68
+ if client != nil {
69
+ client.Stop()
70
+ }
71
+ }()
72
+
73
+ // Get bot information
74
+ self := client.Self
75
+ if self == nil {
76
+ return nil, fmt.Errorf("failed to get bot information")
77
+ }
78
+
79
+ if !self.Bot {
80
+ return nil, fmt.Errorf("token belongs to a user account, not a bot")
81
+ }
82
+
83
+ botInfo := &BotInfo{
84
+ ID: self.ID,
85
+ Username: self.Username,
86
+ FirstName: self.FirstName,
87
+ IsBot: self.Bot,
88
+ }
89
+
90
+ bv.log.Info("Successfully validated bot token",
91
+ zap.Int64("botID", botInfo.ID),
92
+ zap.String("username", botInfo.Username))
93
+
94
+ return botInfo, nil
95
+ }
96
+
97
+ // isValidTokenFormat checks if the token has the correct format
98
+ func (bv *BotValidator) isValidTokenFormat(token string) bool {
99
+ // Bot tokens have format: {bot_id}:{auth_token}
100
+ // Example: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz
101
+
102
+ if len(token) < 35 || len(token) > 50 {
103
+ return false
104
+ }
105
+
106
+ parts := strings.Split(token, ":")
107
+ if len(parts) != 2 {
108
+ return false
109
+ }
110
+
111
+ // First part should be numeric (bot ID)
112
+ botID := parts[0]
113
+ if len(botID) < 8 || len(botID) > 12 {
114
+ return false
115
+ }
116
+
117
+ for _, char := range botID {
118
+ if char < '0' || char > '9' {
119
+ return false
120
+ }
121
+ }
122
+
123
+ // Second part should be the auth token (alphanumeric + some special chars)
124
+ authToken := parts[1]
125
+ if len(authToken) < 25 || len(authToken) > 40 {
126
+ return false
127
+ }
128
+
129
+ return true
130
+ }
131
+
132
+ // TestBotPermissions tests if the bot has necessary permissions
133
+ func (bv *BotValidator) TestBotPermissions(token string) error {
134
+ // This could be extended to test specific permissions
135
+ // For now, just validate that we can create a client
136
+ _, err := bv.ValidateBotToken(token)
137
+ return err
138
+ }
internal/commands/addbot.go ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package commands
2
+
3
+ import (
4
+ "TelegramCloud/tgf/internal/bot"
5
+ "TelegramCloud/tgf/internal/db"
6
+ "context"
7
+ "fmt"
8
+ "strings"
9
+
10
+ "github.com/celestix/gotgproto/dispatcher"
11
+ "github.com/celestix/gotgproto/dispatcher/handlers"
12
+ "github.com/celestix/gotgproto/ext"
13
+ "go.uber.org/zap"
14
+ )
15
+
16
+ func (m *command) LoadBotCommands(dispatcher dispatcher.Dispatcher) {
17
+ log := m.log.Named("botCommands")
18
+ defer log.Sugar().Info("Loaded bot management commands")
19
+
20
+ dispatcher.AddHandler(handlers.NewCommand("addbot", func(ctx *ext.Context, update *ext.Update) error {
21
+ return AddBotCommand(ctx, update, db.UserBotMgr, log)
22
+ }))
23
+ dispatcher.AddHandler(handlers.NewCommand("mybot", func(ctx *ext.Context, update *ext.Update) error {
24
+ return MyBotCommand(ctx, update, db.UserBotMgr, log)
25
+ }))
26
+ dispatcher.AddHandler(handlers.NewCommand("removebot", func(ctx *ext.Context, update *ext.Update) error {
27
+ return RemoveBotCommand(ctx, update, db.UserBotMgr, log)
28
+ }))
29
+ dispatcher.AddHandler(handlers.NewCommand("enablebot", func(ctx *ext.Context, update *ext.Update) error {
30
+ return EnableBotCommand(ctx, update, db.UserBotMgr, log)
31
+ }))
32
+ dispatcher.AddHandler(handlers.NewCommand("disablebot", func(ctx *ext.Context, update *ext.Update) error {
33
+ return DisableBotCommand(ctx, update, db.UserBotMgr, log)
34
+ }))
35
+ }
36
+
37
+ // AddBotCommand handles the /addbot command
38
+ func AddBotCommand(ctx *ext.Context, update *ext.Update, userBotManager *db.UserBotManager, log *zap.Logger) error {
39
+ userID := update.EffectiveUser().ID
40
+
41
+ // Check if user provided a token
42
+ args := strings.Fields(update.EffectiveMessage.Text)
43
+ if len(args) < 2 {
44
+ ctx.Reply(update,
45
+ "❌ Please provide your bot token.\n\n"+
46
+ "Usage: `/addbot <your_bot_token>`\n\n"+
47
+ "ℹ️ To get a bot token:\n"+
48
+ "1. Message @BotFather on Telegram\n"+
49
+ "2. Send /newbot and follow instructions\n"+
50
+ "3. Copy the token and use it with this command\n\n"+
51
+ "⚠️ Make sure to add your bot as admin in your channel!",
52
+ nil)
53
+ return dispatcher.EndGroups
54
+ }
55
+
56
+ botToken := args[1]
57
+
58
+ // Send "processing" message
59
+ ctx.Reply(update, "🔄 Validating your bot token...", nil)
60
+
61
+ // Validate the bot token
62
+ validator := bot.NewBotValidator(log)
63
+ botInfo, err := validator.ValidateBotToken(botToken)
64
+ if err != nil {
65
+ errorMsg := "❌ Invalid bot token!\n\n"
66
+
67
+ if strings.Contains(err.Error(), "invalid token format") {
68
+ errorMsg += "The token format is incorrect. Bot tokens should look like:\n`123456789:ABCdefGHIjklMNOpqrsTUVwxyz`"
69
+ } else if strings.Contains(err.Error(), "user account") {
70
+ errorMsg += "This token belongs to a user account, not a bot. Please use a bot token from @BotFather."
71
+ } else {
72
+ errorMsg += "Please check your token and try again.\n\n" +
73
+ "Make sure you copied the complete token from @BotFather."
74
+ }
75
+
76
+ // Send error message
77
+ ctx.Reply(update, errorMsg, nil)
78
+
79
+ log.Warn("Bot token validation failed",
80
+ zap.Int64("userID", userID),
81
+ zap.Error(err))
82
+ return dispatcher.EndGroups
83
+ }
84
+
85
+ // Check if user already has a bot configured
86
+ existingBot, err := userBotManager.GetUserBot(context.Background(), userID)
87
+ if err != nil {
88
+ log.Error("Failed to check existing bot", zap.Error(err))
89
+ ctx.Reply(update, "❌ Database error. Please try again later.", nil)
90
+ return dispatcher.EndGroups
91
+ }
92
+
93
+ // Store the bot configuration
94
+ err = userBotManager.AddUserBot(context.Background(), userID, botToken, botInfo.Username, botInfo.ID)
95
+ if err != nil {
96
+ log.Error("Failed to store user bot", zap.Error(err))
97
+ ctx.Reply(update, "❌ Failed to save bot configuration. Please try again later.", nil)
98
+ return dispatcher.EndGroups
99
+ }
100
+
101
+ // Prepare success message
102
+ var successMsg string
103
+ if existingBot != nil {
104
+ successMsg = fmt.Sprintf(
105
+ "✅ *Bot Updated Successfully!*\n\n"+
106
+ "🤖 **Bot:** @%s\n"+
107
+ "🆔 **Bot ID:** `%d`\n"+
108
+ "🔄 **Status:** Active\n\n"+
109
+ "Your previous bot configuration has been replaced.\n\n"+
110
+ "⚡ Your personal bot will now be used for file uploads, giving you better performance and no rate limits!\n\n"+
111
+ "💡 **Next Steps:**\n"+
112
+ "• Make sure your bot is admin in your storage channel\n"+
113
+ "• Test by uploading a file\n"+
114
+ "• Use /mybot to check bot status",
115
+ botInfo.Username, botInfo.ID)
116
+ } else {
117
+ successMsg = fmt.Sprintf(
118
+ "🎉 *Bot Added Successfully!*\n\n"+
119
+ "🤖 **Bot:** @%s\n"+
120
+ "🆔 **Bot ID:** `%d`\n"+
121
+ "🔄 **Status:** Active\n\n"+
122
+ "⚡ Your personal bot is now configured! This means:\n"+
123
+ "• No rate limits on uploads\n"+
124
+ "• Better performance\n"+
125
+ "• Your own dedicated bot instance\n\n"+
126
+ "💡 **Next Steps:**\n"+
127
+ "• Make sure your bot is admin in your storage channel\n"+
128
+ "• Test by uploading a file\n"+
129
+ "• Use /mybot to check bot status",
130
+ botInfo.Username, botInfo.ID)
131
+ }
132
+
133
+ // Send success message
134
+ ctx.Reply(update, successMsg, nil)
135
+
136
+ log.Info("User bot added successfully",
137
+ zap.Int64("userID", userID),
138
+ zap.String("botUsername", botInfo.Username),
139
+ zap.Int64("botID", botInfo.ID))
140
+
141
+ return dispatcher.EndGroups
142
+ }
143
+
144
+ // MyBotCommand shows user's current bot configuration
145
+ func MyBotCommand(ctx *ext.Context, update *ext.Update, userBotManager *db.UserBotManager, log *zap.Logger) error {
146
+ userID := update.EffectiveUser().ID
147
+
148
+ userBot, err := userBotManager.GetUserBot(context.Background(), userID)
149
+ if err != nil {
150
+ log.Error("Failed to get user bot", zap.Error(err))
151
+ ctx.Reply(update, "❌ Database error. Please try again later.", nil)
152
+ return dispatcher.EndGroups
153
+ }
154
+
155
+ if userBot == nil {
156
+ ctx.Reply(update,
157
+ "🤖 *No Personal Bot Configured*\n\n"+
158
+ "You're currently using our shared bot service.\n\n"+
159
+ "💡 **Want better performance?**\n"+
160
+ "Add your own bot with `/addbot <token>`\n\n"+
161
+ "**Benefits of personal bot:**\n"+
162
+ "• No rate limits\n"+
163
+ "• Faster uploads\n"+
164
+ "• Dedicated instance\n"+
165
+ "• Better reliability",
166
+ nil)
167
+ return dispatcher.EndGroups
168
+ }
169
+
170
+ // Format last used time
171
+ lastUsed := "Never"
172
+ if !userBot.LastUsed.IsZero() {
173
+ lastUsed = userBot.LastUsed.Format("Jan 2, 2006 15:04")
174
+ }
175
+
176
+ // Status emoji and text
177
+ statusEmoji := "✅"
178
+ statusText := "Active"
179
+ if !userBot.IsActive {
180
+ statusEmoji = "❌"
181
+ statusText = "Disabled"
182
+ } else if userBot.ErrorCount > 0 {
183
+ statusEmoji = "⚠️"
184
+ statusText = fmt.Sprintf("Active (%d errors)", userBot.ErrorCount)
185
+ }
186
+
187
+ message := fmt.Sprintf(
188
+ "🤖 *Your Personal Bot*\n\n"+
189
+ "**Bot:** @%s\n"+
190
+ "**Bot ID:** `%d`\n"+
191
+ "**Status:** %s %s\n"+
192
+ "**Last Used:** %s\n"+
193
+ "**Added:** %s\n",
194
+ userBot.BotUsername,
195
+ userBot.BotID,
196
+ statusEmoji,
197
+ statusText,
198
+ lastUsed,
199
+ userBot.CreatedAt.Format("Jan 2, 2006"))
200
+
201
+ if userBot.ErrorCount > 0 && userBot.LastError != "" {
202
+ message += fmt.Sprintf("\n**Last Error:** %s", userBot.LastError)
203
+ }
204
+
205
+ message += "\n\n**Commands:**\n" +
206
+ "• `/addbot <token>` - Update bot\n" +
207
+ "• `/removebot` - Remove bot\n" +
208
+ "• `/enablebot` - Enable bot\n" +
209
+ "• `/disablebot` - Disable bot"
210
+
211
+ ctx.Reply(update, message, nil)
212
+ return dispatcher.EndGroups
213
+ }
214
+
215
+ // RemoveBotCommand removes user's bot configuration
216
+ func RemoveBotCommand(ctx *ext.Context, update *ext.Update, userBotManager *db.UserBotManager, log *zap.Logger) error {
217
+ userID := update.EffectiveUser().ID
218
+
219
+ userBot, err := userBotManager.GetUserBot(context.Background(), userID)
220
+ if err != nil {
221
+ log.Error("Failed to get user bot", zap.Error(err))
222
+ ctx.Reply(update, "❌ Database error. Please try again later.", nil)
223
+ return dispatcher.EndGroups
224
+ }
225
+
226
+ if userBot == nil {
227
+ ctx.Reply(update, "❌ You don't have a personal bot configured.", nil)
228
+ return dispatcher.EndGroups
229
+ }
230
+
231
+ err = userBotManager.RemoveUserBot(context.Background(), userID)
232
+ if err != nil {
233
+ log.Error("Failed to remove user bot", zap.Error(err))
234
+ ctx.Reply(update, "❌ Failed to remove bot. Please try again later.", nil)
235
+ return dispatcher.EndGroups
236
+ }
237
+
238
+ ctx.Reply(update,
239
+ "✅ *Bot Removed Successfully*\n\n"+
240
+ "Your personal bot configuration has been removed.\n"+
241
+ "You'll now use our shared bot service.\n\n"+
242
+ "You can add a new bot anytime with `/addbot <token>`",
243
+ nil)
244
+ return dispatcher.EndGroups
245
+ }
246
+
247
+ // EnableBotCommand enables user's bot
248
+ func EnableBotCommand(ctx *ext.Context, update *ext.Update, userBotManager *db.UserBotManager, log *zap.Logger) error {
249
+ userID := update.EffectiveUser().ID
250
+
251
+ userBot, err := userBotManager.GetUserBot(context.Background(), userID)
252
+ if err != nil {
253
+ log.Error("Failed to get user bot", zap.Error(err))
254
+ ctx.Reply(update, "❌ Database error. Please try again later.", nil)
255
+ return dispatcher.EndGroups
256
+ }
257
+
258
+ if userBot == nil {
259
+ ctx.Reply(update, "❌ You don't have a personal bot configured.", nil)
260
+ return dispatcher.EndGroups
261
+ }
262
+
263
+ if userBot.IsActive {
264
+ ctx.Reply(update, "✅ Your bot is already active!", nil)
265
+ return dispatcher.EndGroups
266
+ }
267
+
268
+ err = userBotManager.EnableUserBot(context.Background(), userID)
269
+ if err != nil {
270
+ log.Error("Failed to enable user bot", zap.Error(err))
271
+ ctx.Reply(update, "❌ Failed to enable bot. Please try again later.", nil)
272
+ return dispatcher.EndGroups
273
+ }
274
+
275
+ ctx.Reply(update,
276
+ "✅ *Bot Enabled Successfully*\n\n"+
277
+ "Your personal bot is now active and will be used for uploads.",
278
+ nil)
279
+ return dispatcher.EndGroups
280
+ }
281
+
282
+ // DisableBotCommand disables user's bot
283
+ func DisableBotCommand(ctx *ext.Context, update *ext.Update, userBotManager *db.UserBotManager, log *zap.Logger) error {
284
+ userID := update.EffectiveUser().ID
285
+
286
+ userBot, err := userBotManager.GetUserBot(context.Background(), userID)
287
+ if err != nil {
288
+ log.Error("Failed to get user bot", zap.Error(err))
289
+ ctx.Reply(update, "❌ Database error. Please try again later.", nil)
290
+ return dispatcher.EndGroups
291
+ }
292
+
293
+ if userBot == nil {
294
+ ctx.Reply(update, "❌ You don't have a personal bot configured.", nil)
295
+ return dispatcher.EndGroups
296
+ }
297
+
298
+ if !userBot.IsActive {
299
+ ctx.Reply(update, "❌ Your bot is already disabled!", nil)
300
+ return dispatcher.EndGroups
301
+ }
302
+
303
+ err = userBotManager.DisableUserBot(context.Background(), userID)
304
+ if err != nil {
305
+ log.Error("Failed to disable user bot", zap.Error(err))
306
+ ctx.Reply(update, "❌ Failed to disable bot. Please try again later.", nil)
307
+ return dispatcher.EndGroups
308
+ }
309
+
310
+ ctx.Reply(update,
311
+ "✅ *Bot Disabled Successfully*\n\n"+
312
+ "Your personal bot has been disabled. You'll now use our shared bot service.\n\n"+
313
+ "Use `/enablebot` to re-enable it anytime.",
314
+ nil)
315
+ return dispatcher.EndGroups
316
+ }
internal/commands/start.go CHANGED
@@ -94,6 +94,19 @@ func startCallbackHandler(ctx *ext.Context, u *ext.Update) error {
94
  3. When you're done, send /done to get a single, organized list of all links.
95
  4. To abort at any time, send /cancel.
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  This bot is designed to be a simple and effective bridge between Telegram and the web.`
98
 
99
  ctx.Raw.MessagesSetBotCallbackAnswer(ctx, &tg.MessagesSetBotCallbackAnswerRequest{QueryID: queryID})
 
94
  3. When you're done, send /done to get a single, organized list of all links.
95
  4. To abort at any time, send /cancel.
96
 
97
+ 🤖 **Personal Bot Commands**:
98
+ • /addbot <token> - Add your own bot for better performance
99
+ • /mybot - Check your bot status
100
+ • /removebot - Remove your personal bot
101
+ • /enablebot - Enable your bot
102
+ • /disablebot - Disable your bot
103
+
104
+ 💡 **Benefits of Personal Bot**:
105
+ • No rate limits on uploads
106
+ • Faster file processing
107
+ • Better reliability
108
+ • Your own dedicated instance
109
+
110
  This bot is designed to be a simple and effective bridge between Telegram and the web.`
111
 
112
  ctx.Raw.MessagesSetBotCallbackAnswer(ctx, &tg.MessagesSetBotCallbackAnswerRequest{QueryID: queryID})
internal/db/database.go CHANGED
@@ -19,6 +19,8 @@ var (
19
  Files *mongo.Collection
20
  // User management instance
21
  UserMgr *UserManager
 
 
22
  )
23
 
24
  // InitDatabase initializes the MongoDB connection.
@@ -53,6 +55,14 @@ func InitDatabase(log *zap.Logger) error {
53
 
54
  // Initialize UserManager
55
  UserMgr = NewUserManager(database, log)
 
 
 
 
 
 
 
 
56
 
57
  log.Info("MongoDB connection established successfully.")
58
  return nil
 
19
  Files *mongo.Collection
20
  // User management instance
21
  UserMgr *UserManager
22
+ // User bot management instance
23
+ UserBotMgr *UserBotManager
24
  )
25
 
26
  // InitDatabase initializes the MongoDB connection.
 
55
 
56
  // Initialize UserManager
57
  UserMgr = NewUserManager(database, log)
58
+
59
+ // Initialize UserBotManager with encryption key from config
60
+ encryptionKey := config.ValueOf.BotEncryptionKey
61
+ if encryptionKey == "" {
62
+ encryptionKey = "default-encryption-key-change-me" // Fallback key
63
+ log.Warn("BotEncryptionKey not set in config, using default key")
64
+ }
65
+ UserBotMgr = NewUserBotManager(database, log, encryptionKey)
66
 
67
  log.Info("MongoDB connection established successfully.")
68
  return nil
internal/db/models.go CHANGED
@@ -114,6 +114,22 @@ type FileHistory struct {
114
  ExpiresAt *time.Time `bson:"expires_at,omitempty"` // optional expiration
115
  }
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  // UserRole constants
118
  const (
119
  RoleBasic = "basic"
 
114
  ExpiresAt *time.Time `bson:"expires_at,omitempty"` // optional expiration
115
  }
116
 
117
+ // UserBot represents a user's personal bot configuration
118
+ type UserBot struct {
119
+ ID primitive.ObjectID `bson:"_id,omitempty"`
120
+ UserID int64 `bson:"user_id,unique"`
121
+ BotToken string `bson:"bot_token"` // Encrypted bot token
122
+ BotUsername string `bson:"bot_username"` // Bot username for display
123
+ BotID int64 `bson:"bot_id"` // Bot's Telegram ID
124
+ IsActive bool `bson:"is_active"` // Whether bot is currently active
125
+ IsVerified bool `bson:"is_verified"` // Whether bot token was validated
126
+ LastUsed time.Time `bson:"last_used"` // Last time bot was used
127
+ ErrorCount int `bson:"error_count"` // Track consecutive errors
128
+ LastError string `bson:"last_error,omitempty"` // Last error message
129
+ CreatedAt time.Time `bson:"created_at"`
130
+ UpdatedAt time.Time `bson:"updated_at"`
131
+ }
132
+
133
  // UserRole constants
134
  const (
135
  RoleBasic = "basic"
internal/db/userbot_manager.go ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package db
2
+
3
+ import (
4
+ "context"
5
+ "crypto/aes"
6
+ "crypto/cipher"
7
+ "crypto/rand"
8
+ "encoding/base64"
9
+ "fmt"
10
+ "io"
11
+ "time"
12
+
13
+ "go.mongodb.org/mongo-driver/bson"
14
+ "go.mongodb.org/mongo-driver/mongo"
15
+ "go.mongodb.org/mongo-driver/mongo/options"
16
+ "go.uber.org/zap"
17
+ )
18
+
19
+ // UserBotManager handles user bot operations
20
+ type UserBotManager struct {
21
+ collection *mongo.Collection
22
+ log *zap.Logger
23
+ encryptionKey []byte // 32 bytes for AES-256
24
+ }
25
+
26
+ // NewUserBotManager creates a new UserBotManager instance
27
+ func NewUserBotManager(database *mongo.Database, log *zap.Logger, encryptionKey string) *UserBotManager {
28
+ // Ensure encryption key is 32 bytes for AES-256
29
+ key := make([]byte, 32)
30
+ copy(key, []byte(encryptionKey))
31
+
32
+ return &UserBotManager{
33
+ collection: database.Collection("user_bots"),
34
+ log: log.Named("UserBotManager"),
35
+ encryptionKey: key,
36
+ }
37
+ }
38
+
39
+ // encryptToken encrypts a bot token using AES-256-GCM
40
+ func (ubm *UserBotManager) encryptToken(token string) (string, error) {
41
+ block, err := aes.NewCipher(ubm.encryptionKey)
42
+ if err != nil {
43
+ return "", err
44
+ }
45
+
46
+ gcm, err := cipher.NewGCM(block)
47
+ if err != nil {
48
+ return "", err
49
+ }
50
+
51
+ nonce := make([]byte, gcm.NonceSize())
52
+ if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
53
+ return "", err
54
+ }
55
+
56
+ ciphertext := gcm.Seal(nonce, nonce, []byte(token), nil)
57
+ return base64.StdEncoding.EncodeToString(ciphertext), nil
58
+ }
59
+
60
+ // decryptToken decrypts a bot token
61
+ func (ubm *UserBotManager) decryptToken(encryptedToken string) (string, error) {
62
+ data, err := base64.StdEncoding.DecodeString(encryptedToken)
63
+ if err != nil {
64
+ return "", err
65
+ }
66
+
67
+ block, err := aes.NewCipher(ubm.encryptionKey)
68
+ if err != nil {
69
+ return "", err
70
+ }
71
+
72
+ gcm, err := cipher.NewGCM(block)
73
+ if err != nil {
74
+ return "", err
75
+ }
76
+
77
+ nonceSize := gcm.NonceSize()
78
+ if len(data) < nonceSize {
79
+ return "", fmt.Errorf("ciphertext too short")
80
+ }
81
+
82
+ nonce, ciphertext := data[:nonceSize], data[nonceSize:]
83
+ plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
84
+ if err != nil {
85
+ return "", err
86
+ }
87
+
88
+ return string(plaintext), nil
89
+ }
90
+
91
+ // AddUserBot adds or updates a user's bot configuration
92
+ func (ubm *UserBotManager) AddUserBot(ctx context.Context, userID int64, botToken, botUsername string, botID int64) error {
93
+ // Encrypt the bot token
94
+ encryptedToken, err := ubm.encryptToken(botToken)
95
+ if err != nil {
96
+ return fmt.Errorf("failed to encrypt bot token: %w", err)
97
+ }
98
+
99
+ userBot := UserBot{
100
+ UserID: userID,
101
+ BotToken: encryptedToken,
102
+ BotUsername: botUsername,
103
+ BotID: botID,
104
+ IsActive: true,
105
+ IsVerified: true, // Assume verified if we got here
106
+ LastUsed: time.Now(),
107
+ ErrorCount: 0,
108
+ CreatedAt: time.Now(),
109
+ UpdatedAt: time.Now(),
110
+ }
111
+
112
+ // Upsert the user bot (replace if exists)
113
+ opts := options.Replace().SetUpsert(true)
114
+ _, err = ubm.collection.ReplaceOne(
115
+ ctx,
116
+ bson.M{"user_id": userID},
117
+ userBot,
118
+ opts,
119
+ )
120
+
121
+ if err != nil {
122
+ return fmt.Errorf("failed to add user bot: %w", err)
123
+ }
124
+
125
+ ubm.log.Info("Added user bot",
126
+ zap.Int64("userID", userID),
127
+ zap.String("botUsername", botUsername),
128
+ zap.Int64("botID", botID))
129
+
130
+ return nil
131
+ }
132
+
133
+ // GetUserBot retrieves a user's bot configuration
134
+ func (ubm *UserBotManager) GetUserBot(ctx context.Context, userID int64) (*UserBot, error) {
135
+ var userBot UserBot
136
+
137
+ err := ubm.collection.FindOne(ctx, bson.M{"user_id": userID}).Decode(&userBot)
138
+ if err != nil {
139
+ if err == mongo.ErrNoDocuments {
140
+ return nil, nil // No bot configured
141
+ }
142
+ return nil, fmt.Errorf("failed to get user bot: %w", err)
143
+ }
144
+
145
+ return &userBot, nil
146
+ }
147
+
148
+ // GetDecryptedBotToken retrieves and decrypts a user's bot token
149
+ func (ubm *UserBotManager) GetDecryptedBotToken(ctx context.Context, userID int64) (string, error) {
150
+ userBot, err := ubm.GetUserBot(ctx, userID)
151
+ if err != nil {
152
+ return "", err
153
+ }
154
+
155
+ if userBot == nil {
156
+ return "", fmt.Errorf("no bot configured for user")
157
+ }
158
+
159
+ if !userBot.IsActive {
160
+ return "", fmt.Errorf("user bot is disabled")
161
+ }
162
+
163
+ token, err := ubm.decryptToken(userBot.BotToken)
164
+ if err != nil {
165
+ return "", fmt.Errorf("failed to decrypt bot token: %w", err)
166
+ }
167
+
168
+ return token, nil
169
+ }
170
+
171
+ // UpdateBotUsage updates the last used time and resets error count on successful use
172
+ func (ubm *UserBotManager) UpdateBotUsage(ctx context.Context, userID int64) error {
173
+ update := bson.M{
174
+ "$set": bson.M{
175
+ "last_used": time.Now(),
176
+ "updated_at": time.Now(),
177
+ "error_count": 0,
178
+ "last_error": "",
179
+ },
180
+ }
181
+
182
+ _, err := ubm.collection.UpdateOne(
183
+ ctx,
184
+ bson.M{"user_id": userID},
185
+ update,
186
+ )
187
+
188
+ return err
189
+ }
190
+
191
+ // RecordBotError records an error for a user's bot
192
+ func (ubm *UserBotManager) RecordBotError(ctx context.Context, userID int64, errorMsg string) error {
193
+ update := bson.M{
194
+ "$inc": bson.M{"error_count": 1},
195
+ "$set": bson.M{
196
+ "last_error": errorMsg,
197
+ "updated_at": time.Now(),
198
+ },
199
+ }
200
+
201
+ // Disable bot if too many consecutive errors
202
+ result, err := ubm.collection.UpdateOne(
203
+ ctx,
204
+ bson.M{"user_id": userID},
205
+ update,
206
+ )
207
+
208
+ if err != nil {
209
+ return err
210
+ }
211
+
212
+ // Check if we need to disable the bot due to too many errors
213
+ if result.ModifiedCount > 0 {
214
+ userBot, err := ubm.GetUserBot(ctx, userID)
215
+ if err == nil && userBot != nil && userBot.ErrorCount >= 5 {
216
+ // Disable bot after 5 consecutive errors
217
+ ubm.DisableUserBot(ctx, userID)
218
+ ubm.log.Warn("Disabled user bot due to too many errors",
219
+ zap.Int64("userID", userID),
220
+ zap.String("lastError", errorMsg))
221
+ }
222
+ }
223
+
224
+ return nil
225
+ }
226
+
227
+ // DisableUserBot disables a user's bot
228
+ func (ubm *UserBotManager) DisableUserBot(ctx context.Context, userID int64) error {
229
+ update := bson.M{
230
+ "$set": bson.M{
231
+ "is_active": false,
232
+ "updated_at": time.Now(),
233
+ },
234
+ }
235
+
236
+ _, err := ubm.collection.UpdateOne(
237
+ ctx,
238
+ bson.M{"user_id": userID},
239
+ update,
240
+ )
241
+
242
+ return err
243
+ }
244
+
245
+ // EnableUserBot enables a user's bot
246
+ func (ubm *UserBotManager) EnableUserBot(ctx context.Context, userID int64) error {
247
+ update := bson.M{
248
+ "$set": bson.M{
249
+ "is_active": true,
250
+ "error_count": 0,
251
+ "last_error": "",
252
+ "updated_at": time.Now(),
253
+ },
254
+ }
255
+
256
+ _, err := ubm.collection.UpdateOne(
257
+ ctx,
258
+ bson.M{"user_id": userID},
259
+ update,
260
+ )
261
+
262
+ return err
263
+ }
264
+
265
+ // RemoveUserBot removes a user's bot configuration
266
+ func (ubm *UserBotManager) RemoveUserBot(ctx context.Context, userID int64) error {
267
+ _, err := ubm.collection.DeleteOne(ctx, bson.M{"user_id": userID})
268
+ if err != nil {
269
+ return fmt.Errorf("failed to remove user bot: %w", err)
270
+ }
271
+
272
+ ubm.log.Info("Removed user bot", zap.Int64("userID", userID))
273
+ return nil
274
+ }
275
+
276
+ // ListActiveBots returns all active user bots (for admin purposes)
277
+ func (ubm *UserBotManager) ListActiveBots(ctx context.Context) ([]UserBot, error) {
278
+ cursor, err := ubm.collection.Find(ctx, bson.M{"is_active": true})
279
+ if err != nil {
280
+ return nil, err
281
+ }
282
+ defer cursor.Close(ctx)
283
+
284
+ var bots []UserBot
285
+ if err := cursor.All(ctx, &bots); err != nil {
286
+ return nil, err
287
+ }
288
+
289
+ return bots, nil
290
+ }
test_validator.go ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "TelegramCloud/tgf/internal/bot"
5
+ "TelegramCloud/tgf/internal/utils"
6
+ "fmt"
7
+ )
8
+
9
+ func main() {
10
+ // Initialize logger
11
+ utils.InitLogger(true)
12
+ log := utils.Logger
13
+
14
+ // Test bot validator
15
+ validator := bot.NewBotValidator(log)
16
+
17
+ // Test cases
18
+ testTokens := []string{
19
+ "invalid_token",
20
+ "123:short",
21
+ "not_a_number:ABCdef123456789",
22
+ "123456789:ABCdef123456789012345678901234567890", // Valid format but fake
23
+ }
24
+
25
+ for _, token := range testTokens {
26
+ fmt.Printf("\nTesting token: %s\n", token)
27
+
28
+ botInfo, err := validator.ValidateBotToken(token)
29
+ if err != nil {
30
+ fmt.Printf("❌ Error: %v\n", err)
31
+ } else {
32
+ fmt.Printf("✅ Valid bot: @%s (ID: %d)\n", botInfo.Username, botInfo.ID)
33
+ }
34
+ }
35
+ }