package service
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"golang.org/x/crypto/bcrypt"
"github.com/ShukeBta/MediaStationGo/internal/model"
)
// cmdStart 处理 /start 命令。
func (s *TelegramBotService) cmdStart(ctx context.Context, msg *TelegramMessage, args []string) telegramCommandReply {
name := msg.From.FirstName
if msg.From.Username != "" {
name = "@" + msg.From.Username
}
if telegramIsGroupChat(msg.Chat.Type) && len(args) > 0 {
return telegramCommandReply{Text: telegramGroupPrivateUserHint("绑定账号")}
}
if len(args) == 0 {
if binding := s.telegramBinding(ctx, msg.From.ID); binding != nil {
user, _ := s.repo.User.FindByID(ctx, binding.UserID)
if user == nil {
_ = s.repo.DB.WithContext(ctx).Unscoped().Delete(&model.TelegramBinding{}, "id = ?", binding.ID).Error
return telegramCommandReply{Text: "之前绑定的媒体中心账号已不存在,请重新绑定:\n/start 用户名 密码"}
}
status := "未隐藏"
if user.HideAdult {
status = "已隐藏"
}
return telegramCommandReply{
Text: fmt.Sprintf("MediaStationGo 已绑定\n\n你好 %s,当前账号:%s\n成人目录:%s", name, userNameOrFallback(user), status),
Buttons: [][]telegramInlineButton{{{
Text: map[bool]string{true: "显示成人目录", false: "隐藏成人目录"}[user.HideAdult],
Data: "adult_toggle",
}}},
}
}
hint := "如果没有账号,请联系管理员注册。"
if s.openRegEnabled(ctx) {
hint = "如果还没有账号,可直接注册:\n/register 用户名 密码\n或:/register 用户名-密码"
}
return telegramCommandReply{Text: "欢迎使用 MediaStationGo\n\n普通用户请先绑定账号:\n/start 用户名 密码\n或:/start 用户名-密码\n\n" + hint}
}
channel := s.findChannelForMessage(ctx, msg)
if dec := s.telegramUserBindDecision(ctx, channel, msg.From.ID); dec != bindAllowed {
return telegramCommandReply{Text: telegramBindRejectText(dec, "绑定媒体中心账号")}
}
username, password := parseStartCredentials(args)
if username == "" || password == "" {
return telegramCommandReply{Text: "绑定格式不正确,请使用:\n/start 用户名 密码\n或:/start 用户名-密码"}
}
existingBinding := s.telegramBinding(ctx, msg.From.ID)
user, err := s.repo.User.FindByUsername(ctx, username)
if err != nil || user == nil {
if existingBinding != nil {
_ = s.unbindTelegramUser(ctx, msg.From.ID)
return telegramCommandReply{Text: "当前绑定的媒体账号信息已失效,已自动解绑。请使用新的用户名和密码重新绑定。"}
}
return telegramCommandReply{Text: "未找到此用户,请联系管理员注册。"}
}
if !user.IsActive {
return telegramCommandReply{Text: "此账号已被禁用,请联系管理员。"}
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
if existingBinding != nil && existingBinding.UserID == user.ID {
_ = s.unbindTelegramUser(ctx, msg.From.ID)
return telegramCommandReply{Text: "当前绑定账号的密码已失效,已自动解绑。请使用新密码重新绑定。"}
}
return telegramCommandReply{Text: "账号或密码错误。"}
}
if err := s.upsertTelegramBinding(ctx, msg, user.ID); err != nil {
return telegramCommandReply{Text: "绑定失败:" + err.Error()}
}
return telegramCommandReply{
Text: fmt.Sprintf("绑定成功:%s\n\n普通用户只能使用此 Bot 管理自己的成人目录隐藏状态;系统状态、搜索、下载和统计命令仅管理员可用。", user.Username),
Buttons: [][]telegramInlineButton{{{
Text: map[bool]string{true: "显示成人目录", false: "隐藏成人目录"}[user.HideAdult],
Data: "adult_toggle",
}}},
}
}
// cmdRegister 处理 /register 命令:在管理员开启注册后,普通用户可通过 Bot
// 注册一个新的媒体中心账号,并自动绑定到当前 Telegram 账号。
func (s *TelegramBotService) cmdRegister(ctx context.Context, channel *model.NotifyChannel, msg *TelegramMessage, args []string) telegramCommandReply {
if len(args) == 1 && looksLikeRedemptionCode(args[0]) {
return s.redeemRegisterFlow(ctx, channel, msg, args[0])
}
if !s.openRegEnabled(ctx) {
return telegramCommandReply{Text: "注册功能未开放,请联系管理员开启后再试。"}
}
// 开注名额已用尽则拦截(容量随凭证授权实时变化,名额单独计数)。
if c := s.loadCapacity(ctx); c.Remaining() <= 0 {
return telegramCommandReply{Text: "注册名额已满,请等待管理员重新开放或扩容授权。"}
}
if s.auth == nil {
return telegramCommandReply{Text: "注册功能暂不可用,请联系管理员。"}
}
if channel == nil {
channel = s.findChannelForMessage(ctx, msg)
}
if dec := s.telegramUserBindDecision(ctx, channel, msg.From.ID); dec != bindAllowed {
return telegramCommandReply{Text: telegramBindRejectText(dec, "注册账号")}
}
if binding := s.telegramBinding(ctx, msg.From.ID); binding != nil {
if user, _ := s.repo.User.FindByID(ctx, binding.UserID); user != nil {
return telegramCommandReply{Text: fmt.Sprintf("当前 Telegram 已绑定账号:%s,无需重复注册。\n如需切换账号请使用 /start 用户名 密码。", userNameOrFallback(user))}
}
}
username, password := parseStartCredentials(args)
if username == "" || password == "" {
return telegramCommandReply{Text: "注册格式不正确,请使用:\n/register 用户名 密码\n或:/register 用户名-密码"}
}
user, _, err := s.auth.Register(ctx, username, password)
if err != nil {
switch {
case errors.Is(err, ErrUsernameTaken):
return telegramCommandReply{Text: "该用户名已被占用,请换一个;如果是你本人的账号,请改用 /start 用户名 密码 绑定。"}
case errors.Is(err, ErrUserLimitReached):
return telegramCommandReply{Text: "注册失败:已达到用户数量上限,请联系管理员。"}
default:
return telegramCommandReply{Text: "注册失败:" + err.Error()}
}
}
// 注册成功,扣减一个开注名额(名额用尽自动关闭注册)。
s.consumeOpenRegSlot(ctx)
if err := s.upsertTelegramBinding(ctx, msg, user.ID); err != nil {
return telegramCommandReply{Text: fmt.Sprintf("账号 %s 注册成功,但自动绑定失败:%s\n请稍后使用 /start %s 密码 重新绑定。", user.Username, err.Error(), user.Username)}
}
return telegramCommandReply{
Text: fmt.Sprintf("注册并绑定成功:%s\n\n你现在可以用此账号登录网页与第三方客户端。普通用户只能在此 Bot 管理成人目录显隐;其他功能仅管理员可用。", user.Username),
Buttons: [][]telegramInlineButton{{{
Text: map[bool]string{true: "显示成人目录", false: "隐藏成人目录"}[user.HideAdult],
Data: "adult_toggle",
}}},
}
}
// cmdRegistrationToggle handles /registration and /openreg. It uses the same
// quota-aware open-registration state as the inline Bot menu.
func (s *TelegramBotService) cmdRegistrationToggle(ctx context.Context, args []string) telegramCommandReply {
if len(args) == 0 || strings.EqualFold(strings.TrimSpace(args[0]), "status") {
c := s.loadCapacity(ctx)
state := "已关闭"
if c.OpenRegOn {
if c.OpenRegLimit > 0 {
state = fmt.Sprintf("已开启(%d/%d 名额)", c.OpenRegUsed, c.OpenRegLimit)
} else {
state = "已开启(不限名额,受授权上限约束)"
}
}
return telegramCommandReply{Text: fmt.Sprintf("普通用户 Bot 注册功能当前%s。\n剩余可注册:%d 人。\n\n开启:/registration on 10\n不限:/registration on 0\n关闭:/registration off", state, c.Remaining())}
}
switch strings.ToLower(strings.TrimSpace(args[0])) {
case "on", "true", "1", "open", "enable", "enabled", "开启", "打开", "开":
limit := 0
if len(args) > 1 {
n, err := strconv.Atoi(strings.TrimSpace(args[1]))
if err != nil || n < 0 {
return telegramCommandReply{Text: "名额必须是非负整数,0 表示不限名额。"}
}
limit = n
}
if err := s.openRegistration(ctx, limit); err != nil {
return telegramCommandReply{Text: "开启失败:" + err.Error()}
}
label := "不限名额"
if limit > 0 {
label = fmt.Sprintf("%d 个名额", limit)
}
return telegramCommandReply{Text: "普通用户 Bot 注册功能已开启:" + label + "。"}
case "off", "false", "0", "close", "disable", "disabled", "关闭", "关":
if err := s.closeRegistration(ctx); err != nil {
return telegramCommandReply{Text: "关闭失败:" + err.Error()}
}
return telegramCommandReply{Text: "普通用户 Bot 注册功能已关闭。"}
default:
return telegramCommandReply{Text: "参数无效,请使用 /registration on [名额] 或 /registration off。"}
}
}
// cmdHelp 处理 /help 命令。
func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage) string {
channel := s.findChannelForMessage(ctx, msg)
if telegramIsGroupChat(msg.Chat.Type) {
adminHint := ""
if s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
adminHint = "\n\n管理员可在已绑定群组直接发送文本管理命令;管理面板和账号凭据操作请私聊 Bot。"
}
return "MediaStationGo 群组可用命令\n\n" +
"/menu — 打开群组自助菜单\n" +
"/account — 查看账号状态\n" +
"/signin — 签到\n" +
"/devices — 查看登录设备\n" +
"/kick all|编号 — 踢下线设备\n" +
"/hideadult on|off — 隐藏或显示成人目录\n\n" +
"绑定、注册、兑换、改名、改密等包含敏感信息的操作请私聊 Bot。" +
adminHint
}
if !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
register := ""
if s.openRegEnabled(ctx) {
register = "/register 用户名 密码 — 注册新账号\n"
}
return "MediaStationGo 用户命令\n\n" +
register +
"/start 用户名 密码 — 绑定账号\n" +
"/account — 查看账号状态\n" +
"/signin — 签到\n" +
"/devices — 查看登录设备\n" +
"/kick all|编号 — 踢下线设备\n" +
"/setname 当前密码 新用户名 — 修改用户名\n" +
"/setpass 当前密码 新密码 — 修改密码\n" +
"/redeem 兑换码 — 注册或续期兑换\n" +
"/hideadult on|off — 隐藏或显示成人目录\n\n" +
"系统状态、搜索、下载列表与统计命令仅管理员可用。"
}
return "MediaStationGo 命令列表\n\n" +
"/start — 开始使用\n" +
"/help — 帮助信息\n" +
"/account / /devices / /kick all|编号 — 用户自助设备管理\n" +
"/signin / /redeem 兑换码 — 签到与兑换\n" +
"/setname 当前密码 新用户名 / /setpass 当前密码 新密码 — 用户自助改名改密\n" +
"/register 用户名 密码 — 注册新账号(需管理员开启)\n" +
"/registration on [名额]|off — 开启/关闭普通用户注册(管理员)\n" +
"/capacity / /users — 容量与用户管理(管理员)\n" +
"/gencode register|renew 天数 [有效天数] — 生成兑换码(管理员)\n" +
"/renew_user 用户名 天数 / /delete_user 用户名 confirm — 续期/删除用户(管理员)\n" +
"/unbind 用户1 用户2 — 批量解绑 Telegram 绑定(管理员)\n" +
"/unbind_duplicates / /unbind_inactive 天数 — 清理重复/无效绑定或久未登录绑定(管理员)\n" +
"/antishare on play=3 login=3 warn=2 — 防共享策略(管理员)\n" +
"/cleanup run — 预览保号清理候选(管理员)\n" +
"/cleanup run confirm — 确认清理候选账号(管理员)\n" +
"/cleanup on|off — 保号规则开关(管理员)\n" +
"/cleanup_rule list|add|edit|修改|del|enable|disable — Mgo 保号规则(管理员)\n" +
"/ban 用户名 / /unban 用户名 — 禁用/解禁用户(管理员)\n" +
"/hideadult on|off — 隐藏/显示当前绑定账号的成人目录\n" +
"/status — 系统运行状态\n" +
"/search 关键词 — 搜索媒体库\n" +
"/downloads — 下载列表\n" +
"/stats — 媒体库统计\n\n" +
telegramMgoAdminCommandHelp() + "\n\n" +
"自动推送事件:\n" +
"• 订阅命中新资源\n" +
"• 下载任务完成\n" +
"• 刮削失败告警\n" +
"• 系统异常通知"
}
func telegramMgoAdminCommandHelp() string {
return "Mgo 管理命令(管理员可用,已注册到命令栏):\n" +
"用户:/ucr 用户名 密码 [天数] 创建账号;/uinfo 用户名 查询账号;/rmemby 用户名 confirm 删除账号;/only_rm_record tg:ID|用户名 仅删 Bot 绑定;/renewall 天数 confirm 批量续期。\n" +
"审计:/userip 用户名 查用户 IP;/auditip IP 按 IP 审计;/auditdevice 关键词 按终端设备审计;/auditclient 关键词 按客户端审计;/udeviceid 设备ID 按设备指纹审计。\n" +
"清理:/syncunbound 检查未绑定账号;/syncgroupm 校验群成员;/check_ex 检查过期账号;/deleted 按保号规则预览清理候选。\n" +
"权限:/embyadmin 用户名 on|off 设置管理员;/banall confirm//unbanall confirm 批量禁用/解禁;/prouser 用户名//revuser 用户名 管理保护名单;/embylibs_blockall//embylibs_unblockall 批量禁用/开放媒体库权限。\n" +
"运维:/proadmin TelegramID//revadmin TelegramID 管理 Bot 管理员;/backup_db 备份数据库;/restore_from_db 文件名 confirm 恢复数据库。\n" +
"说明:重复别名如 /low_activity、/urm 仍可兼容识别,但不显示在命令栏。"
}
// cmdStatus 处理 /status 命令。
func (s *TelegramBotService) cmdHideAdult(ctx context.Context, msg *TelegramMessage, args []string) telegramCommandReply {
channel := s.findChannelForMessage(ctx, msg)
if dec := s.telegramUserBindDecision(ctx, channel, msg.From.ID); dec != bindAllowed {
return telegramCommandReply{Text: telegramBindRejectText(dec, "使用成人目录隐藏开关")}
}
binding := s.telegramBinding(ctx, msg.From.ID)
if binding == nil {
return telegramCommandReply{Text: "请先绑定账号:/start 用户名 密码"}
}
user, err := s.repo.User.FindByID(ctx, binding.UserID)
if err != nil || user == nil {
return telegramCommandReply{Text: "绑定用户不存在,请重新 /start 绑定。"}
}
next := true
if len(args) > 0 {
switch strings.ToLower(strings.TrimSpace(args[0])) {
case "off", "false", "0", "show", "显示", "关闭":
next = false
case "on", "true", "1", "hide", "隐藏", "开启":
next = true
default:
next = !user.HideAdult
}
} else {
next = !user.HideAdult
}
if err := s.repo.User.UpdateFields(ctx, user.ID, map[string]any{"hide_adult": next}); err != nil {
return telegramCommandReply{Text: "更新失败:" + err.Error()}
}
status := map[bool]string{true: "已隐藏", false: "已显示"}[next]
return telegramCommandReply{
Text: "成人目录" + status + "。此设置会同步影响网页与第三方客户端。",
Buttons: [][]telegramInlineButton{{{
Text: map[bool]string{true: "显示成人目录", false: "隐藏成人目录"}[next],
Data: "adult_toggle",
}}},
}
}