package telegram import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" "proxyrotator/internal/importer" "proxyrotator/internal/model" "proxyrotator/internal/store" tele "gopkg.in/telebot.v3" ) // Commands 命令处理器 type Commands struct { store store.ProxyStore scheduler *Scheduler importer *importer.Importer // 导入状态 importState map[int64]*importSession } type importSession struct { Group string Tags []string } // NewCommands 创建命令处理器 func NewCommands(store store.ProxyStore, scheduler *Scheduler) *Commands { return &Commands{ store: store, scheduler: scheduler, importer: importer.NewImporter(), importState: make(map[int64]*importSession), } } // HandleStart /start 命令 func (c *Commands) HandleStart(ctx tele.Context) error { return ctx.Send(`🚀 *ProxyRotator Bot* 欢迎使用代理池管理机器人! 使用 /help 查看可用命令`, &tele.SendOptions{ParseMode: tele.ModeMarkdown}) } // HandleHelp /help 命令 func (c *Commands) HandleHelp(ctx tele.Context) error { help := `📖 *可用命令* *查询类* /stats - 代理池统计(总数/存活/死亡/未知) /groups - 分组统计 /get [n] - 获取 n 个可用代理(默认 5) *操作类* /import [group] - 导入代理(之后发送文本或文件) /test [group] - 触发测活 /purge - 清理死代理 *其他* /help - 显示帮助信息` return ctx.Send(help, &tele.SendOptions{ParseMode: tele.ModeMarkdown}) } // HandleStats /stats 命令 func (c *Commands) HandleStats(ctx tele.Context) error { stats, err := c.store.GetStats(context.Background()) if err != nil { return ctx.Send(fmt.Sprintf("❌ 获取统计失败: %v", err)) } alive := stats.ByStatus[model.StatusAlive] dead := stats.ByStatus[model.StatusDead] unknown := stats.ByStatus[model.StatusUnknown] var alivePercent float64 if stats.Total > 0 { alivePercent = float64(alive) / float64(stats.Total) * 100 } msg := fmt.Sprintf(`📊 *代理池统计* *总数:* %d *存活:* %d (%.1f%%) *死亡:* %d *未知:* %d *禁用:* %d *平均延迟:* %d ms *平均分数:* %.1f`, stats.Total, alive, alivePercent, dead, unknown, stats.Disabled, stats.AvgLatencyMs, stats.AvgScore, ) return ctx.Send(msg, &tele.SendOptions{ParseMode: tele.ModeMarkdown}) } // HandleGroups /groups 命令 func (c *Commands) HandleGroups(ctx tele.Context) error { stats, err := c.store.GetStats(context.Background()) if err != nil { return ctx.Send(fmt.Sprintf("❌ 获取统计失败: %v", err)) } if len(stats.ByGroup) == 0 { return ctx.Send("📁 暂无分组数据") } var sb strings.Builder sb.WriteString("📁 *分组统计*\n\n") for group, count := range stats.ByGroup { sb.WriteString(fmt.Sprintf("• `%s`: %d\n", group, count)) } return ctx.Send(sb.String(), &tele.SendOptions{ParseMode: tele.ModeMarkdown}) } // IPInfo ipinfo.io 返回结构 type IPInfo struct { IP string `json:"ip"` City string `json:"city"` Region string `json:"region"` Country string `json:"country"` Org string `json:"org"` } // HandleGet /get [n] 命令 func (c *Commands) HandleGet(ctx tele.Context) error { n := 1 args := ctx.Args() if len(args) > 0 { if parsed, err := strconv.Atoi(args[0]); err == nil && parsed > 0 { n = parsed } } if n > 20 { n = 20 } proxies, err := c.store.List(context.Background(), model.ProxyQuery{ StatusIn: []model.ProxyStatus{model.StatusAlive}, OnlyEnabled: true, OrderBy: "random", Limit: n, }) if err != nil { return ctx.Send(fmt.Sprintf("❌ 获取代理失败: %v", err)) } if len(proxies) == 0 { return ctx.Send("😢 没有可用代理") } var sb strings.Builder sb.WriteString(fmt.Sprintf("🔗 *可用代理 (%d)*\n\n", len(proxies))) for _, p := range proxies { var proxyURL string if p.Username != "" { proxyURL = fmt.Sprintf("%s://%s:%s@%s:%d", p.Protocol, p.Username, p.Password, p.Host, p.Port) } else { proxyURL = fmt.Sprintf("%s://%s:%d", p.Protocol, p.Host, p.Port) } // 获取 IP 位置信息 ipInfo := fetchIPInfo(proxyURL) sb.WriteString(fmt.Sprintf("`%s`\n", proxyURL)) if ipInfo != nil { location := fmt.Sprintf("%s, %s, %s", ipInfo.City, ipInfo.Region, ipInfo.Country) sb.WriteString(fmt.Sprintf(" 📍 %s | %s\n", location, ipInfo.Org)) } else { sb.WriteString(" 📍 位置获取失败\n") } sb.WriteString("\n") } return ctx.Send(sb.String(), &tele.SendOptions{ParseMode: tele.ModeMarkdown}) } // fetchIPInfo 通过代理获取 IP 信息 func fetchIPInfo(proxyURL string) *IPInfo { proxy, err := url.Parse(proxyURL) if err != nil { return nil } client := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(proxy), }, Timeout: 10 * time.Second, } resp, err := client.Get("https://ipinfo.io/json") if err != nil { return nil } defer resp.Body.Close() var info IPInfo if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return nil } return &info } // HandleTest /test [group] 命令 func (c *Commands) HandleTest(ctx tele.Context) error { if c.scheduler == nil { return ctx.Send("❌ 调度器未初始化") } group := "" args := ctx.Args() if len(args) > 0 { group = args[0] } _ = ctx.Send("🔄 正在执行测活...") err := c.scheduler.RunTestWithGroup(context.Background(), group) if err != nil { return ctx.Send(fmt.Sprintf("❌ 测活失败: %v", err)) } return ctx.Send("✅ 测活完成") } // HandlePurge /purge 命令 func (c *Commands) HandlePurge(ctx tele.Context) error { deleted, err := c.store.DeleteMany(context.Background(), model.BulkDeleteRequest{ Status: model.StatusDead, }) if err != nil { return ctx.Send(fmt.Sprintf("❌ 清理失败: %v", err)) } return ctx.Send(fmt.Sprintf("🗑️ 已清理 %d 个死代理", deleted)) } // HandleImport /import [group] 命令 func (c *Commands) HandleImport(ctx tele.Context) error { group := "default" args := ctx.Args() if len(args) > 0 { group = args[0] } userID := ctx.Sender().ID c.importState[userID] = &importSession{ Group: group, Tags: []string{"telegram"}, } return ctx.Send(fmt.Sprintf(`📥 *导入模式已开启* 分组: `+"`%s`"+` 请发送代理列表(文本或文件),支持格式: • host:port • host:port:user:pass • protocol://host:port • protocol://user:pass@host:port 发送 /cancel 取消导入`, group), &tele.SendOptions{ParseMode: tele.ModeMarkdown}) } // HandleDocument 处理文件上传 func (c *Commands) HandleDocument(ctx tele.Context) error { userID := ctx.Sender().ID session, ok := c.importState[userID] if !ok { return nil // 不在导入模式,忽略 } doc := ctx.Message().Document if doc == nil { return nil } // 下载文件 reader, err := ctx.Bot().File(&doc.File) if err != nil { return ctx.Send(fmt.Sprintf("❌ 获取文件失败: %v", err)) } defer reader.Close() content, err := io.ReadAll(reader) if err != nil { return ctx.Send(fmt.Sprintf("❌ 读取文件失败: %v", err)) } return c.doImport(ctx, session, string(content)) } // HandleText 处理文本消息(用于导入) func (c *Commands) HandleText(ctx tele.Context) error { userID := ctx.Sender().ID session, ok := c.importState[userID] if !ok { return nil // 不在导入模式,忽略 } text := ctx.Text() if text == "/cancel" { delete(c.importState, userID) return ctx.Send("❌ 已取消导入") } // 检查是否像代理格式 if !strings.Contains(text, ":") { return nil // 不像代理,忽略 } return c.doImport(ctx, session, text) } // doImport 执行导入 func (c *Commands) doImport(ctx tele.Context, session *importSession, text string) error { userID := ctx.Sender().ID defer delete(c.importState, userID) input := model.ImportInput{ Group: session.Group, Tags: session.Tags, } proxies, invalid := c.importer.ParseText(context.Background(), input, text) if len(proxies) == 0 { return ctx.Send(fmt.Sprintf("❌ 未解析到有效代理\n无效行: %d", len(invalid))) } imported, duplicated, err := c.store.UpsertMany(context.Background(), proxies) if err != nil { return ctx.Send(fmt.Sprintf("❌ 导入失败: %v", err)) } msg := fmt.Sprintf(`✅ *导入完成* • 新增: %d • 重复: %d • 无效: %d • 分组: `+"`%s`", imported, duplicated, len(invalid), session.Group) return ctx.Send(msg, &tele.SendOptions{ParseMode: tele.ModeMarkdown}) }