mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-10 23:36:37 +08:00
Adapt Sakura-style bot management commands
This commit is contained in:
@@ -74,6 +74,58 @@ func (s *TelegramBotService) telegramCommandDefinitions(ctx context.Context, cha
|
|||||||
{Aliases: []string{"/search"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSearch(ctx, args) }},
|
{Aliases: []string{"/search"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSearch(ctx, args) }},
|
||||||
{Aliases: []string{"/downloads"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdDownloads(ctx) }},
|
{Aliases: []string{"/downloads"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdDownloads(ctx) }},
|
||||||
{Aliases: []string{"/stats"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdStats(ctx) }},
|
{Aliases: []string{"/stats"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdStats(ctx) }},
|
||||||
|
{Aliases: []string{"/renew"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdUserRenew(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/ucr"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraCreateUser(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/uinfo"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraUserInfo(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/rmemby", "/urm", "/only_rm_emby"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraDeleteUser(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/only_rm_record"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraOnlyRemoveRecord(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/userip"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraUserIP(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/udeviceid"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraAuditDevices(ctx, "udeviceid", args), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/auditip"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraAuditDevices(ctx, "auditip", args), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/auditdevice"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraAuditDevices(ctx, "auditdevice", args), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/auditclient"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraAuditDevices(ctx, "auditclient", args), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/renewall"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraRenewAll(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/callall"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraCallAll(ctx, channel, args), nil }},
|
||||||
|
{Aliases: []string{"/syncunbound"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraSyncUnbound(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/syncgroupm", "/kick_not_emby"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraUnsupported("群成员全量同步", "<code>/syncunbound</code> 检查未绑定 Bot 的站内账号"), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/scan_embyname"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraScanNames(ctx), nil }},
|
||||||
|
{Aliases: []string{"/check_ex"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraCheckExpired(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/deleted", "/low_activity"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdCleanup(ctx, []string{"run"}), nil }},
|
||||||
|
{Aliases: []string{"/uranks"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraRanks(ctx, 0, true), nil }},
|
||||||
|
{Aliases: []string{"/days_ranks"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraRanks(ctx, 24*time.Hour, false), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/week_ranks"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraRanks(ctx, 7*24*time.Hour, false), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/embyadmin"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraAdminRole(ctx, args), nil }},
|
||||||
|
{Aliases: []string{"/unbanall"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraBanAll(ctx, true, args), nil }},
|
||||||
|
{Aliases: []string{"/banall"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraBanAll(ctx, false, args), nil }},
|
||||||
|
{Aliases: []string{"/embylibs_unblockall", "/extraembylibs_unblockall"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraMediaAccessAll(ctx, true), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/embylibs_blockall", "/extraembylibs_blockall"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraMediaAccessAll(ctx, false), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/proadmin"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraBotAdmin(ctx, channel, args, true), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/revadmin"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraBotAdmin(ctx, channel, args, false), nil
|
||||||
|
}},
|
||||||
|
{Aliases: []string{"/backup_db", "/restore_from_db", "/restart", "/update_bot", "/paolu", "/bindall_id", "/sync_favorites", "/coins", "/score", "/coinsall", "/coinsclear", "/red", "/srank", "/prouser", "/revuser", "/white_channel", "/rev_white_channel", "/unban_channel"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||||
|
return s.cmdSakuraUnsupported("Sakura 专属命令", "这些命令涉及外部 Bot 自身运维/积分红包/皮套人管理,已在 MediaStationGo 中由权限、通知渠道、设备策略替代。"), nil
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +187,15 @@ var telegramSupportedCommandSet = map[string]struct{}{
|
|||||||
"/capacity": {}, "/users": {}, "/gencode": {}, "/renew_user": {}, "/delete_user": {}, "/unbind": {}, "/unbind_duplicates": {}, "/unbind_inactive": {},
|
"/capacity": {}, "/users": {}, "/gencode": {}, "/renew_user": {}, "/delete_user": {}, "/unbind": {}, "/unbind_duplicates": {}, "/unbind_inactive": {},
|
||||||
"/devicepolicy": {}, "/policy": {}, "/antishare": {}, "/cleanup": {}, "/cleanup_mode": {}, "/cleanup_rule": {},
|
"/devicepolicy": {}, "/policy": {}, "/antishare": {}, "/cleanup": {}, "/cleanup_mode": {}, "/cleanup_rule": {},
|
||||||
"/ban": {}, "/unban": {}, "/status": {}, "/search": {}, "/downloads": {}, "/stats": {},
|
"/ban": {}, "/unban": {}, "/status": {}, "/search": {}, "/downloads": {}, "/stats": {},
|
||||||
|
"/renew": {}, "/ucr": {}, "/uinfo": {}, "/rmemby": {}, "/urm": {}, "/only_rm_emby": {}, "/only_rm_record": {},
|
||||||
|
"/userip": {}, "/udeviceid": {}, "/auditip": {}, "/auditdevice": {}, "/auditclient": {},
|
||||||
|
"/renewall": {}, "/callall": {}, "/syncunbound": {}, "/syncgroupm": {}, "/kick_not_emby": {}, "/scan_embyname": {},
|
||||||
|
"/check_ex": {}, "/deleted": {}, "/low_activity": {}, "/uranks": {}, "/days_ranks": {}, "/week_ranks": {},
|
||||||
|
"/embyadmin": {}, "/unbanall": {}, "/banall": {}, "/embylibs_unblockall": {}, "/embylibs_blockall": {},
|
||||||
|
"/extraembylibs_unblockall": {}, "/extraembylibs_blockall": {}, "/proadmin": {}, "/revadmin": {},
|
||||||
|
"/backup_db": {}, "/restore_from_db": {}, "/restart": {}, "/update_bot": {}, "/paolu": {}, "/bindall_id": {}, "/sync_favorites": {},
|
||||||
|
"/coins": {}, "/score": {}, "/coinsall": {}, "/coinsclear": {}, "/red": {}, "/srank": {}, "/prouser": {}, "/revuser": {},
|
||||||
|
"/white_channel": {}, "/rev_white_channel": {}, "/unban_channel": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
type telegramBotCommand struct {
|
type telegramBotCommand struct {
|
||||||
|
|||||||
@@ -0,0 +1,576 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraCreateUser(ctx context.Context, args []string) telegramCommandReply {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return telegramCommandReply{Text: "用法:<code>/ucr 用户名 密码 [天数]</code>,天数 0 表示永久。"}
|
||||||
|
}
|
||||||
|
if s.auth == nil {
|
||||||
|
return telegramCommandReply{Text: "注册服务暂不可用。"}
|
||||||
|
}
|
||||||
|
user, _, err := s.auth.Register(ctx, args[0], args[1])
|
||||||
|
if err != nil {
|
||||||
|
return telegramCommandReply{Text: "创建失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
days := 0
|
||||||
|
if len(args) >= 3 {
|
||||||
|
parsed, err := strconv.Atoi(args[2])
|
||||||
|
if err != nil || parsed < 0 {
|
||||||
|
return telegramCommandReply{Text: "账号已创建,但天数无效。请用 <code>/renew 用户名 天数</code> 调整。"}
|
||||||
|
}
|
||||||
|
days = parsed
|
||||||
|
if err := s.applyRenewal(ctx, user.ID, days); err != nil {
|
||||||
|
return telegramCommandReply{Text: "账号已创建,但续期失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("已创建用户:<b>%s</b>\n到期:<b>%s</b>", user.Username, formatExpiry(s.userExpiry(ctx, user.ID)))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraUserInfo(ctx context.Context, args []string) telegramCommandReply {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return telegramCommandReply{Text: "用法:<code>/uinfo 用户名</code>"}
|
||||||
|
}
|
||||||
|
user := s.findSakuraUser(ctx, args[0])
|
||||||
|
if user == nil {
|
||||||
|
return telegramCommandReply{Text: "未找到用户。"}
|
||||||
|
}
|
||||||
|
devices, _ := s.repo.UserDevice.ListByUser(ctx, user.ID)
|
||||||
|
var historyCount int64
|
||||||
|
_ = s.repo.DB.WithContext(ctx).Model(&model.PlaybackHistory{}).Where("user_id = ?", user.ID).Count(&historyCount).Error
|
||||||
|
var binding model.TelegramBinding
|
||||||
|
tg := "未绑定"
|
||||||
|
if err := s.repo.DB.WithContext(ctx).Where("user_id = ?", user.ID).First(&binding).Error; err == nil {
|
||||||
|
tg = fmt.Sprintf("tg:%d", binding.TelegramUserID)
|
||||||
|
if binding.TelegramName != "" {
|
||||||
|
tg += " " + binding.TelegramName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf(
|
||||||
|
"<b>用户信息</b>\n\n用户名:<b>%s</b>\n角色:<b>%s</b>\n状态:<b>%s</b>\n到期:<b>%s</b>\nTelegram:<b>%s</b>\n设备:<b>%d</b>\n播放记录:<b>%d</b>\n最后登录:<b>%s</b>",
|
||||||
|
user.Username, user.Role, activeLabel(user), formatExpiry(user.ExpiredAt), tg, len(devices), historyCount, formatOptionalTime(user.LastLoginAt),
|
||||||
|
)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraDeleteUser(ctx context.Context, args []string) telegramCommandReply {
|
||||||
|
if len(args) < 2 || !strings.EqualFold(args[len(args)-1], "confirm") {
|
||||||
|
return telegramCommandReply{Text: "删除用户需要确认:<code>/rmemby 用户名 confirm</code> 或 <code>/urm 用户名 confirm</code>"}
|
||||||
|
}
|
||||||
|
user := s.findSakuraUser(ctx, args[0])
|
||||||
|
if user == nil {
|
||||||
|
return telegramCommandReply{Text: "未找到用户。"}
|
||||||
|
}
|
||||||
|
if reason := s.protectReason(ctx, user.ID); reason != "" {
|
||||||
|
return telegramCommandReply{Text: reason}
|
||||||
|
}
|
||||||
|
_ = s.repo.UserDevice.DeleteByUser(ctx, user.ID)
|
||||||
|
if err := s.repo.User.Delete(ctx, user.ID); err != nil {
|
||||||
|
return telegramCommandReply{Text: "删除失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("已删除用户 <b>%s</b>。", user.Username)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraOnlyRemoveRecord(ctx context.Context, args []string) telegramCommandReply {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return telegramCommandReply{Text: "用法:<code>/only_rm_record tg:123456</code> 或 <code>/only_rm_record 用户名</code>,只删除 Telegram 绑定记录。"}
|
||||||
|
}
|
||||||
|
target := strings.TrimSpace(args[0])
|
||||||
|
var removed int64
|
||||||
|
if raw, ok := strings.CutPrefix(strings.ToLower(target), "tg:"); ok {
|
||||||
|
tgID, err := strconv.ParseInt(raw, 10, 64)
|
||||||
|
if err != nil || tgID == 0 {
|
||||||
|
return telegramCommandReply{Text: "Telegram ID 无效。"}
|
||||||
|
}
|
||||||
|
removed, err = s.deleteTelegramBindings(ctx, "telegram_user_id = ?", tgID)
|
||||||
|
if err != nil {
|
||||||
|
return telegramCommandReply{Text: "删除绑定失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user := s.findSakuraUser(ctx, target)
|
||||||
|
if user == nil {
|
||||||
|
return telegramCommandReply{Text: "未找到用户。"}
|
||||||
|
}
|
||||||
|
n, err := s.deleteTelegramBindings(ctx, "user_id = ?", user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return telegramCommandReply{Text: "删除绑定失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
removed = n
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("已删除 Telegram 绑定记录:<b>%d</b> 条。", removed)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraUserIP(ctx context.Context, args []string) telegramCommandReply {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return telegramCommandReply{Text: "用法:<code>/userip 用户名</code>"}
|
||||||
|
}
|
||||||
|
user := s.findSakuraUser(ctx, args[0])
|
||||||
|
if user == nil {
|
||||||
|
return telegramCommandReply{Text: "未找到用户。"}
|
||||||
|
}
|
||||||
|
devices, err := s.repo.UserDevice.ListByUser(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return telegramCommandReply{Text: "查询失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
if len(devices) == 0 {
|
||||||
|
return telegramCommandReply{Text: "该用户暂无设备/IP记录。"}
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for i, d := range devices {
|
||||||
|
if i >= 20 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out = append(out, fmt.Sprintf("%d. %s / %s / %s / %s", i+1, blankDash(d.LastIP), blankDash(d.DeviceName), blankDash(d.Client), d.LastSeenAt.Format("2006-01-02 15:04")))
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: "<b>" + user.Username + " 的设备/IP</b>\n\n<code>" + strings.Join(out, "\n") + "</code>"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraAuditDevices(ctx context.Context, mode string, args []string) telegramCommandReply {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("用法:<code>/%s 关键词</code>", mode)}
|
||||||
|
}
|
||||||
|
keyword := strings.TrimSpace(strings.Join(args, " "))
|
||||||
|
var rows []struct {
|
||||||
|
Username string
|
||||||
|
DeviceID string
|
||||||
|
DeviceName string
|
||||||
|
Client string
|
||||||
|
LastIP string
|
||||||
|
LastSeenAt time.Time
|
||||||
|
}
|
||||||
|
q := s.repo.DB.WithContext(ctx).Table("user_devices").
|
||||||
|
Select("users.username, user_devices.device_id, user_devices.device_name, user_devices.client, user_devices.last_ip, user_devices.last_seen_at").
|
||||||
|
Joins("JOIN users ON users.id = user_devices.user_id").
|
||||||
|
Order("user_devices.last_seen_at desc").
|
||||||
|
Limit(20)
|
||||||
|
switch mode {
|
||||||
|
case "auditip":
|
||||||
|
q = q.Where("user_devices.last_ip LIKE ?", "%"+keyword+"%")
|
||||||
|
case "auditdevice":
|
||||||
|
q = q.Where("user_devices.device_name LIKE ? OR user_devices.device_id LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||||
|
case "auditclient":
|
||||||
|
q = q.Where("user_devices.client LIKE ?", "%"+keyword+"%")
|
||||||
|
case "udeviceid":
|
||||||
|
q = q.Where("user_devices.device_id LIKE ?", "%"+keyword+"%")
|
||||||
|
}
|
||||||
|
if err := q.Scan(&rows).Error; err != nil {
|
||||||
|
return telegramCommandReply{Text: "查询失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return telegramCommandReply{Text: "没有匹配记录。"}
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for i, r := range rows {
|
||||||
|
out = append(out, fmt.Sprintf("%d. %s / %s / %s / %s / %s", i+1, r.Username, blankDash(r.LastIP), blankDash(r.DeviceName), blankDash(r.Client), r.LastSeenAt.Format("2006-01-02 15:04")))
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: "<b>审计结果</b>\n\n<code>" + strings.Join(out, "\n") + "</code>"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraRenewAll(ctx context.Context, args []string) telegramCommandReply {
|
||||||
|
if len(args) < 2 || !strings.EqualFold(args[len(args)-1], "confirm") {
|
||||||
|
return telegramCommandReply{Text: "批量续期需要确认:<code>/renewall 天数 confirm</code>"}
|
||||||
|
}
|
||||||
|
days, err := strconv.Atoi(args[0])
|
||||||
|
if err != nil || days < 0 {
|
||||||
|
return telegramCommandReply{Text: "天数必须是非负整数,0 表示永久。"}
|
||||||
|
}
|
||||||
|
users, err := s.repo.User.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return telegramCommandReply{Text: "读取用户失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
var count int
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Role == "admin" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.applyRenewal(ctx, user.ID, days); err == nil {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("批量续期完成:<b>%d</b> 个普通用户。", count)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraBanAll(ctx context.Context, active bool, args []string) telegramCommandReply {
|
||||||
|
if len(args) == 0 || !strings.EqualFold(args[len(args)-1], "confirm") {
|
||||||
|
action := "banall"
|
||||||
|
if active {
|
||||||
|
action = "unbanall"
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("批量操作需要确认:<code>/%s confirm</code>", action)}
|
||||||
|
}
|
||||||
|
users, err := s.repo.User.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return telegramCommandReply{Text: "读取用户失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
var count int
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Role == "admin" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updates := map[string]any{"is_active": active}
|
||||||
|
if active {
|
||||||
|
updates["share_warnings"] = 0
|
||||||
|
updates["last_share_warn_at"] = nil
|
||||||
|
}
|
||||||
|
if err := s.repo.User.UpdateFields(ctx, user.ID, updates); err == nil {
|
||||||
|
_ = s.repo.UserDevice.SetKickedByUser(ctx, user.ID, !active)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if active {
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("已解禁普通用户:<b>%d</b> 个。", count)}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("已禁用普通用户:<b>%d</b> 个。", count)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraCallAll(ctx context.Context, channel *model.NotifyChannel, args []string) telegramCommandReply {
|
||||||
|
message := strings.TrimSpace(strings.Join(args, " "))
|
||||||
|
if message == "" {
|
||||||
|
return telegramCommandReply{Text: "用法:<code>/callall 消息内容</code>"}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.telegramChannelConfig(channel)["bot_token"]) == "" {
|
||||||
|
return telegramCommandReply{Text: "当前 Telegram 渠道未配置 bot_token,无法群发。"}
|
||||||
|
}
|
||||||
|
var bindings []model.TelegramBinding
|
||||||
|
if err := s.repo.DB.WithContext(ctx).Find(&bindings).Error; err != nil {
|
||||||
|
return telegramCommandReply{Text: "读取绑定失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
sent := 0
|
||||||
|
for _, binding := range bindings {
|
||||||
|
if binding.ChatID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.reply(ctx, channel, int(binding.ChatID), telegramCommandReply{Text: message}); err == nil {
|
||||||
|
sent++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("群发完成:成功发送 <b>%d</b> 个绑定用户。", sent)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraSyncUnbound(ctx context.Context, args []string) telegramCommandReply {
|
||||||
|
var users []model.User
|
||||||
|
if err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("role <> ?", "admin").
|
||||||
|
Where("NOT EXISTS (SELECT 1 FROM telegram_bindings WHERE telegram_bindings.user_id = users.id AND telegram_bindings.deleted_at IS NULL)").
|
||||||
|
Order("created_at asc").Find(&users).Error; err != nil {
|
||||||
|
return telegramCommandReply{Text: "查询失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
if len(args) >= 2 && strings.EqualFold(args[0], "delete") && strings.EqualFold(args[1], "confirm") {
|
||||||
|
deleted := 0
|
||||||
|
for _, user := range users {
|
||||||
|
_ = s.repo.UserDevice.DeleteByUser(ctx, user.ID)
|
||||||
|
if err := s.repo.User.Delete(ctx, user.ID); err == nil {
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("已删除未绑定 Bot 的普通用户:<b>%d</b> 个。", deleted)}
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
return telegramCommandReply{Text: "没有未绑定 Bot 的普通用户。"}
|
||||||
|
}
|
||||||
|
names := make([]string, 0, minInt(len(users), 20))
|
||||||
|
for i, user := range users {
|
||||||
|
if i >= 20 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
names = append(names, user.Username)
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("未绑定 Bot 的普通用户:<b>%d</b> 个。\n%s\n\n如需删除:<code>/syncunbound delete confirm</code>", len(users), telegramInlineCodeList(names))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraCheckExpired(ctx context.Context, args []string) telegramCommandReply {
|
||||||
|
now := time.Now()
|
||||||
|
var users []model.User
|
||||||
|
if err := s.repo.DB.WithContext(ctx).Where("expired_at IS NOT NULL AND expired_at < ?", now).Order("expired_at asc").Find(&users).Error; err != nil {
|
||||||
|
return telegramCommandReply{Text: "查询失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
if len(args) >= 2 && strings.EqualFold(args[0], "disable") && strings.EqualFold(args[1], "confirm") {
|
||||||
|
disabled := 0
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Role == "admin" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.repo.User.UpdateFields(ctx, user.ID, map[string]any{"is_active": false}); err == nil {
|
||||||
|
disabled++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("已禁用过期普通用户:<b>%d</b> 个。", disabled)}
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
return telegramCommandReply{Text: "没有过期用户。"}
|
||||||
|
}
|
||||||
|
lines := make([]string, 0, minInt(len(users), 20))
|
||||||
|
for i, user := range users {
|
||||||
|
if i >= 20 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("%s(%s)", user.Username, formatExpiry(user.ExpiredAt)))
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("过期用户:<b>%d</b> 个。\n%s\n\n如需禁用:<code>/check_ex disable confirm</code>", len(users), telegramInlineCodeList(lines))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraScanNames(ctx context.Context) telegramCommandReply {
|
||||||
|
var rows []struct {
|
||||||
|
Username string
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
if err := s.repo.DB.WithContext(ctx).Table("users").
|
||||||
|
Select("LOWER(username) AS username, COUNT(*) AS count").
|
||||||
|
Group("LOWER(username)").Having("COUNT(*) > 1").Scan(&rows).Error; err != nil {
|
||||||
|
return telegramCommandReply{Text: "扫描失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return telegramCommandReply{Text: "未发现同名用户记录。"}
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, fmt.Sprintf("%s x%d", row.Username, row.Count))
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: "<b>同名用户记录</b>\n" + telegramInlineCodeList(out)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraRanks(ctx context.Context, window time.Duration, byDuration bool) telegramCommandReply {
|
||||||
|
since := time.Now().Add(-window)
|
||||||
|
title := "播放次数排行"
|
||||||
|
selectExpr := "COUNT(*) AS score"
|
||||||
|
if byDuration {
|
||||||
|
title = "观影时长排行"
|
||||||
|
selectExpr = "COALESCE(SUM(position_ms), 0) AS score"
|
||||||
|
}
|
||||||
|
q := s.repo.DB.WithContext(ctx).Table("playback_histories").
|
||||||
|
Select("users.username, " + selectExpr).
|
||||||
|
Joins("JOIN users ON users.id = playback_histories.user_id").
|
||||||
|
Group("users.username").
|
||||||
|
Order("score DESC").
|
||||||
|
Limit(10)
|
||||||
|
if window > 0 {
|
||||||
|
q = q.Where("playback_histories.watched_at >= ?", since)
|
||||||
|
}
|
||||||
|
var rows []struct {
|
||||||
|
Username string
|
||||||
|
Score int64
|
||||||
|
}
|
||||||
|
if err := q.Scan(&rows).Error; err != nil {
|
||||||
|
return telegramCommandReply{Text: "排行查询失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return telegramCommandReply{Text: "暂无排行数据。"}
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for i, row := range rows {
|
||||||
|
score := fmt.Sprintf("%d 次", row.Score)
|
||||||
|
if byDuration {
|
||||||
|
score = humanDurationFromMillis(row.Score)
|
||||||
|
}
|
||||||
|
out = append(out, fmt.Sprintf("%d. %s — %s", i+1, row.Username, score))
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: "<b>" + title + "</b>\n\n<code>" + strings.Join(out, "\n") + "</code>"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraAdminRole(ctx context.Context, args []string) telegramCommandReply {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return telegramCommandReply{Text: "用法:<code>/embyadmin 用户名 on|off</code>"}
|
||||||
|
}
|
||||||
|
user := s.findSakuraUser(ctx, args[0])
|
||||||
|
if user == nil {
|
||||||
|
return telegramCommandReply{Text: "未找到用户。"}
|
||||||
|
}
|
||||||
|
enable := parseOnOff(args[1])
|
||||||
|
if enable == nil {
|
||||||
|
return telegramCommandReply{Text: "第二个参数请使用 on/off。"}
|
||||||
|
}
|
||||||
|
if !*enable {
|
||||||
|
if first, _ := s.repo.User.FirstAdmin(ctx); first != nil && first.ID == user.ID {
|
||||||
|
return telegramCommandReply{Text: "默认管理员不可降级。"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
role := "user"
|
||||||
|
if *enable {
|
||||||
|
role = "admin"
|
||||||
|
}
|
||||||
|
if err := s.repo.User.UpdateFields(ctx, user.ID, map[string]any{"role": role}); err != nil {
|
||||||
|
return telegramCommandReply{Text: "更新失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("已将 <b>%s</b> 角色设置为 <b>%s</b>。", user.Username, role)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraMediaAccessAll(ctx context.Context, allow bool) telegramCommandReply {
|
||||||
|
users, err := s.repo.User.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return telegramCommandReply{Text: "读取用户失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
updated := 0
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Role == "admin" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
perm, err := s.repo.Permission.FindByUserID(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if perm == nil {
|
||||||
|
perm = DefaultPermissions(user.ID)
|
||||||
|
perm.CanPlayMedia = allow
|
||||||
|
if err := s.repo.Permission.Create(ctx, perm); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.repo.DB.WithContext(ctx).Model(&model.UserPermission{}).
|
||||||
|
Where("user_id = ?", user.ID).
|
||||||
|
Update("can_play_media", allow).Error; err == nil {
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state := "关闭"
|
||||||
|
if allow {
|
||||||
|
state = "开启"
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: fmt.Sprintf("已为普通用户%s媒体播放权限:<b>%d</b> 个。", state, updated)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraBotAdmin(ctx context.Context, channel *model.NotifyChannel, args []string, add bool) telegramCommandReply {
|
||||||
|
if channel == nil {
|
||||||
|
return telegramCommandReply{Text: "Telegram 渠道不存在。"}
|
||||||
|
}
|
||||||
|
if len(args) == 0 {
|
||||||
|
return telegramCommandReply{Text: "用法:<code>/proadmin TelegramID</code> 或 <code>/revadmin TelegramID</code>"}
|
||||||
|
}
|
||||||
|
tgID := strings.TrimPrefix(strings.TrimSpace(args[0]), "tg:")
|
||||||
|
if _, err := strconv.ParseInt(tgID, 10, 64); err != nil {
|
||||||
|
return telegramCommandReply{Text: "TelegramID 必须是数字。"}
|
||||||
|
}
|
||||||
|
cfg := s.telegramChannelConfig(channel)
|
||||||
|
ids := telegramConfiguredUserIDs(cfg["admin_user_ids"])
|
||||||
|
seen := make(map[string]bool, len(ids)+1)
|
||||||
|
var next []string
|
||||||
|
for _, id := range ids {
|
||||||
|
if id == tgID {
|
||||||
|
seen[id] = true
|
||||||
|
if add {
|
||||||
|
next = append(next, id)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if id != "" {
|
||||||
|
next = append(next, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if add && !seen[tgID] {
|
||||||
|
next = append(next, tgID)
|
||||||
|
}
|
||||||
|
cfg["admin_user_ids"] = strings.Join(next, ",")
|
||||||
|
raw, _ := json.Marshal(cfg)
|
||||||
|
updated := *channel
|
||||||
|
updated.Config = string(raw)
|
||||||
|
if s.crypto != nil {
|
||||||
|
updated.Config = s.crypto.Encrypt(updated.Config)
|
||||||
|
}
|
||||||
|
if err := s.repo.NotifyChannel.Update(ctx, &updated); err != nil {
|
||||||
|
return telegramCommandReply{Text: "更新管理员列表失败:" + err.Error()}
|
||||||
|
}
|
||||||
|
if add {
|
||||||
|
return telegramCommandReply{Text: "已添加 Bot 管理员:<code>" + tgID + "</code>"}
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: "已移除 Bot 管理员:<code>" + tgID + "</code>"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) cmdSakuraUnsupported(name, replacement string) telegramCommandReply {
|
||||||
|
text := fmt.Sprintf("<b>%s</b> 已识别,但当前 Telegram Bot API 无法完整复刻该行为。", name)
|
||||||
|
if replacement != "" {
|
||||||
|
text += "\n请使用:" + replacement
|
||||||
|
}
|
||||||
|
return telegramCommandReply{Text: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TelegramBotService) findSakuraUser(ctx context.Context, target string) *model.User {
|
||||||
|
target = strings.TrimSpace(strings.TrimPrefix(target, "@"))
|
||||||
|
if target == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if user, _ := s.repo.User.FindByUsername(ctx, target); user != nil {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
if user, _ := s.repo.User.FindByID(ctx, target); user != nil {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
if tgRaw, ok := strings.CutPrefix(strings.ToLower(target), "tg:"); ok {
|
||||||
|
if tgID, err := strconv.ParseInt(tgRaw, 10, 64); err == nil {
|
||||||
|
var binding model.TelegramBinding
|
||||||
|
if err := s.repo.DB.WithContext(ctx).Where("telegram_user_id = ?", tgID).First(&binding).Error; err == nil {
|
||||||
|
user, _ := s.repo.User.FindByID(ctx, binding.UserID)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeLabel(user *model.User) string {
|
||||||
|
if user == nil {
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
|
if !user.IsActive {
|
||||||
|
return "已禁用"
|
||||||
|
}
|
||||||
|
if user.ExpiredAt != nil && time.Now().After(*user.ExpiredAt) {
|
||||||
|
return "已过期"
|
||||||
|
}
|
||||||
|
return "正常"
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatOptionalTime(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func blankDash(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func telegramInlineCodeList(items []string) string {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "<code>" + strings.Join(items, "</code>、<code>") + "</code>"
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOnOff(raw string) *bool {
|
||||||
|
value := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
switch value {
|
||||||
|
case "on", "true", "1", "yes", "enable", "enabled", "开启", "开":
|
||||||
|
v := true
|
||||||
|
return &v
|
||||||
|
case "off", "false", "0", "no", "disable", "disabled", "关闭", "关":
|
||||||
|
v := false
|
||||||
|
return &v
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanDurationFromMillis(ms int64) string {
|
||||||
|
if ms <= 0 {
|
||||||
|
return "0 分钟"
|
||||||
|
}
|
||||||
|
totalMinutes := ms / 1000 / 60
|
||||||
|
hours := totalMinutes / 60
|
||||||
|
minutes := totalMinutes % 60
|
||||||
|
if hours == 0 {
|
||||||
|
return fmt.Sprintf("%d 分钟", minutes)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d 小时 %d 分钟", hours, minutes)
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSakuraUserManagementAndAuditCommands(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
repos, bot := newBotTestService(t)
|
||||||
|
if err := repos.User.Create(ctx, &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9401"}`}
|
||||||
|
msg := &TelegramMessage{From: TelegramUser{ID: 9401, Username: "admin"}, Chat: TelegramChat{ID: 9401, Type: "private"}}
|
||||||
|
|
||||||
|
reply, err := bot.executeCommand(ctx, channel, msg, "/ucr viewer secret-pass 30")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(reply.Text, "已创建用户") {
|
||||||
|
t.Fatalf("expected user creation, got %q", reply.Text)
|
||||||
|
}
|
||||||
|
viewer, err := repos.User.FindByUsername(ctx, "viewer")
|
||||||
|
if err != nil || viewer == nil {
|
||||||
|
t.Fatalf("viewer should exist: %v", err)
|
||||||
|
}
|
||||||
|
if err := repos.UserDevice.Create(ctx, &model.UserDevice{
|
||||||
|
UserID: viewer.ID,
|
||||||
|
DeviceID: "dev-abc",
|
||||||
|
DeviceName: "Windows PC",
|
||||||
|
Client: "Infuse",
|
||||||
|
LastIP: "1.2.3.4",
|
||||||
|
FirstSeenAt: time.Now(),
|
||||||
|
LastSeenAt: time.Now(),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
command string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"/uinfo viewer", "用户信息"},
|
||||||
|
{"/userip viewer", "1.2.3.4"},
|
||||||
|
{"/auditip 1.2.3", "viewer"},
|
||||||
|
{"/auditdevice Windows", "viewer"},
|
||||||
|
{"/auditclient Infuse", "viewer"},
|
||||||
|
{"/udeviceid dev-abc", "viewer"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.command, func(t *testing.T) {
|
||||||
|
reply, err := bot.executeCommand(ctx, channel, msg, tc.command)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(reply.Text, tc.want) {
|
||||||
|
t.Fatalf("%s expected %q in %q", tc.command, tc.want, reply.Text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSakuraBatchAndPermissionCommands(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
repos, bot := newBotTestService(t)
|
||||||
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9501"}`}
|
||||||
|
msg := &TelegramMessage{From: TelegramUser{ID: 9501, Username: "admin"}, Chat: TelegramChat{ID: 9501, Type: "private"}}
|
||||||
|
users := []*model.User{
|
||||||
|
{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true},
|
||||||
|
{Username: "viewer1", PasswordHash: "x", Role: "user", IsActive: true},
|
||||||
|
{Username: "viewer2", PasswordHash: "x", Role: "user", IsActive: true},
|
||||||
|
{Username: "viewer3", PasswordHash: "x", Role: "user", IsActive: true},
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
if err := repos.User.Create(ctx, user); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if user.Role != "admin" && user.Username != "viewer3" {
|
||||||
|
if err := repos.Permission.Create(ctx, DefaultPermissions(user.ID)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := bot.executeCommand(ctx, channel, msg, "/renewall 7 confirm")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(reply.Text, "批量续期完成") {
|
||||||
|
t.Fatalf("expected renewall success, got %q", reply.Text)
|
||||||
|
}
|
||||||
|
renewed, _ := repos.User.FindByUsername(ctx, "viewer1")
|
||||||
|
if renewed.ExpiredAt == nil {
|
||||||
|
t.Fatal("renewall should set expiry for normal users")
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply, err = bot.executeCommand(ctx, channel, msg, "/embylibs_blockall"); err != nil || !strings.Contains(reply.Text, "关闭媒体播放权限") {
|
||||||
|
t.Fatalf("expected blockall success, reply=%q err=%v", reply.Text, err)
|
||||||
|
}
|
||||||
|
perm, _ := repos.Permission.FindByUserID(ctx, users[1].ID)
|
||||||
|
if perm == nil || perm.CanPlayMedia {
|
||||||
|
t.Fatal("embylibs_blockall should disable media playback for normal users")
|
||||||
|
}
|
||||||
|
perm, _ = repos.Permission.FindByUserID(ctx, users[3].ID)
|
||||||
|
if perm == nil || perm.CanPlayMedia {
|
||||||
|
t.Fatal("embylibs_blockall should create disabled media playback permissions when missing")
|
||||||
|
}
|
||||||
|
if reply, err = bot.executeCommand(ctx, channel, msg, "/embylibs_unblockall"); err != nil || !strings.Contains(reply.Text, "开启媒体播放权限") {
|
||||||
|
t.Fatalf("expected unblockall success, reply=%q err=%v", reply.Text, err)
|
||||||
|
}
|
||||||
|
perm, _ = repos.Permission.FindByUserID(ctx, users[1].ID)
|
||||||
|
if perm == nil || !perm.CanPlayMedia {
|
||||||
|
t.Fatal("embylibs_unblockall should enable media playback for normal users")
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply, err = bot.executeCommand(ctx, channel, msg, "/banall confirm"); err != nil || !strings.Contains(reply.Text, "已禁用普通用户") {
|
||||||
|
t.Fatalf("expected banall success, reply=%q err=%v", reply.Text, err)
|
||||||
|
}
|
||||||
|
banned, _ := repos.User.FindByUsername(ctx, "viewer2")
|
||||||
|
if banned.IsActive {
|
||||||
|
t.Fatal("banall should disable normal users")
|
||||||
|
}
|
||||||
|
if reply, err = bot.executeCommand(ctx, channel, msg, "/unbanall confirm"); err != nil || !strings.Contains(reply.Text, "已解禁普通用户") {
|
||||||
|
t.Fatalf("expected unbanall success, reply=%q err=%v", reply.Text, err)
|
||||||
|
}
|
||||||
|
unbanned, _ := repos.User.FindByUsername(ctx, "viewer2")
|
||||||
|
if !unbanned.IsActive {
|
||||||
|
t.Fatal("unbanall should re-enable normal users")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSakuraSyncExpiryAndBotAdminCommands(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
repos, bot := newBotTestService(t)
|
||||||
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9601"}`}
|
||||||
|
if err := repos.NotifyChannel.Create(ctx, channel); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
msg := &TelegramMessage{From: TelegramUser{ID: 9601, Username: "admin"}, Chat: TelegramChat{ID: 9601, Type: "private"}}
|
||||||
|
past := time.Now().Add(-24 * time.Hour)
|
||||||
|
if err := repos.User.Create(ctx, &model.User{Username: "expired", PasswordHash: "x", Role: "user", IsActive: true, ExpiredAt: &past}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := bot.executeCommand(ctx, channel, msg, "/syncunbound")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(reply.Text, "expired") {
|
||||||
|
t.Fatalf("syncunbound should list unbound users, got %q", reply.Text)
|
||||||
|
}
|
||||||
|
reply, err = bot.executeCommand(ctx, channel, msg, "/check_ex")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(reply.Text, "expired") {
|
||||||
|
t.Fatalf("check_ex should list expired users, got %q", reply.Text)
|
||||||
|
}
|
||||||
|
reply, err = bot.executeCommand(ctx, channel, msg, "/proadmin 9602")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(reply.Text, "已添加") {
|
||||||
|
t.Fatalf("proadmin should update channel config, got %q", reply.Text)
|
||||||
|
}
|
||||||
|
updated, _ := repos.NotifyChannel.FindByID(ctx, channel.ID)
|
||||||
|
cfg := bot.telegramChannelConfig(updated)
|
||||||
|
if !strings.Contains(cfg["admin_user_ids"], "9602") {
|
||||||
|
t.Fatalf("expected admin ids to include 9602, got %#v", cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user