frist
This commit is contained in:
365
internal/telegram/commands.go
Normal file
365
internal/telegram/commands.go
Normal 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})
|
||||
}
|
||||
Reference in New Issue
Block a user