366 lines
8.3 KiB
Go
366 lines
8.3 KiB
Go
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})
|
||
}
|