Queer European MD passionate about IT

authorization.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. """Provide authorization levels to bot functions."""
  2. # Standard library modules
  3. from collections import OrderedDict
  4. # Project modules
  5. from .utilities import (
  6. Confirmator, get_cleaned_text, get_user, make_button, make_inline_keyboard
  7. )
  8. DEFAULT_ROLES = OrderedDict()
  9. DEFAULT_ROLES[0] = {
  10. 'name': 'banned',
  11. 'symbol': '🚫',
  12. 'singular': 'banned',
  13. 'plural': 'banned',
  14. 'can_appoint': [],
  15. 'can_be_appointed_by': [1, 2, 3]
  16. }
  17. DEFAULT_ROLES[1] = {
  18. 'name': 'founder',
  19. 'symbol': '👑',
  20. 'singular': 'founder',
  21. 'plural': 'founders',
  22. 'can_appoint': [0, 1, 2, 3, 4, 5, 7, 100],
  23. 'can_be_appointed_by': []
  24. }
  25. DEFAULT_ROLES[2] = {
  26. 'name': 'admin',
  27. 'symbol': '⚜️',
  28. 'singular': 'administrator',
  29. 'plural': 'administrators',
  30. 'can_appoint': [0, 3, 4, 5, 7, 100],
  31. 'can_be_appointed_by': [1]
  32. }
  33. DEFAULT_ROLES[3] = {
  34. 'name': 'moderator',
  35. 'symbol': '🔰',
  36. 'singular': 'moderator',
  37. 'plural': 'moderators',
  38. 'can_appoint': [0, 5, 7],
  39. 'can_be_appointed_by': [1, 2]
  40. }
  41. DEFAULT_ROLES[5] = {
  42. 'name': 'user',
  43. 'symbol': '🎫',
  44. 'singular': 'registered user',
  45. 'plural': 'registered users',
  46. 'can_appoint': [],
  47. 'can_be_appointed_by': [1, 2, 3]
  48. }
  49. DEFAULT_ROLES[100] = {
  50. 'name': 'everybody',
  51. 'symbol': '👤',
  52. 'singular': 'common user',
  53. 'plural': 'common users',
  54. 'can_appoint': [],
  55. 'can_be_appointed_by': [1, 2, 3]
  56. }
  57. class Role():
  58. """Authorization level for users of a bot."""
  59. roles = OrderedDict()
  60. default_role_code = 100
  61. def __init__(self, code, name, symbol, singular, plural,
  62. can_appoint, can_be_appointed_by):
  63. """Instantiate Role object.
  64. code : int
  65. The higher the code, the less privileges are connected to that
  66. role.
  67. Use 0 for banned users.
  68. name : str
  69. Short name for role.
  70. symbol : str
  71. Emoji used to represent role.
  72. singular : str
  73. Singular full name of role.
  74. plural : str
  75. Plural full name of role.
  76. can_appoint : lsit of int
  77. List of role codes that this role can appoint.
  78. can_be_appointed_by : list of int
  79. List of role codes this role can be appointed by.
  80. """
  81. self._code = code
  82. self._name = name
  83. self._symbol = symbol
  84. self._singular = singular
  85. self._plural = plural
  86. self._can_appoint = can_appoint
  87. self._can_be_appointed_by = can_be_appointed_by
  88. self.__class__.roles[self.code] = self
  89. @property
  90. def code(self):
  91. """Return code."""
  92. return self._code
  93. @property
  94. def name(self):
  95. """Return name."""
  96. return self._name
  97. @property
  98. def symbol(self):
  99. """Return symbol."""
  100. return self._symbol
  101. @property
  102. def singular(self):
  103. """Return singular."""
  104. return self._singular
  105. @property
  106. def plural(self):
  107. """Return plural."""
  108. return self._plural
  109. @property
  110. def can_appoint(self):
  111. """Return can_appoint."""
  112. return self._can_appoint
  113. @property
  114. def can_be_appointed_by(self):
  115. """Return roles whom this role can be appointed by."""
  116. return self._can_be_appointed_by
  117. @classmethod
  118. def get_by_role_id(cls, role_id=100):
  119. """Give a `role_id`, return the corresponding `Role` instance."""
  120. for code, role in cls.roles.items():
  121. if code == role_id:
  122. return role
  123. raise IndexError(f"Unknown role id: {role_id}")
  124. @classmethod
  125. def get_user_role(cls, user_record=None, user_role_id=None):
  126. """Given a `user_record`, return its `Role`.
  127. `role_id` may be passed as keyword argument or as user_record.
  128. """
  129. if user_role_id is None:
  130. if isinstance(user_record, dict) and 'privileges' in user_record:
  131. user_role_id = user_record['privileges']
  132. elif type(user_record) is int:
  133. user_role_id = user_record
  134. if type(user_role_id) is not int:
  135. for code, role in cls.roles.items():
  136. if role.name == user_role_id:
  137. user_role_id = code
  138. break
  139. else:
  140. user_role_id = cls.default_role_code
  141. return cls.get_by_role_id(role_id=user_role_id)
  142. @classmethod
  143. def set_default_role_code(cls, role):
  144. """Set class default role code.
  145. It will be returned if a specific role code cannot be evaluated.
  146. """
  147. cls.default_role_code = role
  148. @classmethod
  149. def get_user_role_panel(cls, user_record):
  150. """Get text and buttons for user role panel."""
  151. user_role = cls.get_user_role(user_record=user_record)
  152. text = (
  153. """👤 <a href="tg://user?id={u[telegram_id]}">{u[username]}</a>\n"""
  154. f"🔑 <i>{user_role.singular.capitalize()}</i> {user_role.symbol}"
  155. ).format(
  156. u=user_record,
  157. )
  158. buttons = [
  159. make_button(
  160. f"{role.symbol} {role.singular.capitalize()}",
  161. prefix='auth:///',
  162. data=['set', user_record['id'], code]
  163. )
  164. for code, role in cls.roles.items()
  165. ]
  166. return text, buttons
  167. def __eq__(self, other):
  168. """Return True if self is equal to other."""
  169. return self.code == other.code
  170. def __gt__(self, other):
  171. """Return True if self can appoint other."""
  172. return (
  173. (
  174. self.code < other.code
  175. or other.code == 0
  176. )
  177. and self.code in other.can_be_appointed_by
  178. )
  179. def __ge__(self, other):
  180. """Return True if self >= other."""
  181. return self.__gt__(other) or self.__eq__(other)
  182. def __lt__(self, other):
  183. """Return True if self can not appoint other."""
  184. return not self.__ge__(other)
  185. def __le__(self, other):
  186. """Return True if self is superior or equal to other."""
  187. return not self.__gt__(other)
  188. def __ne__(self, other):
  189. """Return True if self is not equal to other."""
  190. return not self.__eq__(other)
  191. def __str__(self):
  192. """Return human-readable description of role."""
  193. return f"<Role object: {self.symbol} {self.singular.capitalize()}>"
  194. def get_authorization_function(bot):
  195. """Take a `bot` and return its authorization_function."""
  196. def is_authorized(update, user_record=None, authorization_level=2):
  197. """Return True if user role is at least at `authorization_level`."""
  198. user_role = bot.Role.get_user_role(user_record=user_record)
  199. if user_role.code == 0:
  200. return False
  201. needed_role = bot.Role.get_user_role(user_role_id=authorization_level)
  202. if needed_role.code < user_role.code:
  203. return False
  204. return True
  205. return is_authorized
  206. deafult_authorization_messages = {
  207. 'auth_command': {
  208. 'description': {
  209. 'en': "Edit user permissions. To select a user, reply to "
  210. "a message of theirs or write their username",
  211. 'it': "Cambia il grado di autorizzazione di un utente "
  212. "(in risposta o scrivendone lo username)"
  213. },
  214. 'unhandled_case': {
  215. 'en': "<code>Unhandled case :/</code>",
  216. 'it': "<code>Caso non previsto :/</code>"
  217. },
  218. 'instructions': {
  219. 'en': "Reply with this command to a user or write "
  220. "<code>/auth username</code> to edit their permissions.",
  221. 'it': "Usa questo comando in risposta a un utente "
  222. "oppure scrivi <code>/auth username</code> per "
  223. "cambiarne il grado di autorizzazione."
  224. },
  225. 'unknown_user': {
  226. 'en': "Unknown user.",
  227. 'it': "Utente sconosciuto."
  228. },
  229. 'choose_user': {
  230. 'en': "{n} users match your query. Please select one.",
  231. 'it': "Ho trovato {n} utenti che soddisfano questi criteri.\n"
  232. "Per procedere selezionane uno."
  233. },
  234. 'no_match': {
  235. 'en': "No user matches your query. Please try again.",
  236. 'it': "Non ho trovato utenti che soddisfino questi criteri.\n"
  237. "Prova di nuovo."
  238. }
  239. },
  240. 'ban_command': {
  241. 'description': {
  242. 'en': "Reply to a user with /ban to ban them",
  243. 'it': "Banna l'utente (da usare in risposta)"
  244. }
  245. },
  246. 'auth_button': {
  247. 'description': {
  248. 'en': "Edit user permissions",
  249. 'it': "Cambia il grado di autorizzazione di un utente"
  250. },
  251. 'confirm': {
  252. 'en': "Are you sure?",
  253. 'it': "Sicuro sicuro?"
  254. },
  255. 'back_to_user': {
  256. 'en': "Back to user",
  257. 'it': "Torna all'utente"
  258. },
  259. 'permission_denied': {
  260. 'user': {
  261. 'en': "You cannot appoint this user!",
  262. 'it': "Non hai l'autorità di modificare i permessi di questo "
  263. "utente!"
  264. },
  265. 'role': {
  266. 'en': "You're not allowed to appoint someone to this role!",
  267. 'it': "Non hai l'autorità di conferire questo permesso!"
  268. }
  269. },
  270. 'no_change': {
  271. 'en': "No change suggested!",
  272. 'it': "È già così!"
  273. },
  274. 'appointed': {
  275. 'en': "Permission granted",
  276. 'it': "Permesso conferito"
  277. }
  278. },
  279. }
  280. async def _authorization_command(bot, update, user_record):
  281. text = get_cleaned_text(bot=bot, update=update, replace=['auth'])
  282. reply_markup = None
  283. result = bot.get_message(
  284. 'authorization', 'auth_command', 'unhandled_case',
  285. update=update, user_record=user_record
  286. )
  287. if not text:
  288. if 'reply_to_message' not in update:
  289. return bot.get_message(
  290. 'authorization', 'auth_command', 'instructions',
  291. update=update, user_record=user_record
  292. )
  293. else:
  294. with bot.db as db:
  295. user_record = db['users'].find_one(
  296. telegram_id=update['reply_to_message']['from']['id']
  297. )
  298. else:
  299. with bot.db as db:
  300. user_record = list(
  301. db.query(
  302. "SELECT * "
  303. "FROM users "
  304. "WHERE COALESCE("
  305. " first_name || last_name || username,"
  306. " last_name || username,"
  307. " first_name || username,"
  308. " username,"
  309. " first_name || last_name,"
  310. " last_name,"
  311. " first_name"
  312. f") LIKE '%{text}%'"
  313. )
  314. )
  315. if user_record is None:
  316. result = bot.get_message(
  317. 'authorization', 'auth_command', 'unknown_user',
  318. update=update, user_record=user_record
  319. )
  320. elif type(user_record) is list and len(user_record) > 1:
  321. result = bot.get_message(
  322. 'authorization', 'auth_command', 'choose_user',
  323. update=update, user_record=user_record,
  324. n=len(user_record)
  325. )
  326. reply_markup = make_inline_keyboard(
  327. [
  328. make_button(
  329. f"👤 {get_user(user, link_profile=False)}",
  330. prefix='auth:///',
  331. data=['show', user['id']]
  332. )
  333. for user in user_record[:30]
  334. ],
  335. 3
  336. )
  337. elif type(user_record) is list and len(user_record) == 0:
  338. result = bot.get_message(
  339. 'authorization', 'auth_command', 'no_match',
  340. update=update, user_record=user_record,
  341. )
  342. else:
  343. if type(user_record) is list:
  344. user_record = user_record[0]
  345. result, buttons = bot.Role.get_user_role_panel(user_record)
  346. reply_markup = make_inline_keyboard(buttons, 1)
  347. return dict(
  348. text=result,
  349. reply_markup=reply_markup,
  350. parse_mode='HTML'
  351. )
  352. async def _authorization_button(bot, update, user_record, data):
  353. if len(data) == 0:
  354. data = ['']
  355. command, *arguments = data
  356. user_id = user_record['telegram_id']
  357. if len(arguments) > 0:
  358. other_user_id = arguments[0]
  359. else:
  360. other_user_id = None
  361. result, text, reply_markup = '', '', None
  362. if command in ['show']:
  363. with bot.db as db:
  364. other_user_record = db['users'].find_one(id=other_user_id)
  365. text, buttons = bot.Role.get_user_role_panel(other_user_record)
  366. reply_markup = make_inline_keyboard(buttons, 1)
  367. elif command in ['set'] and len(arguments) > 1:
  368. other_user_id, new_privileges, *_ = arguments
  369. if not Confirmator.get(
  370. key=f'{user_id}_set_{other_user_id}',
  371. confirm_timedelta=5
  372. ).confirm:
  373. return bot.get_message(
  374. 'authorization', 'auth_button', 'confirm',
  375. update=update, user_record=user_record,
  376. )
  377. with bot.db as db:
  378. other_user_record = db['users'].find_one(id=other_user_id)
  379. user_role = bot.Role.get_user_role(user_record=user_record)
  380. other_user_role = bot.Role.get_user_role(user_record=other_user_record)
  381. if other_user_role.code == new_privileges:
  382. return bot.get_message(
  383. 'authorization', 'auth_button', 'no_change',
  384. update=update, user_record=user_record
  385. )
  386. if not user_role > other_user_role:
  387. text = bot.get_message(
  388. 'authorization', 'auth_button', 'permission_denied', 'user',
  389. update=update, user_record=user_record
  390. )
  391. reply_markup = make_inline_keyboard(
  392. [
  393. make_button(
  394. bot.get_message(
  395. 'authorization', 'auth_button', 'back_to_user',
  396. update=update, user_record=user_record
  397. ),
  398. prefix='auth:///',
  399. data=['show', other_user_id]
  400. )
  401. ],
  402. 1
  403. )
  404. elif new_privileges not in user_role.can_appoint:
  405. text = bot.get_message(
  406. 'authorization', 'auth_button', 'permission_denied', 'role',
  407. update=update, user_record=user_record
  408. )
  409. reply_markup = make_inline_keyboard(
  410. [
  411. make_button(
  412. bot.get_message(
  413. 'authorization', 'auth_button', 'back_to_user',
  414. update=update, user_record=user_record
  415. ),
  416. prefix='auth:///',
  417. data=['show', other_user_id]
  418. )
  419. ],
  420. 1
  421. )
  422. else:
  423. with bot.db as db:
  424. db['users'].update(
  425. dict(
  426. id=other_user_id,
  427. privileges=new_privileges
  428. ),
  429. ['id']
  430. )
  431. other_user_record = db['users'].find_one(id=other_user_id)
  432. result = bot.get_message(
  433. 'authorization', 'auth_button', 'appointed',
  434. update=update, user_record=user_record
  435. )
  436. text, buttons = bot.Role.get_user_role_panel(other_user_record)
  437. reply_markup = make_inline_keyboard(buttons, 1)
  438. if text:
  439. return dict(
  440. text=result,
  441. edit=dict(
  442. text=text,
  443. reply_markup=reply_markup,
  444. parse_mode='HTML'
  445. )
  446. )
  447. return result
  448. async def _ban_command(bot, update, user_record):
  449. # TODO define this function!
  450. return
  451. def init(bot, roles=None, authorization_messages=None):
  452. """Set bot roles and assign role-related commands.
  453. Pass an OrderedDict of `roles` to get them set.
  454. """
  455. class _Role(Role):
  456. roles = OrderedDict()
  457. bot.Role = _Role
  458. if roles is None:
  459. roles = DEFAULT_ROLES
  460. # Cast roles to OrderedDict
  461. if isinstance(roles, list):
  462. roles = OrderedDict(
  463. (i, element)
  464. for i, element in enumerate(list)
  465. )
  466. if not isinstance(roles, OrderedDict):
  467. raise TypeError("`roles` shall be a OrderedDict!")
  468. for id, role in roles.items():
  469. if 'code' not in role:
  470. role['code'] = id
  471. bot.Role(**role)
  472. bot.set_authorization_function(
  473. get_authorization_function(bot)
  474. )
  475. if authorization_messages is None:
  476. authorization_messages = deafult_authorization_messages
  477. bot.messages['authorization'] = authorization_messages
  478. @bot.command(command='/auth', aliases=[], show_in_keyboard=False,
  479. description=(
  480. authorization_messages['auth_command']['description']
  481. ),
  482. authorization_level='moderator')
  483. async def authorization_command(bot, update, user_record):
  484. return await _authorization_command(bot, update, user_record)
  485. @bot.button('auth:///',
  486. description=(
  487. authorization_messages['auth_button']['description']
  488. ),
  489. separator='|',
  490. authorization_level='moderator')
  491. async def authorization_button(bot, update, user_record, data):
  492. return await _authorization_button(bot, update, user_record, data)
  493. @bot.command('/ban', aliases=[], show_in_keyboard=False,
  494. description=(
  495. authorization_messages['ban_command']['description']
  496. ),
  497. authorization_level='admin')
  498. async def ban_command(bot, update, user_record):
  499. return await _ban_command(bot, update, user_record)