bot_permissions.py 10 KB

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