admin_panel.py 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343
  1. from __future__ import annotations
  2. import secrets
  3. from dataclasses import dataclass, field
  4. from datetime import UTC, datetime, timedelta
  5. from html import escape
  6. from typing import Any
  7. from pyrogram import filters
  8. from pyrogram.enums import ParseMode
  9. from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
  10. from wbb import app, log
  11. from wbb.services.blacklist_enforcement import add_risk_keyword, remove_risk_keyword
  12. from wbb.services.chat_management import (
  13. ChatManagementError,
  14. apply_automation_settings,
  15. ensure_permission,
  16. execute_member_action,
  17. get_automation_settings,
  18. get_chat_overview,
  19. list_accessible_chats,
  20. list_recent_members,
  21. send_announcement,
  22. update_chat_permissions,
  23. )
  24. from wbb.services.giveaways import (
  25. cancel_and_refund_giveaway,
  26. create_and_publish_giveaway,
  27. finish_and_publish_giveaway,
  28. )
  29. from wbb.services.member_identity import display_name
  30. from wbb.services.point_settings import apply_point_rules
  31. from wbb.utils.dbadmin import record_audit
  32. from wbb.utils.dbgiveaway import get_giveaway, list_running_giveaways
  33. from wbb.utils.dbpoints import (
  34. SOURCE_ADMIN,
  35. adjust_points,
  36. get_point_account,
  37. get_point_rules,
  38. list_point_accounts,
  39. list_point_transactions,
  40. set_points,
  41. )
  42. from wbb.utils.i18n import (
  43. member_action_label,
  44. point_source_label,
  45. telegram_permission_label,
  46. )
  47. __MODULE__ = "私聊管理"
  48. __HELP__ = """私聊管理员菜单:
  49. /manage [群组 ID] - 打开可管理的群列表,或直接打开指定群。
  50. /cancel - 取消当前输入流程。
  51. 普通管理员只能看到自己拥有管理权限的群,超级管理员可管理全部群。"""
  52. FLOW_TTL = timedelta(minutes=10)
  53. PAGE_SIZE = 6
  54. @dataclass
  55. class PendingFlow:
  56. action: str
  57. chat_id: int
  58. step: str
  59. data: dict[str, Any] = field(default_factory=dict)
  60. expires_at: datetime = field(
  61. default_factory=lambda: datetime.now(UTC) + FLOW_TTL
  62. )
  63. _flows: dict[int, PendingFlow] = {}
  64. _confirmations: dict[str, dict[str, Any]] = {}
  65. def _button(text: str, data: str) -> InlineKeyboardButton:
  66. return InlineKeyboardButton(text, callback_data=data)
  67. def _back(chat_id: int) -> list[InlineKeyboardButton]:
  68. return [_button("返回群菜单", f"mg:c:{chat_id}")]
  69. async def _edit(query: CallbackQuery, text: str, keyboard: InlineKeyboardMarkup) -> None:
  70. await query.message.edit_text(
  71. text,
  72. parse_mode=ParseMode.HTML,
  73. reply_markup=keyboard,
  74. disable_web_page_preview=True,
  75. )
  76. def _media_from_message(message: Message) -> tuple[str | None, str | None]:
  77. for media_type in ("photo", "animation", "video", "document"):
  78. media = getattr(message, media_type, None)
  79. if media:
  80. if media_type == "photo":
  81. media = media[-1] if isinstance(media, list) else media
  82. return media_type, media.file_id
  83. return None, None
  84. async def _show_group_list(
  85. target: Message | CallbackQuery, actor_id: int, page: int = 1
  86. ) -> None:
  87. items, total = await list_accessible_chats(
  88. page=page, page_size=PAGE_SIZE, actor_id=actor_id
  89. )
  90. if not items:
  91. text = "你当前没有可管理的群。只有真实群管理员或 SUDOERS 能使用此菜单。"
  92. if isinstance(target, CallbackQuery):
  93. await target.message.edit_text(text)
  94. else:
  95. await target.reply_text(text)
  96. return
  97. rows = [
  98. [_button(str(item.get("title") or item["chat_id"])[:45], f"mg:c:{item['chat_id']}")]
  99. for item in items
  100. ]
  101. pages = max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE)
  102. navigation = []
  103. if page > 1:
  104. navigation.append(_button("上一页", f"mg:p:{page - 1}"))
  105. if page < pages:
  106. navigation.append(_button("下一页", f"mg:p:{page + 1}"))
  107. if navigation:
  108. rows.append(navigation)
  109. text = f"<b>Telegram 群管理</b>\n选择群组(第 {page}/{pages} 页):"
  110. keyboard = InlineKeyboardMarkup(rows)
  111. if isinstance(target, CallbackQuery):
  112. await _edit(target, text, keyboard)
  113. else:
  114. await target.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=keyboard)
  115. async def _show_chat_menu(query: CallbackQuery, chat_id: int) -> None:
  116. overview = await get_chat_overview(chat_id, actor_id=query.from_user.id)
  117. privileges = (
  118. "、".join(
  119. telegram_permission_label(item)
  120. for item in overview.get("bot_privileges", [])
  121. )
  122. or "无管理权限"
  123. )
  124. text = (
  125. f"<b>{escape(overview.get('title') or str(chat_id))}</b>\n"
  126. f"群 ID:<code>{chat_id}</code>\n"
  127. f"成员数:{overview.get('member_count') or '-'}\n"
  128. f"机器人权限:{escape(privileges)}"
  129. )
  130. keyboard = InlineKeyboardMarkup(
  131. [
  132. [_button("概览", f"mg:s:{chat_id}:overview"), _button("公告", f"mg:s:{chat_id}:announce")],
  133. [_button("成员管理", f"mg:s:{chat_id}:members"), _button("权限", f"mg:s:{chat_id}:permissions")],
  134. [_button("自动回复", f"mg:s:{chat_id}:autoreply"), _button("风控", f"mg:s:{chat_id}:risk")],
  135. [_button("资料监控", f"mg:s:{chat_id}:identity"), _button("积分", f"mg:s:{chat_id}:points")],
  136. [_button("抽奖", f"mg:s:{chat_id}:giveaways")],
  137. [_button("返回群列表", "mg:p:1")],
  138. ]
  139. )
  140. await _edit(query, text, keyboard)
  141. def _risk_member_action(rule: dict[str, Any]) -> str:
  142. actions = set(rule.get("actions", []))
  143. return next(
  144. (item for item in ("ban", "kick", "mute", "warn") if item in actions),
  145. "none",
  146. )
  147. def _duration_label(seconds: int) -> str:
  148. if seconds % 86400 == 0:
  149. return f"{seconds // 86400} 天"
  150. if seconds % 3600 == 0:
  151. return f"{seconds // 3600} 小时"
  152. return f"{max(1, seconds // 60)} 分钟"
  153. async def _show_risk_rule(
  154. query: CallbackQuery, chat_id: int, rule_id: str
  155. ) -> None:
  156. await get_chat_overview(chat_id, actor_id=query.from_user.id)
  157. settings = await get_automation_settings(chat_id)
  158. rule = next(
  159. (item for item in settings["risk_rules"] if item["rule_id"] == rule_id),
  160. None,
  161. )
  162. if not rule:
  163. await query.answer("风控规则不存在或已删除。", show_alert=True)
  164. return await _show_section(query, chat_id, "risk")
  165. action = _risk_member_action(rule)
  166. action_labels = {
  167. "none": "不处罚",
  168. "warn": "警告",
  169. "mute": "禁言",
  170. "kick": "踢出",
  171. "ban": "封禁",
  172. }
  173. triggers = []
  174. if rule["keywords"]:
  175. triggers.append(f"关键词 {len(rule['keywords'])} 个")
  176. if rule["match_images"]:
  177. triggers.append("图片")
  178. if rule["match_links"]:
  179. triggers.append("链接")
  180. rows = [
  181. [
  182. _button(
  183. f"规则状态:{'开' if rule['enabled'] else '关'}",
  184. f"mg:k:{chat_id}:{rule_id}:e",
  185. )
  186. ],
  187. [
  188. _button(
  189. f"图片:{'开' if rule['match_images'] else '关'}",
  190. f"mg:k:{chat_id}:{rule_id}:i",
  191. ),
  192. _button(
  193. f"链接:{'开' if rule['match_links'] else '关'}",
  194. f"mg:k:{chat_id}:{rule_id}:l",
  195. ),
  196. ],
  197. [
  198. _button(
  199. f"删除消息:{'开' if 'delete' in rule['actions'] else '关'}",
  200. f"mg:k:{chat_id}:{rule_id}:d",
  201. )
  202. ],
  203. [
  204. _button(
  205. f"{'✓ ' if action == 'none' else ''}不处罚",
  206. f"mg:k:{chat_id}:{rule_id}:n",
  207. ),
  208. _button(
  209. f"{'✓ ' if action == 'warn' else ''}警告",
  210. f"mg:k:{chat_id}:{rule_id}:w",
  211. ),
  212. ],
  213. [
  214. _button(
  215. f"{'✓ ' if action == 'mute' else ''}禁言",
  216. f"mg:k:{chat_id}:{rule_id}:m",
  217. ),
  218. _button(
  219. f"{'✓ ' if action == 'kick' else ''}踢出",
  220. f"mg:k:{chat_id}:{rule_id}:k",
  221. ),
  222. ],
  223. [
  224. _button(
  225. f"{'✓ ' if action == 'ban' else ''}封禁",
  226. f"mg:k:{chat_id}:{rule_id}:b",
  227. )
  228. ],
  229. ]
  230. if action in {"warn", "mute", "ban"}:
  231. duration_name = "禁言时长" if action == "mute" else "封禁时长"
  232. rows.append(
  233. [
  234. _button(
  235. f"{duration_name}:{_duration_label(int(rule['duration_seconds']))}",
  236. f"mg:q:{chat_id}:{rule_id}",
  237. )
  238. ]
  239. )
  240. rows.extend(
  241. [
  242. [
  243. _button("编辑规则", f"mg:e:{chat_id}:{rule_id}"),
  244. _button("删除规则", f"mg:k:{chat_id}:{rule_id}:x"),
  245. ],
  246. [_button("返回风控规则", f"mg:s:{chat_id}:risk")],
  247. ]
  248. )
  249. duration = (
  250. f"\n{'禁言' if action == 'mute' else '封禁'}时长:"
  251. f"{_duration_label(int(rule['duration_seconds']))}"
  252. if action in {"warn", "mute", "ban"}
  253. else ""
  254. )
  255. text = (
  256. f"<b>{escape(rule['name'])}</b>\n"
  257. f"状态:{'已开启' if rule['enabled'] else '已关闭'}\n"
  258. f"触发条件:{escape('、'.join(triggers) or '未配置')}\n"
  259. f"消息处置:{'删除' if 'delete' in rule['actions'] else '保留'}\n"
  260. f"成员处置:{action_labels[action]}{duration}"
  261. )
  262. await _edit(query, text, InlineKeyboardMarkup(rows))
  263. async def _show_section(query: CallbackQuery, chat_id: int, section: str) -> None:
  264. await get_chat_overview(chat_id, actor_id=query.from_user.id)
  265. if section == "overview":
  266. return await _show_chat_menu(query, chat_id)
  267. if section == "announce":
  268. _flows[query.from_user.id] = PendingFlow("announcement", chat_id, "content")
  269. return await _edit(
  270. query,
  271. "<b>发送公告</b>\n请发送公告文字、图片、GIF、视频或文件。发送 /cancel 取消。",
  272. InlineKeyboardMarkup([_back(chat_id)]),
  273. )
  274. if section == "members":
  275. rows = [
  276. [_button("最近活跃成员", f"mg:s:{chat_id}:recent_members")],
  277. [_button("警告", f"mg:a:{chat_id}:warn"), _button("封禁", f"mg:a:{chat_id}:ban")],
  278. [_button("解封", f"mg:a:{chat_id}:unban"), _button("踢出", f"mg:a:{chat_id}:kick")],
  279. [_button("禁言", f"mg:a:{chat_id}:mute"), _button("解除禁言", f"mg:a:{chat_id}:unmute")],
  280. [_button("提升管理员", f"mg:a:{chat_id}:promote"), _button("降级管理员", f"mg:a:{chat_id}:demote")],
  281. [_back(chat_id)[0]],
  282. ]
  283. return await _edit(
  284. query,
  285. "<b>成员管理</b>\n可从最近活跃成员中选择,也可输入用户 ID 或 @username。",
  286. InlineKeyboardMarkup(rows),
  287. )
  288. if section == "recent_members":
  289. members = await list_recent_members(chat_id, limit=12)
  290. rows = []
  291. for member in members:
  292. user = member["user"]
  293. name = " ".join(
  294. item for item in (user.get("first_name"), user.get("last_name")) if item
  295. )
  296. label = name or (f"@{user['username']}" if user.get("username") else user["id"])
  297. rows.append([_button(label[:45], f"mg:u:{chat_id}:{user['id']}")])
  298. rows.append([_button("返回成员管理", f"mg:s:{chat_id}:members")])
  299. return await _edit(
  300. query,
  301. "<b>最近活跃成员</b>\n列表来自 Telegram 当前结果和机器人近期看到的消息。"
  302. if members
  303. else "<b>最近活跃成员</b>\n暂无记录。",
  304. InlineKeyboardMarkup(rows),
  305. )
  306. if section == "permissions":
  307. return await _edit(
  308. query,
  309. "<b>群权限</b>\n选择预设;执行前仍会检查你和机器人的限制成员权限。",
  310. InlineKeyboardMarkup(
  311. [
  312. [_button("正常发言", f"mg:d:{chat_id}:permissions:normal")],
  313. [_button("全员只读", f"mg:d:{chat_id}:permissions:readonly")],
  314. [_back(chat_id)[0]],
  315. ]
  316. ),
  317. )
  318. if section == "autoreply":
  319. settings = await get_automation_settings(chat_id)
  320. return await _edit(
  321. query,
  322. f"<b>自动回复</b>\n面板规则数:{len(settings.get('auto_replies', []))}",
  323. InlineKeyboardMarkup(
  324. [
  325. [_button("新增规则", f"mg:a:{chat_id}:autoreply")],
  326. [_button("清空面板规则", f"mg:d:{chat_id}:autoreply:clear")],
  327. [_back(chat_id)[0]],
  328. ]
  329. ),
  330. )
  331. if section == "risk":
  332. settings = await get_automation_settings(chat_id)
  333. rules = settings["risk_rules"]
  334. rows = [
  335. [
  336. _button(
  337. f"{'开' if rule['enabled'] else '关'} · {rule['name']}"[:45],
  338. f"mg:r:{chat_id}:{rule['rule_id']}",
  339. )
  340. ]
  341. for rule in rules
  342. ]
  343. rows.append([_button("新增风控规则", f"mg:n:{chat_id}")])
  344. for key, label in (
  345. ("captcha_enabled", "入群验证"),
  346. ("antiflood_enabled", "防刷屏"),
  347. ("chatbot_enabled", "聊天机器人"),
  348. ):
  349. state = bool(settings.get(key))
  350. rows.append(
  351. [_button(f"{label}:{'开' if state else '关'}", f"mg:d:{chat_id}:toggle:{key}")]
  352. )
  353. rows.append(_back(chat_id))
  354. text = (
  355. f"<b>内容风控规则</b>\n共 {len(rules)} 条,"
  356. f"已启用 {sum(1 for rule in rules if rule['enabled'])} 条。"
  357. )
  358. return await _edit(query, text, InlineKeyboardMarkup(rows))
  359. if section == "identity":
  360. settings = await get_automation_settings(chat_id)
  361. monitor = settings["identity_monitor"]
  362. return await _edit(
  363. query,
  364. (
  365. "<b>成员资料监控</b>\n"
  366. f"监控状态:{'已开启' if monitor['enabled'] else '已关闭'}\n"
  367. f"群内提醒:{'已开启' if monitor['notify_in_chat'] else '已关闭'}\n"
  368. "机器人会在成员发言、入群或产生其他可见事件时比对昵称和用户名。"
  369. ),
  370. InlineKeyboardMarkup(
  371. [
  372. [
  373. _button(
  374. f"资料监控:{'开' if monitor['enabled'] else '关'}",
  375. f"mg:d:{chat_id}:identity:enabled",
  376. )
  377. ],
  378. [
  379. _button(
  380. f"群内提醒:{'开' if monitor['notify_in_chat'] else '关'}",
  381. f"mg:d:{chat_id}:identity:notify_in_chat",
  382. )
  383. ],
  384. [_back(chat_id)[0]],
  385. ]
  386. ),
  387. )
  388. if section == "points":
  389. rules = await get_point_rules(chat_id)
  390. accounts, _ = await list_point_accounts(chat_id=chat_id, page=1, page_size=5)
  391. lines = [
  392. f"<b>积分管理</b>\n状态:{'已开启' if rules['enabled'] else '已关闭'}",
  393. "自动规则:签到 {checkin} · 活跃 {activity} · 点赞 {upvote}".format(
  394. checkin="开" if rules["checkin_enabled"] else "关",
  395. activity="开" if rules["activity_enabled"] else "关",
  396. upvote="开" if rules["upvote_enabled"] else "关",
  397. ),
  398. f"底部签到按钮:{'开' if rules['checkin_button_enabled'] else '关'}",
  399. ]
  400. for index, account in enumerate(accounts, 1):
  401. label = (
  402. account.get("display_name")
  403. or account.get("first_name")
  404. or (f"@{account['username']}" if account.get("username") else None)
  405. or account["user_id"]
  406. )
  407. lines.append(f"{index}. {escape(str(label))}:{account['balance']}")
  408. return await _edit(
  409. query,
  410. "\n".join(lines),
  411. InlineKeyboardMarkup(
  412. [
  413. [_button("查询成员", f"mg:a:{chat_id}:points_query"), _button("最近流水", f"mg:s:{chat_id}:point_history")],
  414. [_button("增加", f"mg:a:{chat_id}:points_add"), _button("扣减", f"mg:a:{chat_id}:points_deduct")],
  415. [_button("设置余额", f"mg:a:{chat_id}:points_set")],
  416. [_button("开关积分系统", f"mg:d:{chat_id}:pointtoggle:enabled")],
  417. [
  418. _button("签到开关", f"mg:d:{chat_id}:pointtoggle:checkin_enabled"),
  419. _button("活跃开关", f"mg:d:{chat_id}:pointtoggle:activity_enabled"),
  420. ],
  421. [
  422. _button("点赞开关", f"mg:d:{chat_id}:pointtoggle:upvote_enabled"),
  423. _button("底部签到按钮", f"mg:d:{chat_id}:pointtoggle:checkin_button_enabled"),
  424. ],
  425. [_back(chat_id)[0]],
  426. ]
  427. ),
  428. )
  429. if section == "point_history":
  430. transactions, _ = await list_point_transactions(
  431. chat_id=chat_id, page=1, page_size=10
  432. )
  433. lines = ["<b>最近积分流水</b>"]
  434. if not transactions:
  435. lines.append("暂无流水。")
  436. for item in transactions:
  437. delta = int(item["delta"])
  438. lines.append(
  439. f"<code>{item['user_id']}</code> · {'+' if delta > 0 else ''}{delta} · "
  440. f"{escape(point_source_label(item['source']))} · 余额 {item['balance_after']}"
  441. )
  442. return await _edit(
  443. query,
  444. "\n".join(lines),
  445. InlineKeyboardMarkup(
  446. [[_button("返回积分", f"mg:s:{chat_id}:points")], _back(chat_id)]
  447. ),
  448. )
  449. if section == "giveaways":
  450. giveaways = await list_running_giveaways(chat_id=chat_id, limit=5)
  451. lines = ["<b>抽奖管理</b>"]
  452. rows = [[_button("创建抽奖", f"mg:a:{chat_id}:giveaway_create")]]
  453. for giveaway in giveaways:
  454. lines.append(f"#{giveaway['giveaway_id']} · {escape(giveaway['title'])}")
  455. rows.append(
  456. [
  457. _button("立即开奖", f"mg:d:{chat_id}:gfinish:{giveaway['giveaway_id']}"),
  458. _button("取消", f"mg:d:{chat_id}:gcancel:{giveaway['giveaway_id']}"),
  459. ]
  460. )
  461. rows.append(_back(chat_id))
  462. return await _edit(query, "\n".join(lines), InlineKeyboardMarkup(rows))
  463. async def _start_flow(query: CallbackQuery, chat_id: int, action: str) -> None:
  464. if action == "points_query":
  465. await get_chat_overview(chat_id, actor_id=query.from_user.id)
  466. else:
  467. permission = (
  468. "can_promote_members"
  469. if action in {"promote", "demote"}
  470. else "can_restrict_members"
  471. )
  472. if action.startswith("points_") or action in {
  473. "autoreply",
  474. "giveaway_create",
  475. "risk_keyword_add",
  476. "risk_keyword_remove",
  477. }:
  478. permission = "can_change_info"
  479. await ensure_permission(chat_id, permission, actor_id=query.from_user.id)
  480. if action in {"warn", "ban", "unban", "kick", "mute", "unmute", "promote", "demote"}:
  481. _flows[query.from_user.id] = PendingFlow(action, chat_id, "target")
  482. text = f"<b>{member_action_label(action)}</b>\n请输入用户 ID 或 @username。"
  483. elif action.startswith("points_"):
  484. _flows[query.from_user.id] = PendingFlow(action, chat_id, "target")
  485. text = "请输入成员 ID 或 @username。"
  486. elif action == "autoreply":
  487. _flows[query.from_user.id] = PendingFlow(action, chat_id, "keyword")
  488. text = "请输入自动回复关键词。"
  489. elif action in {"risk_keyword_add", "risk_keyword_remove"}:
  490. _flows[query.from_user.id] = PendingFlow(action, chat_id, "keyword")
  491. text = "请输入要新增的风控关键词。" if action.endswith("add") else "请输入要移除的风控关键词。"
  492. elif action == "giveaway_create":
  493. _flows[query.from_user.id] = PendingFlow(action, chat_id, "title")
  494. text = "请输入抽奖标题。"
  495. else:
  496. return
  497. await _edit(
  498. query,
  499. text + "\n流程 10 分钟无操作自动失效;发送 /cancel 取消。",
  500. InlineKeyboardMarkup([_back(chat_id)]),
  501. )
  502. async def _start_risk_rule_flow(
  503. query: CallbackQuery,
  504. chat_id: int,
  505. action: str,
  506. rule_id: str | None = None,
  507. ) -> None:
  508. await ensure_permission(chat_id, "can_change_info", actor_id=query.from_user.id)
  509. data: dict[str, Any] = {}
  510. if rule_id:
  511. settings = await get_automation_settings(chat_id)
  512. rule = next(
  513. (item for item in settings["risk_rules"] if item["rule_id"] == rule_id),
  514. None,
  515. )
  516. if not rule:
  517. return await query.answer("风控规则不存在或已删除。", show_alert=True)
  518. data.update(rule)
  519. else:
  520. data.update(
  521. {
  522. "rule_id": secrets.token_hex(8),
  523. "enabled": True,
  524. "keywords": [],
  525. "match_images": False,
  526. "match_links": False,
  527. "actions": ["delete", "warn"],
  528. "duration_seconds": 3600,
  529. }
  530. )
  531. if action == "risk_rule_duration":
  532. flow = PendingFlow(action, chat_id, "ban_duration", data)
  533. prompt = "请输入封禁分钟数,范围 1 到 525600。"
  534. else:
  535. flow = PendingFlow(action, chat_id, "risk_name", data)
  536. prompt = "请输入规则名称。"
  537. _flows[query.from_user.id] = flow
  538. await _edit(
  539. query,
  540. prompt + "\n流程 10 分钟无操作自动失效;发送 /cancel 取消。",
  541. InlineKeyboardMarkup(
  542. [[_button("返回规则", f"mg:r:{chat_id}:{data['rule_id']}")]]
  543. if rule_id
  544. else [[_button("返回风控规则", f"mg:s:{chat_id}:risk")]]
  545. ),
  546. )
  547. async def _show_member_target(
  548. query: CallbackQuery,
  549. chat_id: int,
  550. user_id: int,
  551. ) -> None:
  552. await get_chat_overview(chat_id, actor_id=query.from_user.id)
  553. try:
  554. member = await app.get_chat_member(chat_id, user_id)
  555. except Exception:
  556. return await query.answer("该成员当前已不在群内。", show_alert=True)
  557. user = member.user
  558. name = " ".join(
  559. item for item in (user.first_name, user.last_name) if item
  560. ) or (f"@{user.username}" if user.username else str(user.id))
  561. rows = [
  562. [_button("警告", f"mg:v:{chat_id}:{user_id}:warn"), _button("封禁", f"mg:v:{chat_id}:{user_id}:ban")],
  563. [_button("踢出", f"mg:v:{chat_id}:{user_id}:kick"), _button("禁言", f"mg:v:{chat_id}:{user_id}:mute")],
  564. [_button("解除禁言", f"mg:v:{chat_id}:{user_id}:unmute"), _button("提升管理员", f"mg:v:{chat_id}:{user_id}:promote")],
  565. [_button("返回最近活跃", f"mg:s:{chat_id}:recent_members")],
  566. ]
  567. await _edit(
  568. query,
  569. f"<b>{escape(name)}</b>\n用户 ID:<code>{user.id}</code>",
  570. InlineKeyboardMarkup(rows),
  571. )
  572. async def _start_flow_for_member(
  573. query: CallbackQuery,
  574. chat_id: int,
  575. user_id: int,
  576. action: str,
  577. ) -> None:
  578. permission = "can_promote_members" if action in {"promote", "demote"} else "can_restrict_members"
  579. await ensure_permission(chat_id, permission, actor_id=query.from_user.id)
  580. flow = PendingFlow(action, chat_id, "reason", {"user_id": user_id})
  581. if action == "mute":
  582. flow.step = "duration"
  583. text = "请输入禁言分钟数;输入 0 表示永久禁言。"
  584. else:
  585. text = "请输入操作原因。"
  586. _flows[query.from_user.id] = flow
  587. await _edit(
  588. query,
  589. text + "\n流程 10 分钟无操作自动失效;发送 /cancel 取消。",
  590. InlineKeyboardMarkup([[_button("返回成员", f"mg:u:{chat_id}:{user_id}")]]),
  591. )
  592. async def _resolve_user(raw: str) -> Any | None:
  593. try:
  594. user = await app.get_users(int(raw) if raw.lstrip("-").isdigit() else raw)
  595. except Exception:
  596. return None
  597. return user
  598. async def _queue_confirmation(message: Message, flow: PendingFlow) -> None:
  599. token = secrets.token_urlsafe(8)
  600. _confirmations[token] = {
  601. "actor_id": message.from_user.id,
  602. "flow": flow,
  603. "expires_at": datetime.now(UTC) + FLOW_TTL,
  604. }
  605. _flows.pop(message.from_user.id, None)
  606. await message.reply_text(
  607. "请确认执行该操作。确认时会再次检查 Telegram 权限。",
  608. reply_markup=InlineKeyboardMarkup(
  609. [[_button("确认执行", f"mg:x:{token}"), _button("取消", f"mg:z:{token}")]]
  610. ),
  611. )
  612. async def _handle_flow_input(message: Message, flow: PendingFlow) -> None:
  613. text = (message.text or message.caption or "").strip()
  614. flow.expires_at = datetime.now(UTC) + FLOW_TTL
  615. if flow.action in {"risk_rule_add", "risk_rule_edit", "risk_rule_duration"}:
  616. if flow.step == "risk_name":
  617. if not text:
  618. return await message.reply_text("规则名称不能为空。")
  619. flow.data["name"] = text[:60]
  620. flow.step = "risk_keywords"
  621. return await message.reply_text("请输入关键词,用逗号分隔;输入“无”表示不配置关键词。")
  622. if flow.step == "risk_keywords":
  623. pieces = [] if text in {"无", "none", "0"} else text.replace(",", ",").replace("\n", ",").split(",")
  624. flow.data["keywords"] = [item.strip()[:100] for item in pieces if item.strip()][:200]
  625. flow.step = "risk_triggers"
  626. return await message.reply_text("请输入其他触发条件:图片、链接、图片+链接或无。")
  627. if flow.step == "risk_triggers":
  628. normalized = text.lower().replace(" ", "").replace("、", "+").replace(",", "+")
  629. if normalized in {"无", "none", "0"}:
  630. match_images = match_links = False
  631. else:
  632. match_images = "图片" in normalized or "image" in normalized
  633. match_links = "链接" in normalized or "link" in normalized
  634. if not match_images and not match_links:
  635. return await message.reply_text("请输入图片、链接、图片+链接或无。")
  636. flow.data["match_images"] = match_images
  637. flow.data["match_links"] = match_links
  638. flow.step = "risk_delete"
  639. return await message.reply_text("命中后是否删除消息?请输入是或否。")
  640. if flow.step == "risk_delete":
  641. normalized = text.lower()
  642. if normalized not in {"是", "否", "yes", "no", "1", "0"}:
  643. return await message.reply_text("请输入是或否。")
  644. flow.data["delete_message"] = normalized in {"是", "yes", "1"}
  645. flow.step = "risk_member_action"
  646. return await message.reply_text("请输入成员处置:不处罚、警告、禁言、踢出或封禁。")
  647. if flow.step == "risk_member_action":
  648. action_map = {
  649. "不处罚": "none",
  650. "无": "none",
  651. "none": "none",
  652. "警告": "warn",
  653. "warn": "warn",
  654. "禁言": "mute",
  655. "mute": "mute",
  656. "踢出": "kick",
  657. "kick": "kick",
  658. "封禁": "ban",
  659. "ban": "ban",
  660. }
  661. member_action = action_map.get(text.lower())
  662. if not member_action:
  663. return await message.reply_text("请输入不处罚、警告、禁言、踢出或封禁。")
  664. actions = ["delete"] if flow.data.pop("delete_message", False) else []
  665. if member_action != "none":
  666. actions.append(member_action)
  667. flow.data["actions"] = actions
  668. if member_action in {"warn", "mute", "ban"}:
  669. flow.step = "ban_duration"
  670. duration_name = "禁言" if member_action == "mute" else "封禁"
  671. return await message.reply_text(
  672. f"请输入{duration_name}分钟数,范围 1 到 525600。"
  673. )
  674. return await _queue_confirmation(message, flow)
  675. if flow.step == "ban_duration":
  676. if not text.isdigit() or not 1 <= int(text) <= 525600:
  677. return await message.reply_text("请输入 1 到 525600 之间的整数分钟数。")
  678. flow.data["duration_seconds"] = int(text) * 60
  679. return await _queue_confirmation(message, flow)
  680. if flow.step == "target":
  681. target = await _resolve_user(text)
  682. if not target:
  683. return await message.reply_text("未找到用户,请重新输入用户 ID 或 @username。")
  684. user_id = target.id
  685. flow.data["user_id"] = user_id
  686. flow.data["username"] = target.username
  687. flow.data["first_name"] = target.first_name
  688. flow.data["display_name"] = display_name(target.first_name, target.last_name)
  689. if flow.action == "mute":
  690. flow.step = "duration"
  691. return await message.reply_text("请输入禁言分钟数;输入 0 表示永久禁言。")
  692. if flow.action == "points_query":
  693. await get_chat_overview(flow.chat_id, actor_id=message.from_user.id)
  694. account = await get_point_account(flow.chat_id, user_id)
  695. transactions, _ = await list_point_transactions(
  696. chat_id=flow.chat_id, user_id=user_id, page=1, page_size=5
  697. )
  698. _flows.pop(message.from_user.id, None)
  699. label = (
  700. flow.data.get("display_name")
  701. or (f"@{flow.data['username']}" if flow.data.get("username") else None)
  702. or str(user_id)
  703. )
  704. lines = [
  705. f"成员 {escape(str(label))}(<code>{user_id}</code>)当前积分:<b>{account['balance']}</b>",
  706. "最近流水:",
  707. ]
  708. if not transactions:
  709. lines.append("暂无流水。")
  710. for item in transactions:
  711. delta = int(item["delta"])
  712. lines.append(
  713. f"{'+' if delta > 0 else ''}{delta} · "
  714. f"{escape(point_source_label(item['source']))} · "
  715. f"余额 {item['balance_after']}"
  716. )
  717. return await message.reply_text(
  718. "\n".join(lines),
  719. parse_mode=ParseMode.HTML,
  720. reply_markup=InlineKeyboardMarkup(
  721. [[_button("返回积分菜单", f"mg:s:{flow.chat_id}:points")]]
  722. ),
  723. )
  724. if flow.action.startswith("points_"):
  725. flow.step = "amount"
  726. return await message.reply_text("请输入非负积分数值。")
  727. flow.step = "reason"
  728. return await message.reply_text("请输入操作原因。")
  729. if flow.step == "duration":
  730. if not text.isdigit():
  731. return await message.reply_text("请输入整数分钟数。")
  732. flow.data["duration_seconds"] = int(text) * 60 or None
  733. flow.step = "reason"
  734. return await message.reply_text("请输入禁言原因。")
  735. if flow.step == "amount":
  736. if not text.isdigit():
  737. return await message.reply_text("请输入非负整数积分。")
  738. flow.data["amount"] = int(text)
  739. flow.step = "reason"
  740. return await message.reply_text("请输入积分调整原因(必填)。")
  741. if flow.step == "reason":
  742. if not text:
  743. return await message.reply_text("原因不能为空。")
  744. flow.data["reason"] = text[:500]
  745. return await _queue_confirmation(message, flow)
  746. if flow.action == "announcement" and flow.step == "content":
  747. media_type, file_id = _media_from_message(message)
  748. flow.data.update({"text": text, "media_type": media_type, "file_id": file_id})
  749. return await _queue_confirmation(message, flow)
  750. if flow.action in {"risk_keyword_add", "risk_keyword_remove"}:
  751. if not text:
  752. return await message.reply_text("关键词不能为空。")
  753. flow.data["keyword"] = text[:100]
  754. return await _queue_confirmation(message, flow)
  755. if flow.action == "autoreply":
  756. if flow.step == "keyword":
  757. if not text:
  758. return await message.reply_text("关键词不能为空。")
  759. flow.data["keyword"] = text[:100]
  760. flow.step = "reply"
  761. return await message.reply_text("请发送回复文字或媒体。")
  762. media_type, file_id = _media_from_message(message)
  763. flow.data.update(
  764. {"type": media_type or "text", "file_id": file_id, "text": text[:4000]}
  765. )
  766. return await _queue_confirmation(message, flow)
  767. if flow.action == "giveaway_create":
  768. prompts = {
  769. "title": ("duration", "请输入持续分钟数。"),
  770. "duration": ("prizes", "请输入奖项,如 一等奖:1:50, 二等奖:3:10。"),
  771. "prizes": ("minimum_points", "请输入最低积分,0 表示不限制。"),
  772. "minimum_points": ("entry_cost", "请输入报名消耗积分,0 表示免费。"),
  773. "entry_cost": ("participation_reward", "请输入参与奖励积分,0 表示无奖励。"),
  774. }
  775. if flow.step == "title":
  776. flow.data["title"] = text[:200]
  777. elif flow.step == "duration":
  778. if not text.isdigit() or int(text) <= 0:
  779. return await message.reply_text("请输入大于 0 的整数分钟数。")
  780. flow.data["duration"] = int(text)
  781. elif flow.step == "prizes":
  782. prizes = []
  783. for piece in text.split(","):
  784. parts = [part.strip() for part in piece.split(":")]
  785. if len(parts) not in {2, 3} or not parts[1].isdigit() or (len(parts) == 3 and not parts[2].isdigit()):
  786. return await message.reply_text("奖项格式不正确,请重新输入。")
  787. prizes.append(
  788. {
  789. "name": parts[0],
  790. "count": int(parts[1]),
  791. "points_reward": int(parts[2]) if len(parts) == 3 else 0,
  792. }
  793. )
  794. flow.data["prizes"] = prizes
  795. elif flow.step in {"minimum_points", "entry_cost", "participation_reward"}:
  796. if not text.isdigit():
  797. return await message.reply_text("请输入非负整数。")
  798. flow.data[flow.step] = int(text)
  799. if flow.step == "participation_reward":
  800. return await _queue_confirmation(message, flow)
  801. next_step, prompt = prompts[flow.step]
  802. flow.step = next_step
  803. return await message.reply_text(prompt)
  804. async def _execute_confirmation(query: CallbackQuery, token: str) -> None:
  805. confirmation = _confirmations.pop(token, None)
  806. if not confirmation or confirmation["actor_id"] != query.from_user.id:
  807. return await query.answer("确认已失效。", show_alert=True)
  808. if confirmation["expires_at"] < datetime.now(UTC):
  809. return await query.answer("确认已过期。", show_alert=True)
  810. flow: PendingFlow = confirmation["flow"]
  811. actor_id = query.from_user.id
  812. action = flow.action
  813. data = flow.data
  814. if action.startswith("points_") or action.startswith("risk_rule_") or action in {
  815. "announcement",
  816. "giveaway_create",
  817. "risk_keyword_add",
  818. "risk_keyword_remove",
  819. }:
  820. await ensure_permission(flow.chat_id, "can_change_info", actor_id=actor_id)
  821. if action in {"warn", "ban", "unban", "kick", "mute", "unmute", "promote", "demote"}:
  822. result = await execute_member_action(
  823. flow.chat_id,
  824. user_id=int(data["user_id"]),
  825. action=action,
  826. reason=data["reason"],
  827. duration_seconds=data.get("duration_seconds"),
  828. privileges={
  829. "can_delete_messages": True,
  830. "can_restrict_members": True,
  831. "can_invite_users": True,
  832. }
  833. if action == "promote"
  834. else None,
  835. actor_id=actor_id,
  836. )
  837. summary = str(result)
  838. elif action == "announcement":
  839. result = await send_announcement(
  840. flow.chat_id,
  841. text=data["text"],
  842. media_type=data.get("media_type"),
  843. file_id=data.get("file_id"),
  844. actor_id=actor_id,
  845. )
  846. summary = f"消息编号={result['message_id']}"
  847. elif action.startswith("points_"):
  848. operation = action.removeprefix("points_")
  849. amount = int(data["amount"])
  850. key = f"private-points:{flow.chat_id}:{actor_id}:{token}"
  851. if operation == "set":
  852. account, _ = await set_points(
  853. chat_id=flow.chat_id,
  854. user_id=int(data["user_id"]),
  855. balance=amount,
  856. actor_id=actor_id,
  857. reason=data["reason"],
  858. idempotency_key=key,
  859. username=data.get("username"),
  860. first_name=data.get("first_name"),
  861. display_name=data.get("display_name"),
  862. )
  863. else:
  864. account, _ = await adjust_points(
  865. chat_id=flow.chat_id,
  866. user_id=int(data["user_id"]),
  867. delta=amount if operation == "add" else -amount,
  868. source=SOURCE_ADMIN,
  869. idempotency_key=key,
  870. actor_id=actor_id,
  871. reason=data["reason"],
  872. username=data.get("username"),
  873. first_name=data.get("first_name"),
  874. display_name=data.get("display_name"),
  875. )
  876. summary = f"余额={account['balance']}"
  877. elif action == "autoreply":
  878. settings = await get_automation_settings(flow.chat_id)
  879. rules = list(settings.get("auto_replies", []))
  880. rules = [item for item in rules if item.get("keyword") != data["keyword"]]
  881. rules.append(
  882. {
  883. "keyword": data["keyword"],
  884. "type": data["type"],
  885. "text": data["text"],
  886. "file_id": data["file_id"],
  887. }
  888. )
  889. await apply_automation_settings(
  890. flow.chat_id, {"auto_replies": rules}, actor_id=actor_id
  891. )
  892. summary = f"关键词={data['keyword']}"
  893. elif action in {"risk_keyword_add", "risk_keyword_remove"}:
  894. keyword = data["keyword"].strip()
  895. if action == "risk_keyword_add":
  896. await add_risk_keyword(flow.chat_id, keyword)
  897. else:
  898. await remove_risk_keyword(flow.chat_id, keyword)
  899. summary = f"风控关键词={keyword}"
  900. elif action in {"risk_rule_add", "risk_rule_edit", "risk_rule_duration"}:
  901. settings = await get_automation_settings(flow.chat_id)
  902. rules = [dict(item) for item in settings["risk_rules"]]
  903. rule_id = data["rule_id"]
  904. index = next(
  905. (i for i, item in enumerate(rules) if item["rule_id"] == rule_id),
  906. None,
  907. )
  908. if action == "risk_rule_add":
  909. if index is not None:
  910. return await query.answer("规则编号冲突,请重新新增。", show_alert=True)
  911. rules.append(dict(data))
  912. elif index is None:
  913. return await query.answer("风控规则不存在或已删除。", show_alert=True)
  914. elif action == "risk_rule_duration":
  915. rules[index]["duration_seconds"] = data["duration_seconds"]
  916. else:
  917. rules[index] = dict(data)
  918. await apply_automation_settings(
  919. flow.chat_id,
  920. {"risk_rules": rules},
  921. actor_id=actor_id,
  922. )
  923. summary = f"风控规则={rule_id}"
  924. elif action == "giveaway_create":
  925. giveaway = await create_and_publish_giveaway(
  926. chat_id=flow.chat_id,
  927. creator_id=actor_id,
  928. creator_name=query.from_user.first_name,
  929. title=data["title"],
  930. description="",
  931. prizes=data["prizes"],
  932. ends_at=datetime.now(UTC) + timedelta(minutes=data["duration"]),
  933. minimum_points=data["minimum_points"],
  934. entry_cost=data["entry_cost"],
  935. participation_reward=data["participation_reward"],
  936. )
  937. summary = f"抽奖编号={giveaway['giveaway_id']}"
  938. else:
  939. return await query.answer("未知操作。", show_alert=True)
  940. await record_audit(
  941. source="telegram_private",
  942. actor_id=actor_id,
  943. actor_name=query.from_user.first_name,
  944. action=action,
  945. chat_id=flow.chat_id,
  946. target_id=data.get("user_id"),
  947. summary=summary,
  948. )
  949. await query.answer("操作成功。", show_alert=True)
  950. if action.startswith("risk_rule_"):
  951. await _show_section(query, flow.chat_id, "risk")
  952. else:
  953. await _show_chat_menu(query, flow.chat_id)
  954. async def _danger_action(query: CallbackQuery, parts: list[str]) -> None:
  955. chat_id = int(parts[2])
  956. category = parts[3]
  957. value = parts[4] if len(parts) > 4 else ""
  958. await get_chat_overview(chat_id, actor_id=query.from_user.id)
  959. if category == "permissions":
  960. payload = {"action": category, "chat_id": chat_id, "mode": value}
  961. elif category == "toggle":
  962. payload = {"action": category, "chat_id": chat_id, "key": value}
  963. elif category == "pointtoggle":
  964. payload = {"action": "point_rule_toggle", "chat_id": chat_id, "key": value}
  965. elif category == "identity":
  966. payload = {"action": "identity_monitor_toggle", "chat_id": chat_id, "key": value}
  967. elif category in {"gfinish", "gcancel"}:
  968. payload = {"action": category, "chat_id": chat_id, "giveaway_id": value}
  969. elif category == "autoreply":
  970. payload = {"action": "autoreply_clear", "chat_id": chat_id}
  971. else:
  972. return
  973. token = secrets.token_urlsafe(8)
  974. _confirmations[token] = {
  975. "actor_id": query.from_user.id,
  976. "direct": payload,
  977. "expires_at": datetime.now(UTC) + FLOW_TTL,
  978. }
  979. await _edit(
  980. query,
  981. "该操作会立即影响群组,请确认。",
  982. InlineKeyboardMarkup(
  983. [[_button("确认执行", f"mg:y:{token}"), _button("取消", f"mg:z:{token}")]]
  984. ),
  985. )
  986. async def _start_risk_rule_direct(
  987. query: CallbackQuery, chat_id: int, rule_id: str, operation: str
  988. ) -> None:
  989. await ensure_permission(chat_id, "can_change_info", actor_id=query.from_user.id)
  990. settings = await get_automation_settings(chat_id)
  991. rule = next(
  992. (item for item in settings["risk_rules"] if item["rule_id"] == rule_id),
  993. None,
  994. )
  995. if not rule:
  996. return await query.answer("风控规则不存在或已删除。", show_alert=True)
  997. if operation in {"e", "i", "l", "d"}:
  998. payload = {
  999. "action": "risk_rule_toggle",
  1000. "chat_id": chat_id,
  1001. "rule_id": rule_id,
  1002. "key": {
  1003. "e": "enabled",
  1004. "i": "match_images",
  1005. "l": "match_links",
  1006. "d": "delete",
  1007. }[operation],
  1008. }
  1009. elif operation in {"n", "w", "m", "k", "b"}:
  1010. payload = {
  1011. "action": "risk_rule_action",
  1012. "chat_id": chat_id,
  1013. "rule_id": rule_id,
  1014. "key": {
  1015. "n": "none",
  1016. "w": "warn",
  1017. "m": "mute",
  1018. "k": "kick",
  1019. "b": "ban",
  1020. }[operation],
  1021. }
  1022. elif operation == "x":
  1023. payload = {
  1024. "action": "risk_rule_delete",
  1025. "chat_id": chat_id,
  1026. "rule_id": rule_id,
  1027. }
  1028. else:
  1029. return await query.answer("不支持的风控规则操作。", show_alert=True)
  1030. token = secrets.token_urlsafe(8)
  1031. _confirmations[token] = {
  1032. "actor_id": query.from_user.id,
  1033. "direct": payload,
  1034. "expires_at": datetime.now(UTC) + FLOW_TTL,
  1035. }
  1036. await _edit(
  1037. query,
  1038. f"即将修改风控规则 <b>{escape(rule['name'])}</b>,请确认。",
  1039. InlineKeyboardMarkup(
  1040. [[_button("确认执行", f"mg:y:{token}"), _button("取消", f"mg:z:{token}")]]
  1041. ),
  1042. )
  1043. async def _execute_direct_confirmation(query: CallbackQuery, token: str) -> None:
  1044. confirmation = _confirmations.pop(token, None)
  1045. if not confirmation or confirmation.get("actor_id") != query.from_user.id:
  1046. return await query.answer("确认已失效。", show_alert=True)
  1047. if confirmation["expires_at"] < datetime.now(UTC):
  1048. return await query.answer("确认已过期。", show_alert=True)
  1049. payload = confirmation["direct"]
  1050. chat_id = int(payload["chat_id"])
  1051. action = payload["action"]
  1052. if action == "permissions":
  1053. readonly = payload["mode"] == "readonly"
  1054. await update_chat_permissions(
  1055. chat_id,
  1056. {
  1057. "can_send_messages": not readonly,
  1058. "can_send_media_messages": not readonly,
  1059. "can_send_other_messages": not readonly,
  1060. "can_send_polls": not readonly,
  1061. "can_add_web_page_previews": not readonly,
  1062. "can_invite_users": not readonly,
  1063. },
  1064. actor_id=query.from_user.id,
  1065. )
  1066. elif action == "toggle":
  1067. key = payload["key"]
  1068. if key == "points":
  1069. rules = await get_point_rules(chat_id)
  1070. await ensure_permission(chat_id, "can_change_info", actor_id=query.from_user.id)
  1071. await apply_point_rules(chat_id, {"enabled": not rules["enabled"]})
  1072. else:
  1073. settings = await get_automation_settings(chat_id)
  1074. await apply_automation_settings(
  1075. chat_id, {key: not bool(settings.get(key))}, actor_id=query.from_user.id
  1076. )
  1077. elif action == "point_rule_toggle":
  1078. key = payload["key"]
  1079. allowed = {
  1080. "enabled",
  1081. "checkin_enabled",
  1082. "checkin_button_enabled",
  1083. "activity_enabled",
  1084. "upvote_enabled",
  1085. }
  1086. if key not in allowed:
  1087. return await query.answer("不支持的积分规则。", show_alert=True)
  1088. await ensure_permission(chat_id, "can_change_info", actor_id=query.from_user.id)
  1089. rules = await get_point_rules(chat_id)
  1090. await apply_point_rules(chat_id, {key: not bool(rules[key])})
  1091. elif action == "identity_monitor_toggle":
  1092. key = payload["key"]
  1093. if key not in {"enabled", "notify_in_chat"}:
  1094. return await query.answer("不支持的资料监控开关。", show_alert=True)
  1095. await ensure_permission(chat_id, "can_change_info", actor_id=query.from_user.id)
  1096. settings = await get_automation_settings(chat_id)
  1097. monitor = dict(settings["identity_monitor"])
  1098. monitor[key] = not bool(monitor[key])
  1099. await apply_automation_settings(
  1100. chat_id,
  1101. {"identity_monitor": monitor},
  1102. actor_id=query.from_user.id,
  1103. )
  1104. elif action in {"risk_rule_toggle", "risk_rule_action", "risk_rule_delete"}:
  1105. await ensure_permission(chat_id, "can_change_info", actor_id=query.from_user.id)
  1106. settings = await get_automation_settings(chat_id)
  1107. rules = [dict(item) for item in settings["risk_rules"]]
  1108. rule_id = payload["rule_id"]
  1109. index = next(
  1110. (i for i, item in enumerate(rules) if item["rule_id"] == rule_id),
  1111. None,
  1112. )
  1113. if index is None:
  1114. return await query.answer("风控规则不存在或已删除。", show_alert=True)
  1115. if action == "risk_rule_delete":
  1116. rules.pop(index)
  1117. elif action == "risk_rule_toggle":
  1118. key = payload["key"]
  1119. if key == "delete":
  1120. actions = set(rules[index]["actions"])
  1121. if "delete" in actions:
  1122. actions.remove("delete")
  1123. else:
  1124. actions.add("delete")
  1125. rules[index]["actions"] = sorted(actions)
  1126. elif key in {"enabled", "match_images", "match_links"}:
  1127. rules[index][key] = not bool(rules[index][key])
  1128. else:
  1129. return await query.answer("不支持的风控开关。", show_alert=True)
  1130. else:
  1131. key = payload["key"]
  1132. if key not in {"none", "warn", "mute", "kick", "ban"}:
  1133. return await query.answer("不支持的成员处置。", show_alert=True)
  1134. actions = set(rules[index]["actions"])
  1135. actions.difference_update({"warn", "mute", "kick", "ban"})
  1136. if key != "none":
  1137. actions.add(key)
  1138. rules[index]["actions"] = sorted(actions)
  1139. await apply_automation_settings(
  1140. chat_id,
  1141. {"risk_rules": rules},
  1142. actor_id=query.from_user.id,
  1143. )
  1144. elif action == "autoreply_clear":
  1145. await apply_automation_settings(
  1146. chat_id, {"auto_replies": []}, actor_id=query.from_user.id
  1147. )
  1148. elif action == "gfinish":
  1149. giveaway = await get_giveaway(payload["giveaway_id"])
  1150. if not giveaway or int(giveaway["chat_id"]) != chat_id:
  1151. return await query.answer("抽奖不存在。", show_alert=True)
  1152. await ensure_permission(chat_id, "can_change_info", actor_id=query.from_user.id)
  1153. await finish_and_publish_giveaway(payload["giveaway_id"])
  1154. elif action == "gcancel":
  1155. giveaway = await get_giveaway(payload["giveaway_id"])
  1156. if not giveaway or int(giveaway["chat_id"]) != chat_id:
  1157. return await query.answer("抽奖不存在。", show_alert=True)
  1158. await ensure_permission(chat_id, "can_change_info", actor_id=query.from_user.id)
  1159. await cancel_and_refund_giveaway(payload["giveaway_id"], chat_id=chat_id)
  1160. await record_audit(
  1161. source="telegram_private",
  1162. actor_id=query.from_user.id,
  1163. actor_name=query.from_user.first_name,
  1164. action=action,
  1165. chat_id=chat_id,
  1166. summary=str(payload),
  1167. )
  1168. await query.answer("操作成功。", show_alert=True)
  1169. if action == "identity_monitor_toggle":
  1170. await _show_section(query, chat_id, "identity")
  1171. elif action == "risk_rule_delete":
  1172. await _show_section(query, chat_id, "risk")
  1173. elif action.startswith("risk_rule_"):
  1174. await _show_risk_rule(query, chat_id, payload["rule_id"])
  1175. else:
  1176. await _show_chat_menu(query, chat_id)
  1177. @app.on_message(filters.command("manage") & filters.private)
  1178. async def manage_command(_, message: Message):
  1179. _flows.pop(message.from_user.id, None)
  1180. if len(message.command) > 1:
  1181. if not message.command[1].lstrip("-").isdigit():
  1182. return await message.reply_text("用法:/manage [群组 ID]")
  1183. chat_id = int(message.command[1])
  1184. try:
  1185. overview = await get_chat_overview(chat_id, actor_id=message.from_user.id)
  1186. except ChatManagementError:
  1187. return await message.reply_text("你无权管理该群,或机器人已无法访问该群。")
  1188. return await message.reply_text(
  1189. f"已选择 <b>{escape(overview['title'])}</b>。",
  1190. parse_mode=ParseMode.HTML,
  1191. reply_markup=InlineKeyboardMarkup([[_button("打开群菜单", f"mg:c:{chat_id}")]]),
  1192. )
  1193. await _show_group_list(message, message.from_user.id)
  1194. @app.on_message(filters.command("cancel") & filters.private)
  1195. async def cancel_manage_flow(_, message: Message):
  1196. removed = _flows.pop(message.from_user.id, None)
  1197. confirmation_tokens = [
  1198. token
  1199. for token, confirmation in _confirmations.items()
  1200. if confirmation.get("actor_id") == message.from_user.id
  1201. ]
  1202. for token in confirmation_tokens:
  1203. _confirmations.pop(token, None)
  1204. canceled = bool(removed or confirmation_tokens)
  1205. return await message.reply_text(
  1206. "已取消当前操作。" if canceled else "当前没有进行中的操作。"
  1207. )
  1208. @app.on_callback_query(filters.regex(r"^mg:"))
  1209. async def manage_callback(_, query: CallbackQuery):
  1210. parts = query.data.split(":")
  1211. try:
  1212. if parts[1] == "p":
  1213. return await _show_group_list(query, query.from_user.id, int(parts[2]))
  1214. if parts[1] == "c":
  1215. return await _show_chat_menu(query, int(parts[2]))
  1216. if parts[1] == "s":
  1217. return await _show_section(query, int(parts[2]), parts[3])
  1218. if parts[1] == "r":
  1219. return await _show_risk_rule(query, int(parts[2]), parts[3])
  1220. if parts[1] == "k":
  1221. return await _start_risk_rule_direct(
  1222. query, int(parts[2]), parts[3], parts[4]
  1223. )
  1224. if parts[1] == "n":
  1225. return await _start_risk_rule_flow(
  1226. query, int(parts[2]), "risk_rule_add"
  1227. )
  1228. if parts[1] == "e":
  1229. return await _start_risk_rule_flow(
  1230. query, int(parts[2]), "risk_rule_edit", parts[3]
  1231. )
  1232. if parts[1] == "q":
  1233. return await _start_risk_rule_flow(
  1234. query, int(parts[2]), "risk_rule_duration", parts[3]
  1235. )
  1236. if parts[1] == "u":
  1237. return await _show_member_target(query, int(parts[2]), int(parts[3]))
  1238. if parts[1] == "v":
  1239. return await _start_flow_for_member(
  1240. query,
  1241. int(parts[2]),
  1242. int(parts[3]),
  1243. parts[4],
  1244. )
  1245. if parts[1] == "a":
  1246. return await _start_flow(query, int(parts[2]), parts[3])
  1247. if parts[1] == "d":
  1248. return await _danger_action(query, parts)
  1249. if parts[1] == "x":
  1250. return await _execute_confirmation(query, parts[2])
  1251. if parts[1] == "y":
  1252. return await _execute_direct_confirmation(query, parts[2])
  1253. if parts[1] == "z":
  1254. _confirmations.pop(parts[2], None)
  1255. await query.answer("已取消。")
  1256. return await _show_group_list(query, query.from_user.id)
  1257. except ChatManagementError as exc:
  1258. return await query.answer(str(exc), show_alert=True)
  1259. except Exception as exc:
  1260. log.error(f"私聊管理操作失败:{exc}")
  1261. return await query.answer(
  1262. "操作失败,请检查输入内容和机器人权限后重试。",
  1263. show_alert=True,
  1264. )
  1265. @app.on_message(filters.private & ~filters.command(["manage", "cancel"]), group=14)
  1266. async def manage_flow_input(_, message: Message):
  1267. if not message.from_user:
  1268. return
  1269. flow = _flows.get(message.from_user.id)
  1270. if not flow:
  1271. return
  1272. if flow.expires_at < datetime.now(UTC):
  1273. _flows.pop(message.from_user.id, None)
  1274. return await message.reply_text("管理流程已超时,请重新使用 /manage。")
  1275. try:
  1276. await _handle_flow_input(message, flow)
  1277. except ChatManagementError as exc:
  1278. _flows.pop(message.from_user.id, None)
  1279. await message.reply_text(str(exc))