mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
fix(telegram): 区分'不在群'与'查不了',修复在群却被拒注册/绑定
getChatMember 失败(Bot不在群/频道非管理员/群ID失效/代理超时)此前被 吞成 false,导致已在群用户收到'不在绑定群组'误导提示。 - telegramChatMembership 返回三态(是/否/查不了) - telegramUserBindDecision 据此返回 allowed/denied/unverifiable - 四个拦截点(start绑定/register/redeem注册/hideadult)在'查不了' 时改提示管理员检查Bot权限与群ID,不再误导用户 - syncgroupm 仅在'查实非成员'时判定可清理,查不了保守跳过防误删 - getChatMember 超时 8s->15s,缓解代理回退预算耗尽
This commit is contained in:
@@ -347,8 +347,8 @@ func (s *TelegramBotService) cmdStart(ctx context.Context, msg *TelegramMessage,
|
||||
return telegramCommandReply{Text: "<b>欢迎使用 MediaStationGo</b>\n\n普通用户请先绑定账号:\n<code>/start 用户名 密码</code>\n或:<code>/start 用户名-密码</code>\n\n" + hint}
|
||||
}
|
||||
channel := s.findChannelForMessage(ctx, msg)
|
||||
if !s.telegramUserCanBind(ctx, channel, msg.From.ID) {
|
||||
return telegramCommandReply{Text: "当前 Telegram 账号不在管理员配置的绑定群组/频道中,无法绑定媒体中心账号。请先加入管理员配置的群组或频道;如果尚未配置,请联系管理员。"}
|
||||
if dec := s.telegramUserBindDecision(ctx, channel, msg.From.ID); dec != bindAllowed {
|
||||
return telegramCommandReply{Text: telegramBindRejectText(dec, "绑定媒体中心账号")}
|
||||
}
|
||||
username, password := parseStartCredentials(args)
|
||||
if username == "" || password == "" {
|
||||
@@ -404,8 +404,8 @@ func (s *TelegramBotService) cmdRegister(ctx context.Context, channel *model.Not
|
||||
if channel == nil {
|
||||
channel = s.findChannelForMessage(ctx, msg)
|
||||
}
|
||||
if !s.telegramUserCanBind(ctx, channel, msg.From.ID) {
|
||||
return telegramCommandReply{Text: "当前 Telegram 账号不在管理员配置的绑定群组/频道中,无法注册账号。请先加入管理员配置的群组或频道;如果尚未配置,请联系管理员。"}
|
||||
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 {
|
||||
@@ -565,8 +565,8 @@ func telegramMgoAdminCommandHelp() string {
|
||||
// cmdStatus 处理 /status 命令。
|
||||
func (s *TelegramBotService) cmdHideAdult(ctx context.Context, msg *TelegramMessage, args []string) telegramCommandReply {
|
||||
channel := s.findChannelForMessage(ctx, msg)
|
||||
if !s.telegramUserCanBind(ctx, channel, msg.From.ID) {
|
||||
return telegramCommandReply{Text: "当前 Telegram 账号不在管理员配置的绑定群组/频道中,无法使用成人目录隐藏开关。"}
|
||||
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 {
|
||||
@@ -1289,34 +1289,72 @@ func (s *TelegramBotService) telegramChatAllowed(channel *model.NotifyChannel, c
|
||||
return strings.TrimSpace(cfg["chat_id"]) == target
|
||||
}
|
||||
|
||||
func (s *TelegramBotService) telegramUserCanBind(ctx context.Context, channel *model.NotifyChannel, telegramUserID int) bool {
|
||||
// telegramBindDecision 表示成员资格校验的三态结果:通过 / 明确不通过 /
|
||||
// 无法验证(getChatMember 出错,如 Bot 不在群、群 ID 失效、网络或代理不可达)。
|
||||
// 区分「明确不是成员」和「查不了」,是为了避免把验证失败误报成「你不在群」。
|
||||
type telegramBindDecision int
|
||||
|
||||
const (
|
||||
bindDenied telegramBindDecision = iota // 已查实:不在任何绑定群组/频道
|
||||
bindAllowed // 管理员,或查实是某绑定群组/频道成员
|
||||
bindUnverifiable // 配了群组/频道但 getChatMember 全部失败
|
||||
)
|
||||
|
||||
// telegramMembership 表示单个 chat 的成员资格三态。
|
||||
type telegramMembership int
|
||||
|
||||
const (
|
||||
membershipNo telegramMembership = iota // 查实不是成员(left/kicked 等)
|
||||
membershipYes // 查实是成员
|
||||
membershipUnknown // getChatMember 出错,无法判定
|
||||
)
|
||||
|
||||
func (s *TelegramBotService) telegramUserBindDecision(ctx context.Context, channel *model.NotifyChannel, telegramUserID int) telegramBindDecision {
|
||||
if telegramUserID == 0 || channel == nil {
|
||||
return false
|
||||
return bindDenied
|
||||
}
|
||||
if s.telegramUserIDConfigured(channel, telegramUserID) {
|
||||
return true
|
||||
return bindAllowed
|
||||
}
|
||||
cfg := s.telegramChannelConfig(channel)
|
||||
groupID := strings.TrimSpace(cfg["group_chat_id"])
|
||||
channelID := strings.TrimSpace(cfg["channel_chat_id"])
|
||||
if groupID == "" && channelID == "" {
|
||||
return false
|
||||
chatIDs := s.telegramMembershipChatIDs(channel)
|
||||
if len(chatIDs) == 0 {
|
||||
return bindDenied
|
||||
}
|
||||
for _, chatID := range []string{groupID, channelID} {
|
||||
if chatID == "" {
|
||||
continue
|
||||
}
|
||||
if s.telegramUserIsChatMember(ctx, channel, chatID, telegramUserID) {
|
||||
return true
|
||||
sawUnknown := false
|
||||
for _, chatID := range chatIDs {
|
||||
switch s.telegramChatMembership(ctx, channel, chatID, telegramUserID) {
|
||||
case membershipYes:
|
||||
return bindAllowed
|
||||
case membershipUnknown:
|
||||
sawUnknown = true
|
||||
}
|
||||
}
|
||||
return false
|
||||
if sawUnknown {
|
||||
return bindUnverifiable
|
||||
}
|
||||
return bindDenied
|
||||
}
|
||||
|
||||
func (s *TelegramBotService) telegramUserIsChatMember(ctx context.Context, channel *model.NotifyChannel, chatID string, telegramUserID int) bool {
|
||||
// telegramUserCanBind 是 telegramUserBindDecision 的布尔包装,供尽力而为的场景
|
||||
// 使用(如私聊时挑选可用渠道):只有查实通过才返回 true。
|
||||
func (s *TelegramBotService) telegramUserCanBind(ctx context.Context, channel *model.NotifyChannel, telegramUserID int) bool {
|
||||
return s.telegramUserBindDecision(ctx, channel, telegramUserID) == bindAllowed
|
||||
}
|
||||
|
||||
// telegramBindRejectText 根据三态结果生成面向用户的提示。action 形如「兑换注册账号」
|
||||
// 「绑定媒体中心账号」。bindUnverifiable 时不再误导用户「你不在群」,而是提示
|
||||
// 管理员检查 Bot 权限与群组 ID。
|
||||
func telegramBindRejectText(decision telegramBindDecision, action string) string {
|
||||
if decision == bindUnverifiable {
|
||||
return fmt.Sprintf("暂时无法验证你的群组/频道成员身份,%s未成功。这通常是因为 Bot 未加入绑定群组、在频道中不是管理员,或群组 ID 配置有误(如超级群需带 -100 前缀)。请联系管理员检查 Bot 权限与「绑定群组/频道 ID」。", action)
|
||||
}
|
||||
return fmt.Sprintf("当前 Telegram 账号不在管理员配置的绑定群组/频道中,无法%s。请先加入管理员配置的群组或频道;如果尚未配置,请联系管理员。", action)
|
||||
}
|
||||
|
||||
func (s *TelegramBotService) telegramChatMembership(ctx context.Context, channel *model.NotifyChannel, chatID string, telegramUserID int) telegramMembership {
|
||||
cfg := s.telegramChannelConfig(channel)
|
||||
if strings.TrimSpace(cfg["bot_token"]) == "" || chatID == "" || telegramUserID == 0 {
|
||||
return false
|
||||
return membershipUnknown
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"chat_id": chatID,
|
||||
@@ -1328,21 +1366,27 @@ func (s *TelegramBotService) telegramUserIsChatMember(ctx context.Context, chann
|
||||
Status string `json:"status"`
|
||||
} `json:"result"`
|
||||
}
|
||||
if err := telegramPostJSONDecode(ctx, cfg, "getChatMember", payload, 8*time.Second, &result); err != nil {
|
||||
if err := telegramPostJSONDecode(ctx, cfg, "getChatMember", payload, 15*time.Second, &result); err != nil {
|
||||
s.log.Warn("telegram getChatMember failed", zap.String("chat_id", chatID), zap.Int("telegram_user_id", telegramUserID), zap.Error(sanitizeTelegramError(err)))
|
||||
return false
|
||||
return membershipUnknown
|
||||
}
|
||||
if !result.OK {
|
||||
return false
|
||||
return membershipUnknown
|
||||
}
|
||||
switch strings.ToLower(result.Result.Status) {
|
||||
case "creator", "administrator", "member", "restricted":
|
||||
return true
|
||||
return membershipYes
|
||||
default:
|
||||
return false
|
||||
return membershipNo
|
||||
}
|
||||
}
|
||||
|
||||
// telegramUserIsChatMember 是 telegramChatMembership 的布尔包装,仅在查实是成员时
|
||||
// 返回 true(查不了也视为非成员,供尽力而为的场景使用)。
|
||||
func (s *TelegramBotService) telegramUserIsChatMember(ctx context.Context, channel *model.NotifyChannel, chatID string, telegramUserID int) bool {
|
||||
return s.telegramChatMembership(ctx, channel, chatID, telegramUserID) == membershipYes
|
||||
}
|
||||
|
||||
func (s *TelegramBotService) telegramUserIDConfigured(channel *model.NotifyChannel, telegramUserID int) bool {
|
||||
if channel == nil || telegramUserID == 0 {
|
||||
return false
|
||||
|
||||
@@ -526,8 +526,8 @@ func (s *TelegramBotService) redeemRegisterFlow(ctx context.Context, channel *mo
|
||||
if channel == nil {
|
||||
channel = s.findChannelForMessage(ctx, msg)
|
||||
}
|
||||
if !s.telegramUserCanBind(ctx, channel, msg.From.ID) {
|
||||
return telegramCommandReply{Text: "当前 Telegram 账号不在管理员配置的绑定群组/频道中,无法兑换注册账号。请先加入管理员配置的群组或频道;如果尚未配置,请联系管理员。"}
|
||||
if dec := s.telegramUserBindDecision(ctx, channel, msg.From.ID); dec != bindAllowed {
|
||||
return telegramCommandReply{Text: telegramBindRejectText(dec, "兑换注册账号")}
|
||||
}
|
||||
rc, errMsg := s.lookupRedeemableCode(ctx, raw, model.RegistrationCodeRegister)
|
||||
if rc == nil {
|
||||
|
||||
@@ -596,14 +596,16 @@ func (s *TelegramBotService) cmdMgoSyncGroup(ctx context.Context, channel *model
|
||||
if user == nil || UserIsProtectedAccount(ctx, s.repo, user) {
|
||||
continue
|
||||
}
|
||||
member := false
|
||||
// 仅当所有绑定群组/频道都「查实不是成员」时才判定为可清理;
|
||||
// getChatMember 出错(membershipUnknown)时保守跳过,避免误删。
|
||||
confirmedNo := true
|
||||
for _, chatID := range chatIDs {
|
||||
if s.telegramUserIsChatMember(ctx, channel, chatID, int(binding.TelegramUserID)) {
|
||||
member = true
|
||||
if s.telegramChatMembership(ctx, channel, chatID, int(binding.TelegramUserID)) != membershipNo {
|
||||
confirmedNo = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !member {
|
||||
if confirmedNo {
|
||||
stale = append(stale, staleBinding{User: *user, Binding: binding})
|
||||
}
|
||||
}
|
||||
@@ -634,7 +636,7 @@ func (s *TelegramBotService) telegramMembershipChatIDs(channel *model.NotifyChan
|
||||
cfg := s.telegramChannelConfig(channel)
|
||||
seen := map[string]struct{}{}
|
||||
var out []string
|
||||
for _, key := range []string{"group_chat_id", "channel_chat_id"} {
|
||||
for _, key := range []string{"group_chat_id", "channel_chat_id", "command_chat_id"} {
|
||||
value := strings.TrimSpace(cfg[key])
|
||||
if value == "" {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user