Queer European MD passionate about IT

administration_tools.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269
  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. import json
  13. # Third party modules
  14. from davtelepot.utilities import (
  15. async_wrapper, Confirmator, extract, get_cleaned_text, get_user,
  16. escape_html_chars, line_drawing_unordered_list, make_button,
  17. make_inline_keyboard, remove_html_tags, send_part_of_text_file,
  18. send_csv_file
  19. )
  20. from sqlalchemy.exc import ResourceClosedError
  21. default_talk_messages = dict(
  22. admin_session_ended=dict(
  23. en=(
  24. 'Session with user {u} ended.'
  25. ),
  26. it=(
  27. 'Sessione terminata con l\'utente {u}.'
  28. ),
  29. ),
  30. admin_warning=dict(
  31. en=(
  32. 'You are now talking to {u}.\n'
  33. 'Until you end this session, your messages will be '
  34. 'forwarded to each other.'
  35. ),
  36. it=(
  37. 'Sei ora connesso con {u}.\n'
  38. 'Finché non chiuderai la connessione, i messaggi che scriverai '
  39. 'qui saranno inoltrati a {u}, e ti inoltrerò i suoi.'
  40. ),
  41. ),
  42. end_session=dict(
  43. en=(
  44. 'End session?'
  45. ),
  46. it=(
  47. 'Chiudere la sessione?'
  48. ),
  49. ),
  50. help_text=dict(
  51. en='Press the button to search for user.',
  52. it='Premi il pulsante per scegliere un utente.'
  53. ),
  54. search_button=dict(
  55. en="🔍 Search for user",
  56. it="🔍 Cerca utente",
  57. ),
  58. select_user=dict(
  59. en='Which user would you like to talk to?',
  60. it='Con quale utente vorresti parlare?'
  61. ),
  62. user_not_found=dict(
  63. en=(
  64. "Sory, but no user matches your query for\n"
  65. "<code>{q}</code>"
  66. ),
  67. it=(
  68. "Spiacente, ma nessun utente corrisponde alla ricerca per\n"
  69. "<code>{q}</code>"
  70. ),
  71. ),
  72. instructions=dict(
  73. en=(
  74. 'Write a part of name, surname or username of the user you want '
  75. 'to talk to.'
  76. ),
  77. it=(
  78. 'Scrivi una parte del nome, cognome o username dell\'utente con '
  79. 'cui vuoi parlare.'
  80. ),
  81. ),
  82. stop=dict(
  83. en=(
  84. 'End session'
  85. ),
  86. it=(
  87. 'Termina la sessione'
  88. ),
  89. ),
  90. user_session_ended=dict(
  91. en=(
  92. 'Session with admin {u} ended.'
  93. ),
  94. it=(
  95. 'Sessione terminata con l\'amministratore {u}.'
  96. ),
  97. ),
  98. user_warning=dict(
  99. en=(
  100. '{u}, admin of this bot, wants to talk to you.\n'
  101. 'Until this session is ended by {u}, your messages will be '
  102. 'forwarded to each other.'
  103. ),
  104. it=(
  105. '{u}, amministratore di questo bot, vuole parlare con te.\n'
  106. 'Finché non chiuderà la connessione, i messaggi che scriverai '
  107. 'qui saranno inoltrati a {u}, e ti inoltrerò i suoi.'
  108. ),
  109. ),
  110. # key=dict(
  111. # en='',
  112. # it='',
  113. # ),
  114. # key=dict(
  115. # en=(
  116. # ''
  117. # ),
  118. # it=(
  119. # ''
  120. # ),
  121. # ),
  122. )
  123. async def _forward_to(update, bot, sender, addressee, is_admin=False):
  124. if update['text'].lower() in ['stop'] and is_admin:
  125. with bot.db as db:
  126. admin_record = db['users'].find_one(
  127. telegram_id=sender
  128. )
  129. session_record = db['talking_sessions'].find_one(
  130. admin=admin_record['id'],
  131. cancelled=0
  132. )
  133. other_user_record = db['users'].find_one(
  134. id=session_record['user']
  135. )
  136. await end_session(
  137. bot=bot,
  138. other_user_record=other_user_record,
  139. admin_record=admin_record
  140. )
  141. else:
  142. bot.set_individual_text_message_handler(
  143. await async_wrapper(
  144. _forward_to,
  145. sender=sender,
  146. addressee=addressee,
  147. is_admin=is_admin
  148. ),
  149. sender
  150. )
  151. await bot.forward_message(
  152. chat_id=addressee,
  153. update=update
  154. )
  155. return
  156. def get_talk_panel(bot, update, user_record=None, text=''):
  157. """Return text and reply markup of talk panel.
  158. `text` may be:
  159. - `user_id` as string
  160. - `username` as string
  161. - `''` (empty string) for main menu (default)
  162. """
  163. users = []
  164. if len(text):
  165. with bot.db as db:
  166. if text.isnumeric():
  167. users = list(
  168. db['users'].find(id=int(text))
  169. )
  170. else:
  171. users = list(
  172. db.query(
  173. "SELECT * "
  174. "FROM users "
  175. "WHERE COALESCE( "
  176. " first_name || last_name || username, "
  177. " last_name || username, "
  178. " first_name || username, "
  179. " username, "
  180. " first_name || last_name, "
  181. " last_name, "
  182. " first_name "
  183. f") LIKE '%{text}%' "
  184. "ORDER BY LOWER( "
  185. " COALESCE( "
  186. " first_name || last_name || username, "
  187. " last_name || username, "
  188. " first_name || username, "
  189. " username, "
  190. " first_name || last_name, "
  191. " last_name, "
  192. " first_name "
  193. " ) "
  194. ") "
  195. "LIMIT 26"
  196. )
  197. )
  198. if len(text) == 0:
  199. text = (
  200. bot.get_message(
  201. 'talk',
  202. 'help_text',
  203. update=update,
  204. user_record=user_record,
  205. q=escape_html_chars(
  206. remove_html_tags(text)
  207. )
  208. )
  209. )
  210. reply_markup = make_inline_keyboard(
  211. [
  212. make_button(
  213. bot.get_message(
  214. 'talk', 'search_button',
  215. update=update, user_record=user_record
  216. ),
  217. prefix='talk:///',
  218. data=['search']
  219. )
  220. ],
  221. 1
  222. )
  223. elif len(users) == 0:
  224. text = (
  225. bot.get_message(
  226. 'talk',
  227. 'user_not_found',
  228. update=update,
  229. user_record=user_record,
  230. q=escape_html_chars(
  231. remove_html_tags(text)
  232. )
  233. )
  234. )
  235. reply_markup = make_inline_keyboard(
  236. [
  237. make_button(
  238. bot.get_message(
  239. 'talk', 'search_button',
  240. update=update, user_record=user_record
  241. ),
  242. prefix='talk:///',
  243. data=['search']
  244. )
  245. ],
  246. 1
  247. )
  248. else:
  249. text = "{header}\n\n{u}{etc}".format(
  250. header=bot.get_message(
  251. 'talk', 'select_user',
  252. update=update, user_record=user_record
  253. ),
  254. u=line_drawing_unordered_list(
  255. [
  256. get_user(user)
  257. for user in users[:25]
  258. ]
  259. ),
  260. etc=(
  261. '\n\n[...]'
  262. if len(users) > 25
  263. else ''
  264. )
  265. )
  266. reply_markup = make_inline_keyboard(
  267. [
  268. make_button(
  269. '👤 {u}'.format(
  270. u=get_user(
  271. {
  272. key: val
  273. for key, val in user.items()
  274. if key in (
  275. 'first_name',
  276. 'last_name',
  277. 'username'
  278. )
  279. }
  280. )
  281. ),
  282. prefix='talk:///',
  283. data=[
  284. 'select',
  285. user['id']
  286. ]
  287. )
  288. for user in users[:25]
  289. ],
  290. 2
  291. )
  292. return text, reply_markup
  293. async def _talk_command(bot, update, user_record):
  294. text = get_cleaned_text(
  295. update,
  296. bot,
  297. ['talk']
  298. )
  299. text, reply_markup = get_talk_panel(bot=bot, update=update,
  300. user_record=user_record, text=text)
  301. return dict(
  302. text=text,
  303. parse_mode='HTML',
  304. reply_markup=reply_markup,
  305. )
  306. async def start_session(bot, other_user_record, admin_record):
  307. """Start talking session between user and admin.
  308. Register session in database, so it gets loaded before message_loop starts.
  309. Send a notification both to admin and user, set custom parsers and return.
  310. """
  311. with bot.db as db:
  312. db['talking_sessions'].insert(
  313. dict(
  314. user=other_user_record['id'],
  315. admin=admin_record['id'],
  316. cancelled=0
  317. )
  318. )
  319. await bot.send_message(
  320. chat_id=other_user_record['telegram_id'],
  321. text=bot.get_message(
  322. 'talk', 'user_warning',
  323. user_record=other_user_record,
  324. u=get_user(admin_record)
  325. )
  326. )
  327. await bot.send_message(
  328. chat_id=admin_record['telegram_id'],
  329. text=bot.get_message(
  330. 'talk', 'admin_warning',
  331. user_record=admin_record,
  332. u=get_user(other_user_record)
  333. ),
  334. reply_markup=make_inline_keyboard(
  335. [
  336. make_button(
  337. bot.get_message(
  338. 'talk', 'stop',
  339. user_record=admin_record
  340. ),
  341. prefix='talk:///',
  342. data=['stop', other_user_record['id']]
  343. )
  344. ]
  345. )
  346. )
  347. bot.set_individual_text_message_handler(
  348. await async_wrapper(
  349. _forward_to,
  350. sender=other_user_record['telegram_id'],
  351. addressee=admin_record['telegram_id'],
  352. is_admin=False
  353. ),
  354. other_user_record['telegram_id']
  355. )
  356. bot.set_individual_text_message_handler(
  357. await async_wrapper(
  358. _forward_to,
  359. sender=admin_record['telegram_id'],
  360. addressee=other_user_record['telegram_id'],
  361. is_admin=True
  362. ),
  363. admin_record['telegram_id']
  364. )
  365. return
  366. async def end_session(bot, other_user_record, admin_record):
  367. """End talking session between user and admin.
  368. Cancel session in database, so it will not be loaded anymore.
  369. Send a notification both to admin and user, clear custom parsers
  370. and return.
  371. """
  372. with bot.db as db:
  373. db['talking_sessions'].update(
  374. dict(
  375. admin=admin_record['id'],
  376. cancelled=1
  377. ),
  378. ['admin']
  379. )
  380. await bot.send_message(
  381. chat_id=other_user_record['telegram_id'],
  382. text=bot.get_message(
  383. 'talk', 'user_session_ended',
  384. user_record=other_user_record,
  385. u=get_user(admin_record)
  386. )
  387. )
  388. await bot.send_message(
  389. chat_id=admin_record['telegram_id'],
  390. text=bot.get_message(
  391. 'talk', 'admin_session_ended',
  392. user_record=admin_record,
  393. u=get_user(other_user_record)
  394. ),
  395. )
  396. for record in (admin_record, other_user_record, ):
  397. bot.remove_individual_text_message_handler(record['telegram_id'])
  398. return
  399. async def _talk_button(bot, update, user_record, data):
  400. telegram_id = user_record['telegram_id']
  401. command, *arguments = data
  402. result, text, reply_markup = '', '', None
  403. if command == 'search':
  404. bot.set_individual_text_message_handler(
  405. await async_wrapper(
  406. _talk_command,
  407. ),
  408. update
  409. )
  410. text = bot.get_message(
  411. 'talk', 'instructions',
  412. update=update, user_record=user_record
  413. )
  414. reply_markup = None
  415. elif command == 'select':
  416. if (
  417. len(arguments) < 1
  418. or type(arguments[0]) is not int
  419. ):
  420. result = "Errore!"
  421. else:
  422. with bot.db as db:
  423. other_user_record = db['users'].find_one(
  424. id=arguments[0]
  425. )
  426. admin_record = db['users'].find_one(
  427. telegram_id=telegram_id
  428. )
  429. await start_session(
  430. bot,
  431. other_user_record=other_user_record,
  432. admin_record=admin_record
  433. )
  434. elif command == 'stop':
  435. if (
  436. len(arguments) < 1
  437. or type(arguments[0]) is not int
  438. ):
  439. result = "Errore!"
  440. elif not Confirmator.get('stop_bots').confirm(telegram_id):
  441. result = bot.get_message(
  442. 'talk', 'end_session',
  443. update=update, user_record=user_record
  444. )
  445. else:
  446. with bot.db as db:
  447. other_user_record = db['users'].find_one(
  448. id=arguments[0]
  449. )
  450. admin_record = db['users'].find_one(
  451. telegram_id=telegram_id
  452. )
  453. await end_session(
  454. bot,
  455. other_user_record=other_user_record,
  456. admin_record=admin_record
  457. )
  458. text = "Session ended."
  459. reply_markup = None
  460. if text:
  461. return dict(
  462. text=result,
  463. edit=dict(
  464. text=text,
  465. parse_mode='HTML',
  466. reply_markup=reply_markup,
  467. disable_web_page_preview=True
  468. )
  469. )
  470. return result
  471. default_admin_messages = {
  472. 'talk_command': {
  473. 'description': {
  474. 'en': "Choose a user and forward messages to each other",
  475. 'it': "Scegli un utente e il bot farà da tramite inoltrando a "
  476. "ognuno i messaggi dell'altro finché non terminerai la "
  477. "sessione"
  478. }
  479. },
  480. 'restart_command': {
  481. 'description': {
  482. 'en': "Restart bots",
  483. 'it': "Riavvia i bot"
  484. },
  485. 'restart_scheduled_message': {
  486. 'en': "Bots are being restarted, after pulling from repository.",
  487. 'it': "I bot verranno riavviati in pochi secondi, caricando "
  488. "prima le eventuali modifiche al codice."
  489. },
  490. 'restart_completed_message': {
  491. 'en': "<i>Restart was successful.</i>",
  492. 'it': "<i>Restart avvenuto con successo.</i>"
  493. }
  494. },
  495. 'stop_command': {
  496. 'description': {
  497. 'en': "Stop bots",
  498. 'it': "Ferma i bot"
  499. },
  500. 'text': {
  501. 'en': "Are you sure you want to stop all bots?\n"
  502. "To make them start again you will have to ssh-log "
  503. "in server.\n\n"
  504. "To restart the bots remotely use the /restart command "
  505. "instead (before starting over, a <code>git pull</code> "
  506. "is performed).",
  507. 'it': "Sei sicuro di voler fermare i bot?\n"
  508. "Per farli ripartire dovrai accedere al server.\n\n"
  509. "Per far ripartire i bot da remoto usa invece il comando "
  510. "/restart (prima di ripartire farò un "
  511. "<code>git pull</code>)."
  512. }
  513. },
  514. 'stop_button': {
  515. 'stop_text': {
  516. 'en': "Stop bots",
  517. 'it': "Ferma i bot"
  518. },
  519. 'cancel': {
  520. 'en': "Cancel",
  521. 'it': "Annulla"
  522. },
  523. 'confirm': {
  524. 'en': "Do you really want to stop all bots?",
  525. 'it': "Vuoi davvero fermare tutti i bot?"
  526. },
  527. 'stopping': {
  528. 'en': "Stopping bots...",
  529. 'it': "Arresto in corso..."
  530. },
  531. 'cancelled': {
  532. 'en': "Operation was cancelled",
  533. 'it': "Operazione annullata"
  534. }
  535. },
  536. 'db_command': {
  537. 'description': {
  538. 'en': "Ask for bot database via Telegram",
  539. 'it': "Ricevi il database del bot via Telegram"
  540. },
  541. 'not_sqlite': {
  542. 'en': "Only SQLite databases may be sent via Telegram, since they "
  543. "are single-file databases.\n"
  544. "This bot has a `{db_type}` database.",
  545. 'it': "Via Telegram possono essere inviati solo database SQLite, "
  546. "in quanto composti di un solo file.\n"
  547. "Questo bot ha invece un database `{db_type}`."
  548. },
  549. 'file_caption': {
  550. 'en': "Here is bot database.",
  551. 'it': "Ecco il database!"
  552. },
  553. 'db_sent': {
  554. 'en': "Database sent.",
  555. 'it': "Database inviato."
  556. }
  557. },
  558. 'query_command': {
  559. 'description': {
  560. 'en': "Receive the result of a SQL query performed on bot "
  561. "database",
  562. 'it': "Ricevi il risultato di una query SQL sul database del bot"
  563. },
  564. 'help': {
  565. 'en': "Write a SQL query to be run on bot database.\n\n"
  566. "<b>Example</b>\n"
  567. "<code>/query SELECT * FROM users WHERE 0</code>",
  568. 'it': "Invia una query SQL da eseguire sul database del bot.\n\n"
  569. "<b>Esempio</b>\n"
  570. "<code>/query SELECT * FROM users WHERE 0</code>"
  571. },
  572. 'no_iterable': {
  573. 'en': "No result to show was returned",
  574. 'it': "La query non ha restituito risultati da mostrare"
  575. },
  576. 'exception': {
  577. 'en': "The query threw this error:",
  578. 'it': "La query ha dato questo errore:"
  579. },
  580. 'result': {
  581. 'en': "Query result",
  582. 'it': "Risultato della query"
  583. }
  584. },
  585. 'select_command': {
  586. 'description': {
  587. 'en': "Receive the result of a SELECT query performed on bot "
  588. "database",
  589. 'it': "Ricevi il risultato di una query SQL di tipo SELECT "
  590. "sul database del bot"
  591. }
  592. },
  593. 'query_button': {
  594. 'error': {
  595. 'en': "Error!",
  596. 'it': "Errore!"
  597. },
  598. 'file_name': {
  599. 'en': "Query result.csv",
  600. 'it': "Risultato della query.csv"
  601. },
  602. 'empty_file': {
  603. 'en': "No result to show.",
  604. 'it': "Nessun risultato da mostrare."
  605. }
  606. },
  607. 'log_command': {
  608. 'description': {
  609. 'en': "Receive bot log file, if set",
  610. 'it': "Ricevi il file di log del bot, se impostato"
  611. },
  612. 'no_log': {
  613. 'en': "Sorry but no log file is set.\n"
  614. "To set it, use `bot.set_log_file_name` instance method or "
  615. "`Bot.set_class_log_file_name` class method.",
  616. 'it': "Spiacente ma il file di log non è stato impostato.\n"
  617. "Per impostarlo, usa il metodo d'istanza "
  618. "`bot.set_log_file_name` o il metodo di classe"
  619. "`Bot.set_class_log_file_name`."
  620. },
  621. 'sending_failure': {
  622. 'en': "Sending log file failed!\n\n"
  623. "<b>Error:</b>\n"
  624. "<code>{e}</code>",
  625. 'it': "Inviio del messaggio di log fallito!\n\n"
  626. "<b>Errore:</b>\n"
  627. "<code>{e}</code>"
  628. },
  629. 'here_is_log_file': {
  630. 'en': "Here is the complete log file.",
  631. 'it': "Ecco il file di log completo."
  632. },
  633. 'log_file_first_lines': {
  634. 'en': "Here are the first {lines} lines of the log file.",
  635. 'it': "Ecco le prime {lines} righe del file di log."
  636. },
  637. 'log_file_last_lines': {
  638. 'en': "Here are the last {lines} lines of the log file.\n"
  639. "Newer lines are at the top of the file.",
  640. 'it': "Ecco le ultime {lines} righe del file di log.\n"
  641. "L'ordine è cronologico, con i messaggi nuovi in alto."
  642. }
  643. },
  644. 'errors_command': {
  645. 'description': {
  646. 'en': "Receive bot error log file, if set",
  647. 'it': "Ricevi il file di log degli errori del bot, se impostato"
  648. },
  649. 'no_log': {
  650. 'en': "Sorry but no errors log file is set.\n"
  651. "To set it, use `bot.set_errors_file_name` instance method"
  652. "or `Bot.set_class_errors_file_name` class method.",
  653. 'it': "Spiacente ma il file di log degli errori non è stato "
  654. "impostato.\n"
  655. "Per impostarlo, usa il metodo d'istanza "
  656. "`bot.set_errors_file_name` o il metodo di classe"
  657. "`Bot.set_class_errors_file_name`."
  658. },
  659. 'empty_log': {
  660. 'en': "Congratulations! Errors log is empty!",
  661. 'it': "Congratulazioni! Il log degli errori è vuoto!"
  662. },
  663. 'sending_failure': {
  664. 'en': "Sending errors log file failed!\n\n"
  665. "<b>Error:</b>\n"
  666. "<code>{e}</code>",
  667. 'it': "Inviio del messaggio di log degli errori fallito!\n\n"
  668. "<b>Errore:</b>\n"
  669. "<code>{e}</code>"
  670. },
  671. 'here_is_log_file': {
  672. 'en': "Here is the complete errors log file.",
  673. 'it': "Ecco il file di log degli errori completo."
  674. },
  675. 'log_file_first_lines': {
  676. 'en': "Here are the first {lines} lines of the errors log file.",
  677. 'it': "Ecco le prime {lines} righe del file di log degli errori."
  678. },
  679. 'log_file_last_lines': {
  680. 'en': "Here are the last {lines} lines of the errors log file.\n"
  681. "Newer lines are at the top of the file.",
  682. 'it': "Ecco le ultime {lines} righe del file di log degli "
  683. "errori.\n"
  684. "L'ordine è cronologico, con i messaggi nuovi in alto."
  685. }
  686. },
  687. 'maintenance_command': {
  688. 'description': {
  689. 'en': "Put the bot under maintenance",
  690. 'it': "Metti il bot in manutenzione"
  691. },
  692. 'maintenance_started': {
  693. 'en': "<i>Bot has just been put under maintenance!</i>\n\n"
  694. "Until further notice, it will reply to users "
  695. "with the following message:\n\n"
  696. "{message}",
  697. 'it': "<i>Il bot è stato messo in manutenzione!</i>\n\n"
  698. "Fino a nuovo ordine, risponderà a tutti i comandi con il "
  699. "seguente messaggio\n\n"
  700. "{message}"
  701. },
  702. 'maintenance_ended': {
  703. 'en': "<i>Maintenance ended!</i>",
  704. 'it': "<i>Manutenzione terminata!</i>"
  705. }
  706. }
  707. }
  708. async def _restart_command(bot, update, user_record):
  709. with bot.db as db:
  710. db['restart_messages'].insert(
  711. dict(
  712. text=bot.get_message(
  713. 'admin', 'restart_command', 'restart_completed_message',
  714. update=update, user_record=user_record
  715. ),
  716. chat_id=update['chat']['id'],
  717. parse_mode='HTML',
  718. reply_to_message_id=update['message_id'],
  719. sent=None
  720. )
  721. )
  722. await bot.reply(
  723. update=update,
  724. text=bot.get_message(
  725. 'admin', 'restart_command', 'restart_scheduled_message',
  726. update=update, user_record=user_record
  727. )
  728. )
  729. bot.__class__.stop(message='=== RESTART ===', final_state=65)
  730. return
  731. async def _stop_command(bot, update, user_record):
  732. text = bot.get_message(
  733. 'admin', 'stop_command', 'text',
  734. update=update, user_record=user_record
  735. )
  736. reply_markup = make_inline_keyboard(
  737. [
  738. make_button(
  739. text=bot.get_message(
  740. 'admin', 'stop_button', 'stop_text',
  741. update=update, user_record=user_record
  742. ),
  743. prefix='stop:///',
  744. data=['stop']
  745. ),
  746. make_button(
  747. text=bot.get_message(
  748. 'admin', 'stop_button', 'cancel',
  749. update=update, user_record=user_record
  750. ),
  751. prefix='stop:///',
  752. data=['cancel']
  753. )
  754. ],
  755. 1
  756. )
  757. return dict(
  758. text=text,
  759. parse_mode='HTML',
  760. reply_markup=reply_markup
  761. )
  762. async def stop_bots(bot):
  763. """Stop bots in `bot` class."""
  764. await asyncio.sleep(2)
  765. bot.__class__.stop(message='=== STOP ===', final_state=0)
  766. return
  767. async def _stop_button(bot, update, user_record, data):
  768. result, text, reply_markup = '', '', None
  769. telegram_id = user_record['telegram_id']
  770. command = data[0] if len(data) > 0 else 'None'
  771. if command == 'stop':
  772. if not Confirmator.get('stop_bots').confirm(telegram_id):
  773. return bot.get_message(
  774. 'admin', 'stop_button', 'confirm',
  775. update=update, user_record=user_record
  776. )
  777. text = bot.get_message(
  778. 'admin', 'stop_button', 'stopping',
  779. update=update, user_record=user_record
  780. )
  781. result = text
  782. # Do not stop bots immediately, otherwise callback query
  783. # will never be answered
  784. asyncio.ensure_future(stop_bots(bot))
  785. elif command == 'cancel':
  786. text = bot.get_message(
  787. 'admin', 'stop_button', 'cancelled',
  788. update=update, user_record=user_record
  789. )
  790. result = text
  791. if text:
  792. return dict(
  793. text=result,
  794. edit=dict(
  795. text=text,
  796. parse_mode='HTML',
  797. reply_markup=reply_markup,
  798. disable_web_page_preview=True
  799. )
  800. )
  801. return result
  802. async def _send_bot_database(bot, update, user_record):
  803. if not all(
  804. [
  805. bot.db_url.endswith('.db'),
  806. bot.db_url.startswith('sqlite:///')
  807. ]
  808. ):
  809. return bot.get_message(
  810. 'admin', 'db_command', 'not_sqlite',
  811. update=update, user_record=user_record,
  812. db_type=bot.db_url.partition(':///')[0]
  813. )
  814. await bot.send_document(
  815. chat_id=user_record['telegram_id'],
  816. document_path=extract(bot.db.url, starter='sqlite:///'),
  817. caption=bot.get_message(
  818. 'admin', 'db_command', 'file_caption',
  819. update=update, user_record=user_record
  820. )
  821. )
  822. return bot.get_message(
  823. 'admin', 'db_command', 'db_sent',
  824. update=update, user_record=user_record
  825. )
  826. async def _query_command(bot, update, user_record):
  827. query = get_cleaned_text(
  828. update,
  829. bot,
  830. ['query', ]
  831. )
  832. query_id = None
  833. if len(query) == 0:
  834. return bot.get_message(
  835. 'admin', 'query_command', 'help',
  836. update=update, user_record=user_record
  837. )
  838. try:
  839. with bot.db as db:
  840. record = db.query(query)
  841. try:
  842. record = list(record)
  843. except ResourceClosedError:
  844. record = bot.get_message(
  845. 'admin', 'query_command', 'no_iterable',
  846. update=update, user_record=user_record
  847. )
  848. query_id = db['queries'].upsert(
  849. dict(
  850. query=query
  851. ),
  852. ['query']
  853. )
  854. if query_id is True:
  855. query_id = db['queries'].find_one(
  856. query=query
  857. )['id']
  858. result = json.dumps(record, indent=2)
  859. if len(result) > 500:
  860. result = (
  861. f"{result[:200]}\n" # First 200 characters
  862. f"[...]\n" # Interruption symbol
  863. f"{result[-200:]}" # Last 200 characters
  864. )
  865. except Exception as e:
  866. result = "{first_line}\n{e}".format(
  867. first_line=bot.get_message(
  868. 'admin', 'query_command', 'exception',
  869. update=update, user_record=user_record
  870. ),
  871. e=e
  872. )
  873. result = (
  874. "<b>{first_line}</b>\n".format(
  875. first_line=bot.get_message(
  876. 'admin', 'query_command', 'result',
  877. update=update, user_record=user_record
  878. )
  879. )
  880. + f"<code>{query}</code>\n\n"
  881. f"{result}"
  882. )
  883. if query_id:
  884. reply_markup = make_inline_keyboard(
  885. [
  886. make_button(
  887. text='CSV',
  888. prefix='db_query:///',
  889. data=['csv', query_id]
  890. )
  891. ],
  892. 1
  893. )
  894. else:
  895. reply_markup = None
  896. return dict(
  897. chat_id=update['chat']['id'],
  898. text=result,
  899. parse_mode='HTML',
  900. reply_markup=reply_markup
  901. )
  902. async def _query_button(bot, update, user_record, data):
  903. result, text, reply_markup = '', '', None
  904. command = data[0] if len(data) else 'default'
  905. error_message = bot.get_message(
  906. 'admin', 'query_button', 'error',
  907. user_record=user_record, update=update
  908. )
  909. if command == 'csv':
  910. if not len(data) > 1:
  911. return error_message
  912. if len(data) > 1:
  913. with bot.db as db:
  914. query_record = db['queries'].find_one(id=data[1])
  915. if query_record is None or 'query' not in query_record:
  916. return error_message
  917. await send_csv_file(
  918. bot=bot,
  919. chat_id=update['from']['id'],
  920. query=query_record['query'],
  921. file_name=bot.get_message(
  922. 'admin', 'query_button', 'file_name',
  923. user_record=user_record, update=update
  924. ),
  925. update=update,
  926. user_record=user_record
  927. )
  928. if text:
  929. return dict(
  930. text=result,
  931. edit=dict(
  932. text=text,
  933. reply_markup=reply_markup
  934. )
  935. )
  936. return result
  937. async def _log_command(bot, update, user_record):
  938. if bot.log_file_path is None:
  939. return bot.get_message(
  940. 'admin', 'log_command', 'no_log',
  941. update=update, user_record=user_record
  942. )
  943. # Always send log file in private chat
  944. chat_id = update['from']['id']
  945. text = get_cleaned_text(update, bot, ['log'])
  946. reversed_ = 'r' not in text
  947. text = text.strip('r')
  948. if text.isnumeric():
  949. limit = int(text)
  950. else:
  951. limit = 100
  952. if limit is None:
  953. sent = await bot.send_document(
  954. chat_id=chat_id,
  955. document_path=bot.log_file_path,
  956. caption=bot.get_message(
  957. 'admin', 'log_command', 'here_is_log_file',
  958. update=update, user_record=user_record
  959. )
  960. )
  961. else:
  962. sent = await send_part_of_text_file(
  963. bot=bot,
  964. update=update,
  965. user_record=user_record,
  966. chat_id=chat_id,
  967. file_path=bot.log_file_path,
  968. file_name=bot.log_file_name,
  969. caption=bot.get_message(
  970. 'admin', 'log_command', (
  971. 'log_file_last_lines'
  972. if reversed_
  973. else 'log_file_first_lines'
  974. ),
  975. update=update, user_record=user_record,
  976. lines=limit
  977. ),
  978. reversed_=reversed_,
  979. limit=limit
  980. )
  981. if isinstance(sent, Exception):
  982. return bot.get_message(
  983. 'admin', 'log_command', 'sending_failure',
  984. update=update, user_record=user_record,
  985. e=sent
  986. )
  987. return
  988. async def _errors_command(bot, update, user_record):
  989. # Always send errors log file in private chat
  990. chat_id = update['from']['id']
  991. if bot.errors_file_path is None:
  992. return bot.get_message(
  993. 'admin', 'errors_command', 'no_log',
  994. update=update, user_record=user_record
  995. )
  996. await bot.sendChatAction(chat_id=chat_id, action='upload_document')
  997. try:
  998. # Check that error log is not empty
  999. with open(bot.errors_file_path, 'r') as errors_file:
  1000. for line in errors_file:
  1001. break
  1002. else:
  1003. return bot.get_message(
  1004. 'admin', 'errors_command', 'empty_log',
  1005. update=update, user_record=user_record
  1006. )
  1007. # Send error log
  1008. sent = await bot.send_document(
  1009. # Always send log file in private chat
  1010. chat_id=chat_id,
  1011. document_path=bot.errors_file_path,
  1012. caption=bot.get_message(
  1013. 'admin', 'errors_command', 'here_is_log_file',
  1014. update=update, user_record=user_record
  1015. )
  1016. )
  1017. # Reset error log
  1018. with open(bot.errors_file_path, 'w') as errors_file:
  1019. errors_file.write('')
  1020. except Exception as e:
  1021. sent = e
  1022. # Notify failure
  1023. if isinstance(sent, Exception):
  1024. return bot.get_message(
  1025. 'admin', 'errors_command', 'sending_failure',
  1026. update=update, user_record=user_record,
  1027. e=sent
  1028. )
  1029. return
  1030. async def _maintenance_command(bot, update, user_record):
  1031. maintenance_status = bot.change_maintenance_status(
  1032. maintenance_message=get_cleaned_text(update, bot, ['maintenance'])
  1033. )
  1034. if maintenance_status:
  1035. return bot.get_message(
  1036. 'admin', 'maintenance_command', 'maintenance_started',
  1037. update=update, user_record=user_record,
  1038. message=bot.maintenance_message
  1039. )
  1040. return bot.get_message(
  1041. 'admin', 'maintenance_command', 'maintenance_ended',
  1042. update=update, user_record=user_record
  1043. )
  1044. def get_maintenance_exception_criterion(bot, allowed_command):
  1045. """Get a criterion to allow a type of updates during maintenance.
  1046. `bot` : davtelepot.bot.Bot() instance
  1047. `allowed_command` : str (command to be allowed during maintenance)
  1048. """
  1049. def criterion(update):
  1050. if 'message' not in update:
  1051. return False
  1052. update = update['message']
  1053. text = get_cleaned_text(update, bot, [])
  1054. if (
  1055. 'from' not in update
  1056. or 'id' not in update['from']
  1057. ):
  1058. return False
  1059. with bot.db as db:
  1060. user_record = db['users'].find_one(
  1061. telegram_id=update['from']['id']
  1062. )
  1063. if not bot.authorization_function(
  1064. update=update,
  1065. user_record=user_record,
  1066. authorization_level=2
  1067. ):
  1068. return False
  1069. return text == allowed_command.strip('/')
  1070. return criterion
  1071. def init(bot, talk_messages=None, admin_messages=None):
  1072. """Assign parsers, commands, buttons and queries to given `bot`."""
  1073. if talk_messages is None:
  1074. talk_messages = default_talk_messages
  1075. bot.messages['talk'] = talk_messages
  1076. if admin_messages is None:
  1077. admin_messages = default_admin_messages
  1078. bot.messages['admin'] = admin_messages
  1079. with bot.db as db:
  1080. if 'talking_sessions' not in db.tables:
  1081. db['talking_sessions'].insert(
  1082. dict(
  1083. user=0,
  1084. admin=0,
  1085. cancelled=1
  1086. )
  1087. )
  1088. allowed_during_maintenance = [
  1089. get_maintenance_exception_criterion(bot, command)
  1090. for command in ['stop', 'restart', 'maintenance']
  1091. ]
  1092. @bot.additional_task(when='BEFORE')
  1093. async def load_talking_sessions():
  1094. sessions = []
  1095. with bot.db as db:
  1096. for session in db.query(
  1097. """SELECT *
  1098. FROM talking_sessions
  1099. WHERE NOT cancelled
  1100. """
  1101. ):
  1102. sessions.append(
  1103. dict(
  1104. other_user_record=db['users'].find_one(
  1105. id=session['user']
  1106. ),
  1107. admin_record=db['users'].find_one(
  1108. id=session['admin']
  1109. ),
  1110. )
  1111. )
  1112. for session in sessions:
  1113. await start_session(
  1114. bot=bot,
  1115. other_user_record=session['other_user_record'],
  1116. admin_record=session['admin_record']
  1117. )
  1118. @bot.command(command='/talk', aliases=[], show_in_keyboard=False,
  1119. description=admin_messages['talk_command']['description'],
  1120. authorization_level='admin')
  1121. async def talk_command(bot, update, user_record):
  1122. return await _talk_command(bot, update, user_record)
  1123. @bot.button(prefix='talk:///', separator='|', authorization_level='admin')
  1124. async def talk_button(bot, update, user_record, data):
  1125. return await _talk_button(bot, update, user_record, data)
  1126. @bot.command(command='/restart', aliases=[], show_in_keyboard=False,
  1127. description=admin_messages['restart_command']['description'],
  1128. authorization_level='admin')
  1129. async def restart_command(bot, update, user_record):
  1130. return await _restart_command(bot, update, user_record)
  1131. @bot.additional_task('BEFORE')
  1132. async def send_restart_messages():
  1133. """Send restart messages at restart."""
  1134. with bot.db as db:
  1135. for restart_message in db['restart_messages'].find(sent=None):
  1136. asyncio.ensure_future(
  1137. bot.send_message(
  1138. **{
  1139. key: val
  1140. for key, val in restart_message.items()
  1141. if key in (
  1142. 'chat_id',
  1143. 'text',
  1144. 'parse_mode',
  1145. 'reply_to_message_id'
  1146. )
  1147. }
  1148. )
  1149. )
  1150. db['restart_messages'].update(
  1151. dict(
  1152. sent=datetime.datetime.now(),
  1153. id=restart_message['id']
  1154. ),
  1155. ['id'],
  1156. ensure=True
  1157. )
  1158. return
  1159. @bot.command(command='/stop', aliases=[], show_in_keyboard=False,
  1160. description=admin_messages['stop_command']['description'],
  1161. authorization_level='admin')
  1162. async def stop_command(bot, update, user_record):
  1163. return await _stop_command(bot, update, user_record)
  1164. @bot.button(prefix='stop:///', separator='|',
  1165. description=admin_messages['stop_command']['description'],
  1166. authorization_level='admin')
  1167. async def stop_button(bot, update, user_record, data):
  1168. return await _stop_button(bot, update, user_record, data)
  1169. @bot.command(command='/db', aliases=[], show_in_keyboard=False,
  1170. description=admin_messages['db_command']['description'],
  1171. authorization_level='admin')
  1172. async def send_bot_database(bot, update, user_record):
  1173. return await _send_bot_database(bot, update, user_record)
  1174. @bot.command(command='/query', aliases=[], show_in_keyboard=False,
  1175. description=admin_messages['query_command']['description'],
  1176. authorization_level='admin')
  1177. async def query_command(bot, update, user_record):
  1178. return await _query_command(bot, update, user_record)
  1179. @bot.command(command='/select', aliases=[], show_in_keyboard=False,
  1180. description=admin_messages['select_command']['description'],
  1181. authorization_level='admin')
  1182. async def select_command(bot, update, user_record):
  1183. return await _query_command(bot, update, user_record)
  1184. @bot.button(prefix='db_query:///', separator='|',
  1185. description=admin_messages['query_command']['description'],
  1186. authorization_level='admin')
  1187. async def query_button(bot, update, user_record, data):
  1188. return await _query_button(bot, update, user_record, data)
  1189. @bot.command(command='/log', aliases=[], show_in_keyboard=False,
  1190. description=admin_messages['log_command']['description'],
  1191. authorization_level='admin')
  1192. async def log_command(bot, update, user_record):
  1193. return await _log_command(bot, update, user_record)
  1194. @bot.command(command='/errors', aliases=[], show_in_keyboard=False,
  1195. description=admin_messages['errors_command']['description'],
  1196. authorization_level='admin')
  1197. async def errors_command(bot, update, user_record):
  1198. return await _errors_command(bot, update, user_record)
  1199. for exception in allowed_during_maintenance:
  1200. bot.allow_during_maintenance(exception)
  1201. @bot.command(command='/maintenance', aliases=[], show_in_keyboard=False,
  1202. description=admin_messages[
  1203. 'maintenance_command']['description'],
  1204. authorization_level='admin')
  1205. async def maintenance_command(bot, update, user_record):
  1206. return await _maintenance_command(bot, update, user_record)