This commit is contained in:
dela
2026-01-31 22:53:12 +08:00
commit bc639cf460
30 changed files with 6836 additions and 0 deletions

View File

@@ -0,0 +1,365 @@
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})
}