Files
ProxyPool/internal/telegram/commands.go
2026-01-31 22:53:12 +08:00

366 lines
8.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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})
}