__main__.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. """
  2. MIT License
  3. Copyright (c) 2024 TheHamkerCat
  4. Permission is hereby granted, free of charge, to any person obtaining a copy
  5. of this software and associated documentation files (the "Software"), to deal
  6. in the Software without restriction, including without limitation the rights
  7. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in all
  11. copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  17. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  18. SOFTWARE.
  19. """
  20. import asyncio
  21. import importlib
  22. import os
  23. import re
  24. import time
  25. from contextlib import closing, suppress
  26. import psutil
  27. from pyrogram import StopPropagation, filters, idle
  28. from pyrogram.enums import ChatType, ParseMode
  29. from pyrogram.types import BotCommand, InlineKeyboardButton, InlineKeyboardMarkup
  30. from uvloop import install
  31. from wbb import (
  32. BOT_NAME,
  33. BOT_PERMISSIONS,
  34. BOT_USERNAME,
  35. LOG_GROUP_ID,
  36. USERBOT_CONNECTED,
  37. USERBOT_NAME,
  38. aiohttpsession,
  39. app,
  40. app2,
  41. bot_start_time,
  42. log,
  43. )
  44. from wbb.core.keyboard import ikb
  45. from wbb.modules import ALL_MODULES
  46. from wbb.services.bot_permissions import (
  47. GROUP_MANAGEMENT_PERMISSIONS,
  48. PRIVATE_MANAGEMENT_PERMISSIONS,
  49. TELEGRAM_COMMAND_PERMISSIONS,
  50. has_any_permission,
  51. has_permission,
  52. )
  53. from wbb.utils import paginate_modules
  54. from wbb.utils.constants import MARKDOWN
  55. from wbb.utils.dbfunctions import clean_restart_stage, get_rules
  56. from wbb.utils.functions import extract_text_and_keyb
  57. loop = asyncio.get_event_loop()
  58. HELPABLE = {}
  59. HELP_MODULE_KEYS = {
  60. "admin",
  61. "admin_misc",
  62. "admin_panel",
  63. "antiservice",
  64. "blacklist",
  65. "chatbot",
  66. "feds",
  67. "filters",
  68. "flood",
  69. "giveaway",
  70. "greetings",
  71. "karma",
  72. "locks",
  73. "notes",
  74. "points",
  75. "rules",
  76. }
  77. BOT_COMMANDS = [
  78. BotCommand("start", "打开机器人首页"),
  79. BotCommand("help", "查看中文功能帮助"),
  80. BotCommand("manage", "打开私聊群管理菜单"),
  81. BotCommand("cancel", "取消当前管理流程"),
  82. BotCommand("points", "查看积分余额"),
  83. BotCommand("checkin", "领取每日签到积分"),
  84. BotCommand("points_rank", "查看群积分排行榜"),
  85. BotCommand("points_history", "查看最近积分流水"),
  86. BotCommand("giveaway", "创建群抽奖"),
  87. BotCommand("gjoin", "报名参加抽奖"),
  88. BotCommand("glist", "查看进行中的抽奖"),
  89. BotCommand("gparticipants", "查看抽奖参与者"),
  90. BotCommand("gend", "立即开奖"),
  91. BotCommand("gcancel", "取消抽奖并退款"),
  92. BotCommand("greroll", "重新抽取中奖者"),
  93. BotCommand("rules", "查看本群群规"),
  94. BotCommand("directory", "打开师生与附近菜单"),
  95. BotCommand("nearby", "查找附近老师"),
  96. BotCommand("teacher_rank", "查看老师榜单"),
  97. BotCommand("teacher_apply", "申请成为老师"),
  98. BotCommand("teacher_online", "切换老师为上线"),
  99. BotCommand("teacher_offline", "切换老师为下线"),
  100. BotCommand("teacher_list", "老师自助上榜"),
  101. BotCommand("teacher_unlist", "老师自助下榜"),
  102. ]
  103. BOT_COMMAND_PERMISSIONS = {
  104. "manage": "__group_any__",
  105. "cancel": "__any__",
  106. "points": "points.manage",
  107. "checkin": "points.manage",
  108. "points_rank": "points.manage",
  109. "points_history": "points.manage",
  110. "giveaway": "giveaways.manage",
  111. "gjoin": "giveaways.manage",
  112. "glist": "giveaways.manage",
  113. "gparticipants": "giveaways.manage",
  114. "gend": "giveaways.manage",
  115. "gcancel": "giveaways.manage",
  116. "greroll": "giveaways.manage",
  117. "rules": "chat.rules",
  118. "directory": "teacher_directory.manage",
  119. "nearby": "teacher_directory.manage",
  120. "teacher_rank": "teacher_directory.manage",
  121. "teacher_apply": "teacher_directory.manage",
  122. "teacher_online": "teacher_directory.manage",
  123. "teacher_offline": "teacher_directory.manage",
  124. "teacher_list": "teacher_directory.manage",
  125. "teacher_unlist": "teacher_directory.manage",
  126. }
  127. def _visible_bot_commands() -> list[BotCommand]:
  128. visible = []
  129. for command in BOT_COMMANDS:
  130. required = BOT_COMMAND_PERMISSIONS.get(command.command)
  131. if required == "__group_any__" and not has_any_permission(
  132. GROUP_MANAGEMENT_PERMISSIONS, BOT_PERMISSIONS
  133. ):
  134. continue
  135. if required == "__any__" and not has_any_permission(
  136. PRIVATE_MANAGEMENT_PERMISSIONS, BOT_PERMISSIONS
  137. ):
  138. continue
  139. if required and required not in {"__any__", "__group_any__"} and not has_permission(
  140. required, BOT_PERMISSIONS
  141. ):
  142. continue
  143. visible.append(command)
  144. return visible
  145. @app.on_message(
  146. filters.command(list(TELEGRAM_COMMAND_PERMISSIONS)),
  147. group=-1000,
  148. )
  149. async def bot_role_command_guard(_, message):
  150. command = (
  151. str(message.command[0]).lower().split("@", 1)[0]
  152. if message.command
  153. else ""
  154. )
  155. required = TELEGRAM_COMMAND_PERMISSIONS.get(command)
  156. if required and not has_permission(required, BOT_PERMISSIONS):
  157. raise StopPropagation
  158. @app.on_message(
  159. filters.command(["admins", "admin"], prefixes="@"),
  160. group=-1000,
  161. )
  162. async def bot_role_at_command_guard(_, __):
  163. if not has_permission("chat.members", BOT_PERMISSIONS):
  164. raise StopPropagation
  165. @app.on_callback_query(filters.regex(r"^unwarn_"), group=-1000)
  166. async def bot_role_callback_guard(_, __):
  167. if not has_permission("chat.members", BOT_PERMISSIONS):
  168. raise StopPropagation
  169. async def bot_sys_stats() -> str:
  170. uptime = max(0, int(time.time() - bot_start_time))
  171. days, remainder = divmod(uptime, 86400)
  172. hours, remainder = divmod(remainder, 3600)
  173. minutes, seconds = divmod(remainder, 60)
  174. duration = f"{days} 天 {hours} 小时 {minutes} 分 {seconds} 秒"
  175. process = psutil.Process(os.getpid())
  176. return (
  177. f"机器人:{BOT_NAME}\n"
  178. f"运行时长:{duration}\n"
  179. f"进程内存:{round(process.memory_info().rss / 1024 ** 2)} MB\n"
  180. f"处理器:{psutil.cpu_percent()}%\n"
  181. f"系统内存:{psutil.virtual_memory().percent}%\n"
  182. f"磁盘:{psutil.disk_usage('/').percent}%"
  183. )
  184. async def start_bot():
  185. global HELPABLE
  186. for module in ALL_MODULES:
  187. imported_module = importlib.import_module("wbb.modules." + module)
  188. if (
  189. module in HELP_MODULE_KEYS
  190. and hasattr(imported_module, "__MODULE__")
  191. and imported_module.__MODULE__
  192. ):
  193. if (
  194. hasattr(imported_module, "__HELP__")
  195. and imported_module.__HELP__
  196. ):
  197. HELPABLE[module] = imported_module
  198. bot_modules = ""
  199. j = 1
  200. for i in ALL_MODULES:
  201. if j == 4:
  202. bot_modules += "|{:<15}|\n".format(i)
  203. j = 0
  204. else:
  205. bot_modules += "|{:<15}".format(i)
  206. j += 1
  207. print("+===============================================================+")
  208. print("| WBB |")
  209. print("+===============+===============+===============+===============+")
  210. print(bot_modules)
  211. print("+===============+===============+===============+===============+")
  212. log.info(f"机器人已启动:{BOT_NAME}")
  213. if USERBOT_CONNECTED:
  214. log.info(f"用户客户端已启动:{USERBOT_NAME}")
  215. try:
  216. await app.set_bot_commands(_visible_bot_commands())
  217. log.info("已注册中文 Telegram 命令菜单")
  218. except Exception as exc:
  219. log.error(f"注册 Telegram 命令菜单失败:{exc}")
  220. restart_data = await clean_restart_stage()
  221. try:
  222. log.info("正在发送上线状态")
  223. if restart_data:
  224. await app.edit_message_text(
  225. restart_data["chat_id"],
  226. restart_data["message_id"],
  227. "**机器人已成功重启。**",
  228. )
  229. else:
  230. await app.send_message(LOG_GROUP_ID, "机器人已启动。")
  231. except Exception:
  232. pass
  233. await idle()
  234. await aiohttpsession.close()
  235. log.info("正在停止客户端")
  236. await app.stop()
  237. if USERBOT_CONNECTED:
  238. await app2.stop()
  239. log.info("正在取消异步任务")
  240. for task in asyncio.all_tasks():
  241. task.cancel()
  242. log.info("机器人已停止")
  243. home_actions = [
  244. InlineKeyboardButton(text="功能帮助", callback_data="bot_commands"),
  245. ]
  246. if has_any_permission(GROUP_MANAGEMENT_PERMISSIONS, BOT_PERMISSIONS):
  247. home_actions.append(
  248. InlineKeyboardButton(text="群组管理", callback_data="manage_hint")
  249. )
  250. if has_permission("teacher_directory.manage", BOT_PERMISSIONS):
  251. home_actions.append(
  252. InlineKeyboardButton(
  253. text="师生与附近",
  254. url=f"https://t.me/{BOT_USERNAME}?start=directory",
  255. )
  256. )
  257. home_keyboard_pm = InlineKeyboardMarkup(
  258. [
  259. home_actions,
  260. [
  261. InlineKeyboardButton(
  262. text="运行状态",
  263. callback_data="stats_callback",
  264. )
  265. ],
  266. [
  267. InlineKeyboardButton(
  268. text="添加机器人到群组",
  269. url=f"http://t.me/{BOT_USERNAME}?startgroup=new",
  270. )
  271. ],
  272. ]
  273. )
  274. assigned_features = [
  275. label
  276. for permission, label in (
  277. ("chat.profile", "群资料"),
  278. ("chat.members", "成员管理"),
  279. ("chat.permissions", "群权限"),
  280. ("chat.announcements", "公告"),
  281. ("chat.invites", "邀请链接"),
  282. ("chat.rules", "群规"),
  283. ("chat.notes", "群笔记"),
  284. ("automation.manage", "自动化与风控"),
  285. ("points.manage", "积分"),
  286. ("giveaways.manage", "抽奖"),
  287. ("karma.manage", "声望"),
  288. ("teacher_directory.manage", "师生与附近"),
  289. )
  290. if has_permission(permission, BOT_PERMISSIONS)
  291. ]
  292. home_text_pm = (
  293. f"你好,我是 {BOT_NAME}。\n\n"
  294. + (
  295. f"我当前负责:{'、'.join(assigned_features)}。"
  296. if assigned_features
  297. else "我当前尚未分配管理职责。"
  298. )
  299. + (
  300. "群管理员可发送 /manage 打开私聊管理菜单。"
  301. if has_any_permission(GROUP_MANAGEMENT_PERMISSIONS, BOT_PERMISSIONS)
  302. else ""
  303. )
  304. )
  305. keyboard = InlineKeyboardMarkup(
  306. [
  307. [
  308. InlineKeyboardButton(
  309. text="查看帮助",
  310. url=f"t.me/{BOT_USERNAME}?start=help",
  311. ),
  312. InlineKeyboardButton(
  313. text="运行状态",
  314. callback_data="stats_callback",
  315. ),
  316. ],
  317. ]
  318. )
  319. FED_MARKUP = InlineKeyboardMarkup(
  320. [
  321. [
  322. InlineKeyboardButton(
  323. "联邦创建者命令", callback_data="fed_owner"
  324. ),
  325. InlineKeyboardButton(
  326. "联邦管理员命令", callback_data="fed_admin"
  327. ),
  328. ],
  329. [
  330. InlineKeyboardButton("普通成员命令", callback_data="fed_user"),
  331. ],
  332. [
  333. InlineKeyboardButton("返回", callback_data="help_back"),
  334. ],
  335. ]
  336. )
  337. @app.on_message(filters.command("start"))
  338. async def start(_, message):
  339. if message.chat.type != ChatType.PRIVATE:
  340. return await message.reply(
  341. "请私聊我查看详细功能。", reply_markup=keyboard
  342. )
  343. if len(message.text.split()) > 1:
  344. user = await app.get_users(message.from_user.id)
  345. name = (message.text.split(None, 1)[1]).lower()
  346. match = re.match(r"rules_(.*)", name)
  347. if match:
  348. chat_id = match.group(1)
  349. user_id = message.from_user.id
  350. chat = await app.get_chat(int(chat_id))
  351. text = f"**`{chat.title}` 的群规:\n\n**"
  352. rules = await get_rules(int(chat_id))
  353. if rules:
  354. text = text + rules
  355. if "{chat}" in text:
  356. text = text.replace("{chat}", chat.title)
  357. if "{name}" in text:
  358. text = text.replace("{name}", user.mention)
  359. keyb = None
  360. if re.findall(r"\[.+\,.+\]", text):
  361. text, keyb = extract_text_and_keyb(ikb, text)
  362. await app.send_message(user_id, text=text, reply_markup=keyb)
  363. else:
  364. await app.send_message(
  365. user_id,
  366. "该群管理员尚未设置群规。",
  367. )
  368. return
  369. if name == "directory" and has_permission(
  370. "teacher_directory.manage", BOT_PERMISSIONS
  371. ):
  372. from wbb.modules.teacher_directory import send_directory_menu
  373. return await send_directory_menu(message)
  374. if name == "mkdwn_help":
  375. await message.reply(
  376. MARKDOWN,
  377. parse_mode=ParseMode.HTML,
  378. disable_web_page_preview=True,
  379. )
  380. elif "_" in name:
  381. module = name.split("_", 1)[1]
  382. if module not in HELPABLE:
  383. return await message.reply("未找到对应的中文帮助内容。")
  384. text = (
  385. f"以下是 **{HELPABLE[module].__MODULE__}** 的使用说明:\n"
  386. + HELPABLE[module].__HELP__
  387. )
  388. if module == "feds":
  389. return await message.reply(
  390. text=text,
  391. reply_markup=FED_MARKUP,
  392. disable_web_page_preview=True,
  393. )
  394. await message.reply(
  395. text,
  396. reply_markup=InlineKeyboardMarkup(
  397. [[InlineKeyboardButton("返回", callback_data="help_back")]]
  398. ),
  399. disable_web_page_preview=True,
  400. )
  401. elif name == "help":
  402. text, keyb = await help_parser(message.from_user.first_name)
  403. await message.reply(
  404. text,
  405. reply_markup=keyb,
  406. )
  407. else:
  408. await message.reply(
  409. home_text_pm,
  410. reply_markup=home_keyboard_pm,
  411. )
  412. if has_permission("teacher_directory.manage", BOT_PERMISSIONS):
  413. from wbb.modules.teacher_directory import send_directory_menu
  414. await send_directory_menu(message)
  415. return
  416. @app.on_message(filters.command("help"))
  417. async def help_command(_, message):
  418. if message.chat.type != ChatType.PRIVATE:
  419. if len(message.command) >= 2:
  420. name = (message.text.split(None, 1)[1]).replace(" ", "_").lower()
  421. if str(name) in HELPABLE:
  422. key = InlineKeyboardMarkup(
  423. [
  424. [
  425. InlineKeyboardButton(
  426. text="查看帮助",
  427. url=f"t.me/{BOT_USERNAME}?start=help_{name}",
  428. )
  429. ],
  430. ]
  431. )
  432. await message.reply(
  433. f"点击下方按钮查看“{HELPABLE[name].__MODULE__}”的帮助。",
  434. reply_markup=key,
  435. )
  436. else:
  437. await message.reply(
  438. "请私聊我查看详细功能。", reply_markup=keyboard
  439. )
  440. else:
  441. await message.reply(
  442. "请私聊我查看详细功能。", reply_markup=keyboard
  443. )
  444. else:
  445. if len(message.command) >= 2:
  446. name = (message.text.split(None, 1)[1]).replace(" ", "_").lower()
  447. if str(name) in HELPABLE:
  448. text = (
  449. f"以下是 **{HELPABLE[name].__MODULE__}** 的使用说明:\n"
  450. + HELPABLE[name].__HELP__
  451. )
  452. await message.reply(text, disable_web_page_preview=True)
  453. else:
  454. text, help_keyboard = await help_parser(
  455. message.from_user.first_name
  456. )
  457. await message.reply(
  458. text,
  459. reply_markup=help_keyboard,
  460. disable_web_page_preview=True,
  461. )
  462. else:
  463. text, help_keyboard = await help_parser(
  464. message.from_user.first_name
  465. )
  466. await message.reply(
  467. text, reply_markup=help_keyboard, disable_web_page_preview=True
  468. )
  469. return
  470. async def help_parser(name, keyboard=None):
  471. if not keyboard:
  472. keyboard = InlineKeyboardMarkup(paginate_modules(0, HELPABLE, "help"))
  473. return (
  474. """你好,{first_name},我是 {bot_name}。
  475. 我可以管理群组、自动回复、积分和抽奖。
  476. 请选择下方功能分类查看中文命令说明。
  477. """.format(
  478. first_name=name,
  479. bot_name=BOT_NAME,
  480. ),
  481. keyboard,
  482. )
  483. @app.on_callback_query(filters.regex("bot_commands"))
  484. async def commands_callbacc(_, CallbackQuery):
  485. text, keyboard = await help_parser(CallbackQuery.from_user.mention)
  486. await app.send_message(
  487. CallbackQuery.message.chat.id,
  488. text=text,
  489. reply_markup=keyboard,
  490. )
  491. await CallbackQuery.message.delete()
  492. @app.on_callback_query(filters.regex("^manage_hint$"))
  493. async def manage_hint_callback(_, query):
  494. if not has_any_permission(PRIVATE_MANAGEMENT_PERMISSIONS, BOT_PERMISSIONS):
  495. return await query.answer("当前机器人未分配私聊管理职责。", show_alert=True)
  496. await query.answer("请发送 /manage 打开群组管理菜单。", show_alert=True)
  497. @app.on_callback_query(filters.regex("stats_callback"))
  498. async def stats_callbacc(_, CallbackQuery):
  499. text = await bot_sys_stats()
  500. await app.answer_callback_query(CallbackQuery.id, text, show_alert=True)
  501. @app.on_callback_query(filters.regex(r"help_(.*?)"))
  502. async def help_button(client, query):
  503. home_match = re.match(r"help_home\((.+?)\)", query.data)
  504. mod_match = re.match(r"help_module\((.+?)\)", query.data)
  505. prev_match = re.match(r"help_prev\((.+?)\)", query.data)
  506. next_match = re.match(r"help_next\((.+?)\)", query.data)
  507. back_match = re.match(r"help_back", query.data)
  508. create_match = re.match(r"help_create", query.data)
  509. top_text = f"""
  510. 你好,{query.from_user.first_name},我是 {BOT_NAME}。
  511. 我可以管理群组、自动回复、积分和抽奖。
  512. 请选择下方功能分类查看中文命令说明。
  513. 常用命令:
  514. - /start:打开机器人首页
  515. - /help:查看功能帮助
  516. - /manage:打开私聊群管理菜单
  517. """
  518. if mod_match:
  519. module = (mod_match.group(1)).replace(" ", "_")
  520. text = (
  521. "{} **{}**:\n".format(
  522. "以下是使用说明", HELPABLE[module].__MODULE__
  523. )
  524. + HELPABLE[module].__HELP__
  525. )
  526. if module == "feds":
  527. return await query.message.edit(
  528. text=text,
  529. reply_markup=FED_MARKUP,
  530. disable_web_page_preview=True,
  531. )
  532. await query.message.edit(
  533. text=text,
  534. reply_markup=InlineKeyboardMarkup(
  535. [[InlineKeyboardButton("返回", callback_data="help_back")]]
  536. ),
  537. disable_web_page_preview=True,
  538. )
  539. elif home_match:
  540. await app.send_message(
  541. query.from_user.id,
  542. text=home_text_pm,
  543. reply_markup=home_keyboard_pm,
  544. )
  545. await query.message.delete()
  546. elif prev_match:
  547. curr_page = int(prev_match.group(1))
  548. await query.message.edit(
  549. text=top_text,
  550. reply_markup=InlineKeyboardMarkup(
  551. paginate_modules(curr_page - 1, HELPABLE, "help")
  552. ),
  553. disable_web_page_preview=True,
  554. )
  555. elif next_match:
  556. next_page = int(next_match.group(1))
  557. await query.message.edit(
  558. text=top_text,
  559. reply_markup=InlineKeyboardMarkup(
  560. paginate_modules(next_page + 1, HELPABLE, "help")
  561. ),
  562. disable_web_page_preview=True,
  563. )
  564. elif back_match:
  565. await query.message.edit(
  566. text=top_text,
  567. reply_markup=InlineKeyboardMarkup(
  568. paginate_modules(0, HELPABLE, "help")
  569. ),
  570. disable_web_page_preview=True,
  571. )
  572. elif create_match:
  573. text, keyboard = await help_parser(query)
  574. await query.message.edit(
  575. text=text,
  576. reply_markup=keyboard,
  577. disable_web_page_preview=True,
  578. )
  579. return await client.answer_callback_query(query.id)
  580. if __name__ == "__main__":
  581. install()
  582. with closing(loop):
  583. with suppress(asyncio.exceptions.CancelledError):
  584. loop.run_until_complete(start_bot())
  585. loop.run_until_complete(asyncio.sleep(3.0)) # task cancel wait