bot_permissions.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. from __future__ import annotations
  2. from collections.abc import Iterable
  3. from dataclasses import asdict, dataclass
  4. @dataclass(frozen=True)
  5. class BotPermission:
  6. key: str
  7. name: str
  8. group: str
  9. description: str
  10. @dataclass(frozen=True)
  11. class BuiltinBotRole:
  12. role_id: str
  13. name: str
  14. description: str
  15. permissions: tuple[str, ...]
  16. PERMISSIONS = (
  17. BotPermission("chat.profile", "群资料", "群组管理", "修改群标题和描述。"),
  18. BotPermission("chat.members", "成员与管理员", "群组管理", "查询、处罚及调整管理员。"),
  19. BotPermission("chat.permissions", "群权限", "群组管理", "修改普通成员默认权限。"),
  20. BotPermission("chat.announcements", "公告", "群组管理", "发送并置顶群公告。"),
  21. BotPermission("chat.invites", "邀请链接", "群组管理", "创建和撤销邀请链接。"),
  22. BotPermission("chat.rules", "群规", "群组管理", "查看和维护群规。"),
  23. BotPermission("chat.notes", "群笔记", "群组管理", "维护群内笔记功能。"),
  24. BotPermission(
  25. "automation.manage",
  26. "自动化与风控",
  27. "自动化",
  28. "管理欢迎语、自动回复、验证码、聊天机器人、内容风控和资料监控。",
  29. ),
  30. BotPermission("points.manage", "积分", "社区运营", "运行积分规则、排行和人工调整。"),
  31. BotPermission("giveaways.manage", "抽奖", "社区运营", "创建、开奖、退款和管理参与者。"),
  32. BotPermission("karma.manage", "声望", "社区运营", "处理点赞、点踩和声望排行。"),
  33. BotPermission(
  34. "teacher_directory.manage",
  35. "技师与附近",
  36. "社区运营",
  37. "提供技师申请、上下榜、在线状态、位置和附近技师目录。",
  38. ),
  39. BotPermission(
  40. "business_assistant.manage",
  41. "智能接待",
  42. "自动化",
  43. "管理 Telegram Business 连接、知识库、客户会话和人工接管。",
  44. ),
  45. BotPermission("media.upload", "媒体中转", "系统能力", "向媒体中转群上传文件。"),
  46. )
  47. ALL_BOT_PERMISSIONS = frozenset(item.key for item in PERMISSIONS)
  48. GROUP_MANAGEMENT_PERMISSIONS = frozenset(
  49. {
  50. "chat.announcements",
  51. "chat.members",
  52. "chat.permissions",
  53. "automation.manage",
  54. "points.manage",
  55. "giveaways.manage",
  56. }
  57. )
  58. PRIVATE_MANAGEMENT_PERMISSIONS = GROUP_MANAGEMENT_PERMISSIONS | {
  59. "teacher_directory.manage",
  60. "business_assistant.manage",
  61. }
  62. BUILTIN_ROLES = (
  63. BuiltinBotRole(
  64. "full_access",
  65. "全部职责",
  66. "拥有当前系统的全部机器人职责。",
  67. tuple(item.key for item in PERMISSIONS),
  68. ),
  69. BuiltinBotRole(
  70. "group_manager",
  71. "群组管理员",
  72. "负责群资料、成员、权限、公告、邀请链接、群规和群笔记。",
  73. (
  74. "chat.profile",
  75. "chat.members",
  76. "chat.permissions",
  77. "chat.announcements",
  78. "chat.invites",
  79. "chat.rules",
  80. "chat.notes",
  81. ),
  82. ),
  83. BuiltinBotRole(
  84. "automation_manager",
  85. "自动化与风控专员",
  86. "负责自动回复、欢迎语、验证码、聊天机器人、风控和资料监控。",
  87. ("automation.manage", "media.upload"),
  88. ),
  89. BuiltinBotRole(
  90. "points_manager",
  91. "积分专员",
  92. "负责积分规则、签到、排行榜和积分调整。",
  93. ("points.manage",),
  94. ),
  95. BuiltinBotRole(
  96. "giveaway_manager",
  97. "抽奖专员",
  98. "负责抽奖全流程,并可处理抽奖关联积分。",
  99. ("giveaways.manage", "points.manage"),
  100. ),
  101. BuiltinBotRole(
  102. "karma_manager",
  103. "声望专员",
  104. "负责点赞、点踩和声望排行。",
  105. ("karma.manage",),
  106. ),
  107. BuiltinBotRole(
  108. "teacher_directory_manager",
  109. "技师目录专员",
  110. "负责技师申请、位置、上下榜、在线状态和附近技师目录。",
  111. ("teacher_directory.manage",),
  112. ),
  113. BuiltinBotRole(
  114. "business_assistant_manager",
  115. "智能接待专员",
  116. "负责 Telegram Business 智能接待、知识库和客户会话。",
  117. ("business_assistant.manage",),
  118. ),
  119. )
  120. MODULE_PERMISSIONS: dict[str, frozenset[str]] = {
  121. "admin": frozenset({"chat.members", "chat.announcements", "chat.invites"}),
  122. "admin_misc": frozenset({"chat.profile", "chat.members"}),
  123. "admin_panel": GROUP_MANAGEMENT_PERMISSIONS,
  124. "antiservice": frozenset({"automation.manage"}),
  125. "blacklist": frozenset({"automation.manage"}),
  126. "blacklist_chat": frozenset({"automation.manage"}),
  127. "business_assistant": frozenset({"business_assistant.manage"}),
  128. "chatbot": frozenset({"automation.manage"}),
  129. "filters": frozenset({"automation.manage"}),
  130. "flood": frozenset({"automation.manage"}),
  131. "greetings": frozenset({"automation.manage"}),
  132. "giveaway": frozenset({"giveaways.manage"}),
  133. "karma": frozenset({"karma.manage"}),
  134. "locks": frozenset({"chat.permissions"}),
  135. "notes": frozenset({"chat.notes"}),
  136. "points": frozenset({"points.manage"}),
  137. "rules": frozenset({"chat.rules"}),
  138. "teacher_directory": frozenset({"teacher_directory.manage"}),
  139. "technician_services": frozenset({"teacher_directory.manage"}),
  140. }
  141. TELEGRAM_COMMAND_PERMISSIONS = {
  142. "purge": "chat.members",
  143. "kick": "chat.members",
  144. "dkick": "chat.members",
  145. "ban": "chat.members",
  146. "dban": "chat.members",
  147. "tban": "chat.members",
  148. "unban": "chat.members",
  149. "listban": "chat.members",
  150. "listunban": "chat.members",
  151. "del": "chat.members",
  152. "promote": "chat.members",
  153. "fullpromote": "chat.members",
  154. "demote": "chat.members",
  155. "mute": "chat.members",
  156. "tmute": "chat.members",
  157. "unmute": "chat.members",
  158. "ban_ghosts": "chat.members",
  159. "warn": "chat.members",
  160. "dwarn": "chat.members",
  161. "rmwarns": "chat.members",
  162. "warns": "chat.members",
  163. "report": "chat.members",
  164. "admins": "chat.members",
  165. "admin": "chat.members",
  166. "set_user_title": "chat.members",
  167. "pin": "chat.announcements",
  168. "unpin": "chat.announcements",
  169. "invite": "chat.invites",
  170. "set_chat_title": "chat.profile",
  171. "set_chat_photo": "chat.profile",
  172. }
  173. def permission_catalog() -> list[dict[str, str]]:
  174. return [asdict(item) for item in PERMISSIONS]
  175. def builtin_roles() -> list[dict[str, object]]:
  176. return [
  177. {
  178. "role_id": item.role_id,
  179. "name": item.name,
  180. "description": item.description,
  181. "permissions": list(item.permissions),
  182. "builtin": True,
  183. }
  184. for item in BUILTIN_ROLES
  185. ]
  186. def normalize_permissions(values: object) -> list[str]:
  187. if not isinstance(values, (list, tuple, set, frozenset)):
  188. values = str(values or "").replace(",", " ").split()
  189. normalized = list(dict.fromkeys(str(item).strip() for item in values if str(item).strip()))
  190. unknown = sorted(set(normalized) - ALL_BOT_PERMISSIONS)
  191. if unknown:
  192. raise ValueError(f"未知机器人权限:{', '.join(unknown)}")
  193. return normalized
  194. def has_permission(permission: str, permissions: Iterable[str] | None) -> bool:
  195. current = set(permissions or ())
  196. return "*" in current or permission in current
  197. def has_any_permission(
  198. required: Iterable[str],
  199. permissions: Iterable[str] | None,
  200. ) -> bool:
  201. current = set(permissions or ())
  202. return "*" in current or bool(set(required) & current)
  203. def module_allowed(module: str, permissions: Iterable[str] | None) -> bool:
  204. required = MODULE_PERMISSIONS.get(module)
  205. return not required or has_any_permission(required, permissions)
  206. def api_permission(method: str, path: str) -> str | None:
  207. if path.startswith("/api/admin/v1/business-assistant"):
  208. return "business_assistant.manage"
  209. if path.startswith("/api/admin/v1/directory"):
  210. return "teacher_directory.manage"
  211. if path.startswith("/api/admin/v1/media"):
  212. return "media.upload"
  213. if path.startswith("/api/admin/v1/points"):
  214. return "points.manage"
  215. if path.startswith("/api/admin/v1/giveaways"):
  216. return "giveaways.manage"
  217. if path.startswith("/api/admin/v1/external-risk"):
  218. return "automation.manage"
  219. if "/giveaway-bans" in path:
  220. return "giveaways.manage"
  221. if not path.startswith("/api/admin/v1/chats/"):
  222. return None
  223. if (
  224. method == "DELETE"
  225. and path.removeprefix("/api/admin/v1/chats/").lstrip("-").isdigit()
  226. ):
  227. return "chat.profile"
  228. if path.endswith("/profile"):
  229. return "chat.profile"
  230. if path.endswith("/permissions"):
  231. return "chat.permissions"
  232. if path.endswith("/announcements"):
  233. return "chat.announcements"
  234. if "/members" in path or path.endswith("/admins"):
  235. return "chat.members"
  236. if path.endswith("/invites"):
  237. return "chat.invites"
  238. if path.endswith("/rules"):
  239. return "chat.rules"
  240. if path.endswith("/automation"):
  241. return "automation.manage"
  242. if path.endswith("/interaction-settings"):
  243. return "automation.manage"
  244. if "/points/" in path:
  245. return "points.manage"
  246. return None