Queer European MD passionate about IT

administration_tools.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. """Administration tools for telegram bots.
  2. Usage:
  3. ```
  4. import davtelepot
  5. my_bot = davtelepot.Bot.get('my_token', 'my_database.db')
  6. davtelepot.admin_tools.init(my_bot)
  7. ```
  8. """
  9. # Standard library modules
  10. import asyncio
  11. import datetime
  12. # Third party modules
  13. from davtelepot.utilities import (
  14. async_wrapper, Confirmator, get_cleaned_text, get_user, escape_html_chars,
  15. line_drawing_unordered_list, make_button, make_inline_keyboard,
  16. remove_html_tags
  17. )
  18. default_talk_messages = dict(
  19. admin_session_ended=dict(
  20. en=(
  21. 'Session with user {u} ended.'
  22. ),
  23. it=(
  24. 'Sessione terminata con l\'utente {u}.'
  25. ),
  26. ),
  27. admin_warning=dict(
  28. en=(
  29. 'You are now talking to {u}.\n'
  30. 'Until you end this session, your messages will be '
  31. 'forwarded to each other.'
  32. ),
  33. it=(
  34. 'Sei ora connesso con {u}.\n'
  35. 'Finché non chiuderai la connessione, i messaggi che scriverai '
  36. 'qui saranno inoltrati a {u}, e ti inoltrerò i suoi.'
  37. ),
  38. ),
  39. end_session=dict(
  40. en=(
  41. 'End session?'
  42. ),
  43. it=(
  44. 'Chiudere la sessione?'
  45. ),
  46. ),
  47. help_text=dict(
  48. en='Press the button to search for user.',
  49. it='Premi il pulsante per scegliere un utente.'
  50. ),
  51. search_button=dict(
  52. en="🔍 Search for user",
  53. it="🔍 Cerca utente",
  54. ),
  55. select_user=dict(
  56. en='Which user would you like to talk to?',
  57. it='Con quale utente vorresti parlare?'
  58. ),
  59. user_not_found=dict(
  60. en=(
  61. "Sory, but no user matches your query for\n"
  62. "<code>{q}</code>"
  63. ),
  64. it=(
  65. "Spiacente, ma nessun utente corrisponde alla ricerca per\n"
  66. "<code>{q}</code>"
  67. ),
  68. ),
  69. instructions=dict(
  70. en=(
  71. 'Write a part of name, surname or username of the user you want '
  72. 'to talk to.'
  73. ),
  74. it=(
  75. 'Scrivi una parte del nome, cognome o username dell\'utente con '
  76. 'cui vuoi parlare.'
  77. ),
  78. ),
  79. stop=dict(
  80. en=(
  81. 'End session'
  82. ),
  83. it=(
  84. 'Termina la sessione'
  85. ),
  86. ),
  87. user_session_ended=dict(
  88. en=(
  89. 'Session with admin {u} ended.'
  90. ),
  91. it=(
  92. 'Sessione terminata con l\'amministratore {u}.'
  93. ),
  94. ),
  95. user_warning=dict(
  96. en=(
  97. '{u}, admin of this bot, wants to talk to you.\n'
  98. 'Until this session is ended by {u}, your messages will be '
  99. 'forwarded to each other.'
  100. ),
  101. it=(
  102. '{u}, amministratore di questo bot, vuole parlare con te.\n'
  103. 'Finché non chiuderà la connessione, i messaggi che scriverai '
  104. 'qui saranno inoltrati a {u}, e ti inoltrerò i suoi.'
  105. ),
  106. ),
  107. # key=dict(
  108. # en='',
  109. # it='',
  110. # ),
  111. # key=dict(
  112. # en=(
  113. # ''
  114. # ),
  115. # it=(
  116. # ''
  117. # ),
  118. # ),
  119. )
  120. async def _forward_to(update, bot, sender, addressee, is_admin=False):
  121. if update['text'].lower() in ['stop'] and is_admin:
  122. with bot.db as db:
  123. admin_record = db['users'].find_one(
  124. telegram_id=sender
  125. )
  126. session_record = db['talking_sessions'].find_one(
  127. admin=admin_record['id'],
  128. cancelled=0
  129. )
  130. other_user_record = db['users'].find_one(
  131. id=session_record['user']
  132. )
  133. await end_session(
  134. bot=bot,
  135. other_user_record=other_user_record,
  136. admin_record=admin_record
  137. )
  138. else:
  139. bot.set_individual_text_message_handler(
  140. await async_wrapper(
  141. _forward_to,
  142. sender=sender,
  143. addressee=addressee,
  144. is_admin=is_admin
  145. ),
  146. sender
  147. )
  148. await bot.forward_message(
  149. chat_id=addressee,
  150. update=update
  151. )
  152. return
  153. def get_talk_panel(update, bot, text=''):
  154. """Return text and reply markup of talk panel.
  155. `text` may be:
  156. - `user_id` as string
  157. - `username` as string
  158. - `''` (empty string) for main menu (default)
  159. """
  160. users = []
  161. if len(text):
  162. with bot.db as db:
  163. if text.isnumeric():
  164. users = list(
  165. db['users'].find(id=int(text))
  166. )
  167. else:
  168. users = list(
  169. db.query(
  170. """SELECT *
  171. FROM users
  172. WHERE COALESCE(
  173. first_name || last_name || username,
  174. last_name || username,
  175. first_name || username,
  176. username,
  177. first_name || last_name,
  178. last_name,
  179. first_name
  180. ) LIKE '%{username}%'
  181. ORDER BY LOWER(
  182. COALESCE(
  183. first_name || last_name || username,
  184. last_name || username,
  185. first_name || username,
  186. username,
  187. first_name || last_name,
  188. last_name,
  189. first_name
  190. )
  191. )
  192. LIMIT 26
  193. """.format(
  194. username=text
  195. )
  196. )
  197. )
  198. if len(text) == 0:
  199. text = (
  200. bot.get_message(
  201. 'talk',
  202. 'help_text',
  203. update=update,
  204. q=escape_html_chars(
  205. remove_html_tags(text)
  206. )
  207. )
  208. )
  209. reply_markup = make_inline_keyboard(
  210. [
  211. make_button(
  212. bot.get_message(
  213. 'talk', 'search_button',
  214. update=update
  215. ),
  216. prefix='talk:///',
  217. data=['search']
  218. )
  219. ],
  220. 1
  221. )
  222. elif len(users) == 0:
  223. text = (
  224. bot.get_message(
  225. 'talk',
  226. 'user_not_found',
  227. update=update,
  228. q=escape_html_chars(
  229. remove_html_tags(text)
  230. )
  231. )
  232. )
  233. reply_markup = make_inline_keyboard(
  234. [
  235. make_button(
  236. bot.get_message(
  237. 'talk', 'search_button',
  238. update=update
  239. ),
  240. prefix='talk:///',
  241. data=['search']
  242. )
  243. ],
  244. 1
  245. )
  246. else:
  247. text = "{header}\n\n{u}{etc}".format(
  248. header=bot.get_message(
  249. 'talk', 'select_user',
  250. update=update
  251. ),
  252. u=line_drawing_unordered_list(
  253. [
  254. get_user(user)
  255. for user in users[:25]
  256. ]
  257. ),
  258. etc=(
  259. '\n\n[...]'
  260. if len(users) > 25
  261. else ''
  262. )
  263. )
  264. reply_markup = make_inline_keyboard(
  265. [
  266. make_button(
  267. '👤 {u}'.format(
  268. u=get_user(
  269. {
  270. key: val
  271. for key, val in user.items()
  272. if key in (
  273. 'first_name',
  274. 'last_name',
  275. 'username'
  276. )
  277. }
  278. )
  279. ),
  280. prefix='talk:///',
  281. data=[
  282. 'select',
  283. user['id']
  284. ]
  285. )
  286. for user in users[:25]
  287. ],
  288. 2
  289. )
  290. return text, reply_markup
  291. async def _talk_command(update, bot):
  292. text = get_cleaned_text(
  293. update,
  294. bot,
  295. ['talk']
  296. )
  297. text, reply_markup = get_talk_panel(update, bot, text)
  298. return dict(
  299. text=text,
  300. parse_mode='HTML',
  301. reply_markup=reply_markup,
  302. )
  303. async def start_session(bot, other_user_record, admin_record):
  304. """Start talking session between user and admin.
  305. Register session in database, so it gets loaded before message_loop starts.
  306. Send a notification both to admin and user, set custom parsers and return.
  307. """
  308. with bot.db as db:
  309. db['talking_sessions'].insert(
  310. dict(
  311. user=other_user_record['id'],
  312. admin=admin_record['id'],
  313. cancelled=0
  314. )
  315. )
  316. await bot.send_message(
  317. chat_id=other_user_record['telegram_id'],
  318. text=bot.get_message(
  319. 'talk', 'user_warning',
  320. user_record=other_user_record,
  321. u=get_user(admin_record)
  322. )
  323. )
  324. await bot.send_message(
  325. chat_id=admin_record['telegram_id'],
  326. text=bot.get_message(
  327. 'talk', 'admin_warning',
  328. user_record=admin_record,
  329. u=get_user(other_user_record)
  330. ),
  331. reply_markup=make_inline_keyboard(
  332. [
  333. make_button(
  334. bot.get_message(
  335. 'talk', 'stop',
  336. user_record=admin_record
  337. ),
  338. prefix='talk:///',
  339. data=['stop', other_user_record['id']]
  340. )
  341. ]
  342. )
  343. )
  344. bot.set_individual_text_message_handler(
  345. await async_wrapper(
  346. _forward_to,
  347. sender=other_user_record['telegram_id'],
  348. addressee=admin_record['telegram_id'],
  349. is_admin=False
  350. ),
  351. other_user_record['telegram_id']
  352. )
  353. bot.set_individual_text_message_handler(
  354. await async_wrapper(
  355. _forward_to,
  356. sender=admin_record['telegram_id'],
  357. addressee=other_user_record['telegram_id'],
  358. is_admin=True
  359. ),
  360. admin_record['telegram_id']
  361. )
  362. return
  363. async def end_session(bot, other_user_record, admin_record):
  364. """End talking session between user and admin.
  365. Cancel session in database, so it will not be loaded anymore.
  366. Send a notification both to admin and user, clear custom parsers
  367. and return.
  368. """
  369. with bot.db as db:
  370. db['talking_sessions'].update(
  371. dict(
  372. admin=admin_record['id'],
  373. cancelled=1
  374. ),
  375. ['admin']
  376. )
  377. await bot.send_message(
  378. chat_id=other_user_record['telegram_id'],
  379. text=bot.get_message(
  380. 'talk', 'user_session_ended',
  381. user_record=other_user_record,
  382. u=get_user(admin_record)
  383. )
  384. )
  385. await bot.send_message(
  386. chat_id=admin_record['telegram_id'],
  387. text=bot.get_message(
  388. 'talk', 'admin_session_ended',
  389. user_record=admin_record,
  390. u=get_user(other_user_record)
  391. ),
  392. )
  393. for record in (admin_record, other_user_record, ):
  394. bot.remove_individual_text_message_handler(record['telegram_id'])
  395. return
  396. async def _talk_button(bot, update, user_record, data):
  397. telegram_id = user_record['telegram_id']
  398. command, *arguments = data
  399. result, text, reply_markup = '', '', None
  400. if command == 'search':
  401. bot.set_individual_text_message_handler(
  402. await async_wrapper(
  403. _talk_command,
  404. ),
  405. update
  406. )
  407. text = bot.get_message(
  408. 'talk', 'instructions',
  409. update=update
  410. )
  411. reply_markup = None
  412. elif command == 'select':
  413. if (
  414. len(arguments) < 1
  415. or type(arguments[0]) is not int
  416. ):
  417. result = "Errore!"
  418. else:
  419. with bot.db as db:
  420. other_user_record = db['users'].find_one(
  421. id=arguments[0]
  422. )
  423. admin_record = db['users'].find_one(
  424. telegram_id=telegram_id
  425. )
  426. await start_session(
  427. bot,
  428. other_user_record=other_user_record,
  429. admin_record=admin_record
  430. )
  431. elif command == 'stop':
  432. if (
  433. len(arguments) < 1
  434. or type(arguments[0]) is not int
  435. ):
  436. result = "Errore!"
  437. elif not Confirmator.get('stop_bots').confirm(telegram_id):
  438. result = bot.get_message(
  439. 'talk', 'end_session',
  440. update=update,
  441. )
  442. else:
  443. with bot.db as db:
  444. other_user_record = db['users'].find_one(
  445. id=arguments[0]
  446. )
  447. admin_record = db['users'].find_one(
  448. telegram_id=telegram_id
  449. )
  450. await end_session(
  451. bot,
  452. other_user_record=other_user_record,
  453. admin_record=admin_record
  454. )
  455. text = "Session ended."
  456. reply_markup = None
  457. if text:
  458. return dict(
  459. text=result,
  460. edit=dict(
  461. text=text,
  462. parse_mode='HTML',
  463. reply_markup=reply_markup,
  464. disable_web_page_preview=True
  465. )
  466. )
  467. return result
  468. default_admin_messages = {
  469. 'restart_command': {
  470. 'description': {
  471. 'en': "Restart bots",
  472. 'it': "Riavvia i bot"
  473. },
  474. 'restart_scheduled_message': {
  475. 'en': "Bots are being restarted, after pulling from repository.",
  476. 'it': "I bot verranno riavviati in pochi secondi, caricando "
  477. "prima le eventuali modifiche al codice."
  478. },
  479. 'restart_completed_message': {
  480. 'en': "<i>Restart was successful.</i>",
  481. 'it': "<i>Restart avvenuto con successo.</i>"
  482. }
  483. }
  484. }
  485. async def _restart_command(bot, update, user_record):
  486. with bot.db as db:
  487. db['restart_messages'].insert(
  488. dict(
  489. text=bot.get_message(
  490. 'admin', 'restart_command', 'restart_completed_message',
  491. update=update, user_record=user_record
  492. ),
  493. chat_id=update['chat']['id'],
  494. parse_mode='HTML',
  495. reply_to_message_id=update['message_id'],
  496. sent=None
  497. )
  498. )
  499. await bot.reply(
  500. update=update,
  501. text=bot.get_message(
  502. 'admin', 'restart_command', 'restart_scheduled_message',
  503. update=update, user_record=user_record
  504. )
  505. )
  506. bot.__class__.stop(message='=== RESTART ===', final_state=65)
  507. return
  508. def init(bot, talk_messages=None, admin_messages=None, language='en'):
  509. """Assign parsers, commands, buttons and queries to given `bot`."""
  510. if talk_messages is None:
  511. talk_messages = default_talk_messages
  512. bot.messages['talk'] = talk_messages
  513. if admin_messages is None:
  514. admin_messages = default_admin_messages
  515. bot.messages['admin'] = admin_messages
  516. with bot.db as db:
  517. if 'talking_sessions' not in db.tables:
  518. db['talking_sessions'].insert(
  519. dict(
  520. user=0,
  521. admin=0,
  522. cancelled=1
  523. )
  524. )
  525. @bot.additional_task(when='BEFORE')
  526. async def load_talking_sessions():
  527. sessions = []
  528. with bot.db as db:
  529. for session in db.query(
  530. """SELECT *
  531. FROM talking_sessions
  532. WHERE NOT cancelled
  533. """
  534. ):
  535. sessions.append(
  536. dict(
  537. other_user_record=db['users'].find_one(
  538. id=session['user']
  539. ),
  540. admin_record=db['users'].find_one(
  541. id=session['admin']
  542. ),
  543. )
  544. )
  545. for session in sessions:
  546. await start_session(
  547. bot=bot,
  548. other_user_record=session['other_user_record'],
  549. admin_record=session['admin_record']
  550. )
  551. @bot.command(command='/talk', aliases=[], show_in_keyboard=False,
  552. description="Choose a user and forward messages to each "
  553. "other.",
  554. authorization_level='admin')
  555. async def talk_command(update):
  556. return await _talk_command(update, bot)
  557. @bot.button(prefix='talk:///', separator='|', authorization_level='admin')
  558. async def talk_button(bot, update, user_record, data):
  559. return await _talk_button(bot, update, user_record, data)
  560. restart_command_description = bot.get_message(
  561. 'admin', 'restart_command', 'description',
  562. language=language
  563. )
  564. @bot.command(command='/restart', aliases=[], show_in_keyboard=False,
  565. description=restart_command_description,
  566. authorization_level='admin')
  567. async def restart_command(bot, update, user_record):
  568. return await _restart_command(bot, update, user_record)
  569. @bot.additional_task('BEFORE')
  570. async def send_restart_messages():
  571. """Send restart messages at restart."""
  572. with bot.db as db:
  573. for restart_message in db['restart_messages'].find(sent=None):
  574. asyncio.ensure_future(
  575. bot.send_message(
  576. **{
  577. key: val
  578. for key, val in restart_message.items()
  579. if key in (
  580. 'chat_id',
  581. 'text',
  582. 'parse_mode',
  583. 'reply_to_message_id'
  584. )
  585. }
  586. )
  587. )
  588. db['restart_messages'].update(
  589. dict(
  590. sent=datetime.datetime.now(),
  591. id=restart_message['id']
  592. ),
  593. ['id'],
  594. ensure=True
  595. )
  596. return