Queer European MD passionate about IT

administration_tools.py 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999
  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. import logging
  14. import re
  15. import types
  16. from collections import OrderedDict
  17. from typing import Union, List, Tuple
  18. # Third party modules
  19. from sqlalchemy.exc import ResourceClosedError
  20. # Project modules
  21. from . import messages
  22. from .bot import Bot
  23. from .utilities import (
  24. async_wrapper, CachedPage, Confirmator, extract, get_cleaned_text,
  25. get_user, escape_html_chars, line_drawing_unordered_list, make_button,
  26. make_inline_keyboard, remove_html_tags, send_part_of_text_file,
  27. send_csv_file, make_lines_of_buttons
  28. )
  29. # Use this parameter in SQL `LIMIT x OFFSET y` clauses
  30. rows_number_limit = 10
  31. command_description_parser = re.compile(r'(?P<command>\w+)(\s?-\s?(?P<description>.*))?')
  32. async def _forward_to(update,
  33. bot: Bot,
  34. sender,
  35. addressee,
  36. is_admin=False):
  37. if update['text'].lower() in ['stop'] and is_admin:
  38. with bot.db as db:
  39. admin_record = db['users'].find_one(
  40. telegram_id=sender
  41. )
  42. session_record = db['talking_sessions'].find_one(
  43. admin=admin_record['id'],
  44. cancelled=0
  45. )
  46. other_user_record = db['users'].find_one(
  47. id=session_record['user']
  48. )
  49. await end_session(
  50. bot=bot,
  51. other_user_record=other_user_record,
  52. admin_record=admin_record
  53. )
  54. else:
  55. bot.set_individual_text_message_handler(
  56. await async_wrapper(
  57. _forward_to,
  58. sender=sender,
  59. addressee=addressee,
  60. is_admin=is_admin
  61. ),
  62. sender
  63. )
  64. await bot.forward_message(
  65. chat_id=addressee,
  66. update=update
  67. )
  68. return
  69. def get_talk_panel(bot: Bot,
  70. update,
  71. user_record=None,
  72. text: str = ''):
  73. """Return text and reply markup of talk panel.
  74. `text` may be:
  75. - `user_id` as string
  76. - `username` as string
  77. - `''` (empty string) for main menu (default)
  78. """
  79. users = []
  80. if len(text):
  81. with bot.db as db:
  82. if text.isnumeric():
  83. users = list(
  84. db['users'].find(id=int(text))
  85. )
  86. else:
  87. users = list(
  88. db.query(
  89. "SELECT * "
  90. "FROM users "
  91. "WHERE COALESCE( "
  92. " first_name || last_name || username, "
  93. " last_name || username, "
  94. " first_name || username, "
  95. " username, "
  96. " first_name || last_name, "
  97. " last_name, "
  98. " first_name "
  99. f") LIKE '%{text}%' "
  100. "ORDER BY LOWER( "
  101. " COALESCE( "
  102. " first_name || last_name || username, "
  103. " last_name || username, "
  104. " first_name || username, "
  105. " username, "
  106. " first_name || last_name, "
  107. " last_name, "
  108. " first_name "
  109. " ) "
  110. ") "
  111. "LIMIT 26"
  112. )
  113. )
  114. if len(text) == 0:
  115. text = (
  116. bot.get_message(
  117. 'talk',
  118. 'help_text',
  119. update=update,
  120. user_record=user_record,
  121. q=escape_html_chars(
  122. remove_html_tags(text)
  123. )
  124. )
  125. )
  126. reply_markup = make_inline_keyboard(
  127. [
  128. make_button(
  129. bot.get_message(
  130. 'talk', 'search_button',
  131. update=update, user_record=user_record
  132. ),
  133. prefix='talk:///',
  134. data=['search']
  135. )
  136. ],
  137. 1
  138. )
  139. elif len(users) == 0:
  140. text = (
  141. bot.get_message(
  142. 'talk',
  143. 'user_not_found',
  144. update=update,
  145. user_record=user_record,
  146. q=escape_html_chars(
  147. remove_html_tags(text)
  148. )
  149. )
  150. )
  151. reply_markup = make_inline_keyboard(
  152. [
  153. make_button(
  154. bot.get_message(
  155. 'talk', 'search_button',
  156. update=update, user_record=user_record
  157. ),
  158. prefix='talk:///',
  159. data=['search']
  160. )
  161. ],
  162. 1
  163. )
  164. else:
  165. text = "{header}\n\n{u}{etc}".format(
  166. header=bot.get_message(
  167. 'talk', 'select_user',
  168. update=update, user_record=user_record
  169. ),
  170. u=line_drawing_unordered_list(
  171. [
  172. get_user(user)
  173. for user in users[:25]
  174. ]
  175. ),
  176. etc=(
  177. '\n\n[...]'
  178. if len(users) > 25
  179. else ''
  180. )
  181. )
  182. reply_markup = make_inline_keyboard(
  183. [
  184. make_button(
  185. '👤 {u}'.format(
  186. u=get_user(
  187. {
  188. key: val
  189. for key, val in user.items()
  190. if key in ('first_name',
  191. 'last_name',
  192. 'username')
  193. }
  194. )
  195. ),
  196. prefix='talk:///',
  197. data=[
  198. 'select',
  199. user['id']
  200. ]
  201. )
  202. for user in users[:25]
  203. ],
  204. 2
  205. )
  206. return text, reply_markup
  207. async def _talk_command(bot: Bot,
  208. update,
  209. user_record):
  210. text = get_cleaned_text(
  211. update,
  212. bot,
  213. ['talk']
  214. )
  215. text, reply_markup = get_talk_panel(bot=bot, update=update,
  216. user_record=user_record, text=text)
  217. return dict(
  218. text=text,
  219. parse_mode='HTML',
  220. reply_markup=reply_markup,
  221. )
  222. async def start_session(bot: Bot,
  223. other_user_record,
  224. admin_record):
  225. """Start talking session between user and admin.
  226. Register session in database, so it gets loaded before message_loop starts.
  227. Send a notification both to admin and user, set custom parsers and return.
  228. """
  229. with bot.db as db:
  230. db['talking_sessions'].insert(
  231. dict(
  232. user=other_user_record['id'],
  233. admin=admin_record['id'],
  234. cancelled=0
  235. )
  236. )
  237. await bot.send_message(
  238. chat_id=other_user_record['telegram_id'],
  239. text=bot.get_message(
  240. 'talk', 'user_warning',
  241. user_record=other_user_record,
  242. u=get_user(admin_record)
  243. )
  244. )
  245. await bot.send_message(
  246. chat_id=admin_record['telegram_id'],
  247. text=bot.get_message(
  248. 'talk', 'admin_warning',
  249. user_record=admin_record,
  250. u=get_user(other_user_record)
  251. ),
  252. reply_markup=make_inline_keyboard(
  253. [
  254. make_button(
  255. bot.get_message(
  256. 'talk', 'stop',
  257. user_record=admin_record
  258. ),
  259. prefix='talk:///',
  260. data=['stop', other_user_record['id']]
  261. )
  262. ]
  263. )
  264. )
  265. bot.set_individual_text_message_handler(
  266. await async_wrapper(
  267. _forward_to,
  268. sender=other_user_record['telegram_id'],
  269. addressee=admin_record['telegram_id'],
  270. is_admin=False
  271. ),
  272. other_user_record['telegram_id']
  273. )
  274. bot.set_individual_text_message_handler(
  275. await async_wrapper(
  276. _forward_to,
  277. sender=admin_record['telegram_id'],
  278. addressee=other_user_record['telegram_id'],
  279. is_admin=True
  280. ),
  281. admin_record['telegram_id']
  282. )
  283. return
  284. async def end_session(bot: Bot,
  285. other_user_record,
  286. admin_record):
  287. """End talking session between user and admin.
  288. Cancel session in database, so it will not be loaded anymore.
  289. Send a notification both to admin and user, clear custom parsers
  290. and return.
  291. """
  292. with bot.db as db:
  293. db['talking_sessions'].update(
  294. dict(
  295. admin=admin_record['id'],
  296. cancelled=1
  297. ),
  298. ['admin']
  299. )
  300. await bot.send_message(
  301. chat_id=other_user_record['telegram_id'],
  302. text=bot.get_message(
  303. 'talk', 'user_session_ended',
  304. user_record=other_user_record,
  305. u=get_user(admin_record)
  306. )
  307. )
  308. await bot.send_message(
  309. chat_id=admin_record['telegram_id'],
  310. text=bot.get_message(
  311. 'talk', 'admin_session_ended',
  312. user_record=admin_record,
  313. u=get_user(other_user_record)
  314. ),
  315. )
  316. for record in (admin_record, other_user_record,):
  317. bot.remove_individual_text_message_handler(record['telegram_id'])
  318. return
  319. async def _talk_button(bot: Bot,
  320. update,
  321. user_record,
  322. data):
  323. telegram_id = user_record['telegram_id']
  324. command, *arguments = data
  325. result, text, reply_markup = '', '', None
  326. if command == 'search':
  327. bot.set_individual_text_message_handler(
  328. await async_wrapper(
  329. _talk_command,
  330. ),
  331. update
  332. )
  333. text = bot.get_message(
  334. 'talk', 'instructions',
  335. update=update, user_record=user_record
  336. )
  337. reply_markup = None
  338. elif command == 'select':
  339. if (
  340. len(arguments) < 1
  341. or type(arguments[0]) is not int
  342. ):
  343. result = "Errore!"
  344. else:
  345. with bot.db as db:
  346. other_user_record = db['users'].find_one(
  347. id=arguments[0]
  348. )
  349. admin_record = db['users'].find_one(
  350. telegram_id=telegram_id
  351. )
  352. await start_session(
  353. bot,
  354. other_user_record=other_user_record,
  355. admin_record=admin_record
  356. )
  357. elif command == 'stop':
  358. if (
  359. len(arguments) < 1
  360. or type(arguments[0]) is not int
  361. ):
  362. result = "Errore!"
  363. elif not Confirmator.get('stop_bots').confirm(telegram_id):
  364. result = bot.get_message(
  365. 'talk', 'end_session',
  366. update=update, user_record=user_record
  367. )
  368. else:
  369. with bot.db as db:
  370. other_user_record = db['users'].find_one(
  371. id=arguments[0]
  372. )
  373. admin_record = db['users'].find_one(
  374. telegram_id=telegram_id
  375. )
  376. await end_session(
  377. bot,
  378. other_user_record=other_user_record,
  379. admin_record=admin_record
  380. )
  381. text = "Session ended."
  382. reply_markup = None
  383. if text:
  384. return dict(
  385. text=result,
  386. edit=dict(
  387. text=text,
  388. parse_mode='HTML',
  389. reply_markup=reply_markup,
  390. disable_web_page_preview=True
  391. )
  392. )
  393. return result
  394. async def _restart_command(bot: Bot,
  395. update,
  396. user_record):
  397. with bot.db as db:
  398. db['restart_messages'].insert(
  399. dict(
  400. text=bot.get_message(
  401. 'admin', 'restart_command', 'restart_completed_message',
  402. update=update, user_record=user_record
  403. ),
  404. chat_id=update['chat']['id'],
  405. parse_mode='HTML',
  406. reply_to_message_id=update['message_id'],
  407. sent=None
  408. )
  409. )
  410. await bot.reply(
  411. update=update,
  412. text=bot.get_message(
  413. 'admin', 'restart_command', 'restart_scheduled_message',
  414. update=update, user_record=user_record
  415. )
  416. )
  417. bot.__class__.stop(message='=== RESTART ===', final_state=65)
  418. return
  419. async def _stop_command(bot: Bot,
  420. update,
  421. user_record):
  422. text = bot.get_message(
  423. 'admin', 'stop_command', 'text',
  424. update=update, user_record=user_record
  425. )
  426. reply_markup = make_inline_keyboard(
  427. [
  428. make_button(
  429. text=bot.get_message(
  430. 'admin', 'stop_button', 'stop_text',
  431. update=update, user_record=user_record
  432. ),
  433. prefix='stop:///',
  434. data=['stop']
  435. ),
  436. make_button(
  437. text=bot.get_message(
  438. 'admin', 'stop_button', 'cancel',
  439. update=update, user_record=user_record
  440. ),
  441. prefix='stop:///',
  442. data=['cancel']
  443. )
  444. ],
  445. 1
  446. )
  447. return dict(
  448. text=text,
  449. parse_mode='HTML',
  450. reply_markup=reply_markup
  451. )
  452. async def stop_bots(bot: Bot):
  453. """Stop bots in `bot` class."""
  454. await asyncio.sleep(2)
  455. bot.__class__.stop(message='=== STOP ===', final_state=0)
  456. return
  457. async def _stop_button(bot: Bot,
  458. update,
  459. user_record,
  460. data: List[Union[int, str]]):
  461. result, text, reply_markup = '', '', None
  462. telegram_id = user_record['telegram_id']
  463. command = data[0] if len(data) > 0 else 'None'
  464. if command == 'stop':
  465. if not Confirmator.get('stop_bots').confirm(telegram_id):
  466. return bot.get_message(
  467. 'admin', 'stop_button', 'confirm',
  468. update=update, user_record=user_record
  469. )
  470. text = bot.get_message(
  471. 'admin', 'stop_button', 'stopping',
  472. update=update, user_record=user_record
  473. )
  474. result = text
  475. # Do not stop bots immediately, otherwise callback query
  476. # will never be answered
  477. asyncio.ensure_future(stop_bots(bot))
  478. elif command == 'cancel':
  479. text = bot.get_message(
  480. 'admin', 'stop_button', 'cancelled',
  481. update=update, user_record=user_record
  482. )
  483. result = text
  484. if text:
  485. return dict(
  486. text=result,
  487. edit=dict(
  488. text=text,
  489. parse_mode='HTML',
  490. reply_markup=reply_markup,
  491. disable_web_page_preview=True
  492. )
  493. )
  494. return result
  495. async def _send_bot_database(bot, update, user_record):
  496. if not all(
  497. [
  498. bot.db_url.endswith('.db'),
  499. bot.db_url.startswith('sqlite:///')
  500. ]
  501. ):
  502. return bot.get_message(
  503. 'admin', 'db_command', 'not_sqlite',
  504. update=update, user_record=user_record,
  505. db_type=bot.db_url.partition(':///')[0]
  506. )
  507. await bot.send_document(
  508. chat_id=user_record['telegram_id'],
  509. document_path=extract(bot.db.url, starter='sqlite:///'),
  510. caption=bot.get_message(
  511. 'admin', 'db_command', 'file_caption',
  512. update=update, user_record=user_record
  513. )
  514. )
  515. return bot.get_message(
  516. 'admin', 'db_command', 'db_sent',
  517. update=update, user_record=user_record
  518. )
  519. async def _query_command(bot, update, user_record):
  520. query = get_cleaned_text(
  521. update,
  522. bot,
  523. ['query', ]
  524. )
  525. query_id = None
  526. if len(query) == 0:
  527. return bot.get_message(
  528. 'admin', 'query_command', 'help',
  529. update=update, user_record=user_record
  530. )
  531. try:
  532. with bot.db as db:
  533. record = db.query(query)
  534. try:
  535. record = list(record)
  536. except ResourceClosedError:
  537. record = bot.get_message(
  538. 'admin', 'query_command', 'no_iterable',
  539. update=update, user_record=user_record
  540. )
  541. query_id = db['queries'].upsert(
  542. dict(
  543. query=query
  544. ),
  545. ['query']
  546. )
  547. if query_id is True:
  548. query_id = db['queries'].find_one(
  549. query=query
  550. )['id']
  551. result = json.dumps(record, indent=2)
  552. if len(result) > 500:
  553. result = (
  554. f"{result[:200]}\n" # First 200 characters
  555. f"[...]\n" # Interruption symbol
  556. f"{result[-200:]}" # Last 200 characters
  557. )
  558. except Exception as e:
  559. result = "{first_line}\n{e}".format(
  560. first_line=bot.get_message(
  561. 'admin', 'query_command', 'exception',
  562. update=update, user_record=user_record
  563. ),
  564. e=e
  565. )
  566. result = (
  567. "<b>{first_line}</b>\n".format(
  568. first_line=bot.get_message(
  569. 'admin', 'query_command', 'result',
  570. update=update, user_record=user_record
  571. )
  572. )
  573. + f"<code>{query}</code>\n\n"
  574. f"{result}"
  575. )
  576. if query_id:
  577. reply_markup = make_inline_keyboard(
  578. [
  579. make_button(
  580. text='CSV',
  581. prefix='db_query:///',
  582. data=['csv', query_id]
  583. )
  584. ],
  585. 1
  586. )
  587. else:
  588. reply_markup = None
  589. return dict(
  590. chat_id=update['chat']['id'],
  591. text=result,
  592. parse_mode='HTML',
  593. reply_markup=reply_markup
  594. )
  595. async def _query_button(bot, update, user_record, data):
  596. result, text, reply_markup = '', '', None
  597. command = data[0] if len(data) else 'default'
  598. error_message = bot.get_message(
  599. 'admin', 'query_button', 'error',
  600. user_record=user_record, update=update
  601. )
  602. if command == 'csv':
  603. if not len(data) > 1:
  604. return error_message
  605. if len(data) > 1:
  606. with bot.db as db:
  607. query_record = db['queries'].find_one(id=data[1])
  608. if query_record is None or 'query' not in query_record:
  609. return error_message
  610. await send_csv_file(
  611. bot=bot,
  612. chat_id=update['from']['id'],
  613. query=query_record['query'],
  614. file_name=bot.get_message(
  615. 'admin', 'query_button', 'file_name',
  616. user_record=user_record, update=update
  617. ),
  618. update=update,
  619. user_record=user_record
  620. )
  621. if text:
  622. return dict(
  623. text=result,
  624. edit=dict(
  625. text=text,
  626. reply_markup=reply_markup
  627. )
  628. )
  629. return result
  630. async def _log_command(bot, update, user_record):
  631. if bot.log_file_path is None:
  632. return bot.get_message(
  633. 'admin', 'log_command', 'no_log',
  634. update=update, user_record=user_record
  635. )
  636. # Always send log file in private chat
  637. chat_id = update['from']['id']
  638. text = get_cleaned_text(update, bot, ['log'])
  639. reversed_ = 'r' not in text
  640. text = text.strip('r')
  641. if text.isnumeric():
  642. limit = int(text)
  643. else:
  644. limit = 100
  645. if limit is None:
  646. sent = await bot.send_document(
  647. chat_id=chat_id,
  648. document_path=bot.log_file_path,
  649. caption=bot.get_message(
  650. 'admin', 'log_command', 'here_is_log_file',
  651. update=update, user_record=user_record
  652. )
  653. )
  654. else:
  655. sent = await send_part_of_text_file(
  656. bot=bot,
  657. update=update,
  658. user_record=user_record,
  659. chat_id=chat_id,
  660. file_path=bot.log_file_path,
  661. file_name=bot.log_file_name,
  662. caption=bot.get_message(
  663. 'admin', 'log_command', (
  664. 'log_file_last_lines'
  665. if reversed_
  666. else 'log_file_first_lines'
  667. ),
  668. update=update, user_record=user_record,
  669. lines=limit
  670. ),
  671. reversed_=reversed_,
  672. limit=limit
  673. )
  674. if isinstance(sent, Exception):
  675. return bot.get_message(
  676. 'admin', 'log_command', 'sending_failure',
  677. update=update, user_record=user_record,
  678. e=sent
  679. )
  680. return
  681. async def _errors_command(bot, update, user_record):
  682. # Always send errors log file in private chat
  683. chat_id = update['from']['id']
  684. if bot.errors_file_path is None:
  685. return bot.get_message(
  686. 'admin', 'errors_command', 'no_log',
  687. update=update, user_record=user_record
  688. )
  689. await bot.sendChatAction(chat_id=chat_id, action='upload_document')
  690. try:
  691. # Check that error log is not empty
  692. with open(bot.errors_file_path, 'r') as errors_file:
  693. for _ in errors_file:
  694. break
  695. else:
  696. return bot.get_message(
  697. 'admin', 'errors_command', 'empty_log',
  698. update=update, user_record=user_record
  699. )
  700. # Send error log
  701. sent = await bot.send_document(
  702. # Always send log file in private chat
  703. chat_id=chat_id,
  704. document_path=bot.errors_file_path,
  705. caption=bot.get_message(
  706. 'admin', 'errors_command', 'here_is_log_file',
  707. update=update, user_record=user_record
  708. )
  709. )
  710. # Reset error log
  711. with open(bot.errors_file_path, 'w') as errors_file:
  712. errors_file.write('')
  713. except Exception as e:
  714. sent = e
  715. # Notify failure
  716. if isinstance(sent, Exception):
  717. return bot.get_message(
  718. 'admin', 'errors_command', 'sending_failure',
  719. update=update, user_record=user_record,
  720. e=sent
  721. )
  722. return
  723. async def _maintenance_command(bot, update, user_record):
  724. maintenance_message = get_cleaned_text(update, bot, ['maintenance'])
  725. if maintenance_message.startswith('{'):
  726. maintenance_message = json.loads(maintenance_message)
  727. maintenance_status = bot.change_maintenance_status(
  728. maintenance_message=maintenance_message
  729. )
  730. if maintenance_status:
  731. return bot.get_message(
  732. 'admin', 'maintenance_command', 'maintenance_started',
  733. update=update, user_record=user_record,
  734. message=bot.maintenance_message
  735. )
  736. return bot.get_message(
  737. 'admin', 'maintenance_command', 'maintenance_ended',
  738. update=update, user_record=user_record
  739. )
  740. def get_maintenance_exception_criterion(bot, allowed_command):
  741. """Get a criterion to allow a type of updates during maintenance.
  742. `bot` : davtelepot.bot.Bot() instance
  743. `allowed_command` : str (command to be allowed during maintenance)
  744. """
  745. def criterion(update):
  746. if 'message' not in update:
  747. return False
  748. update = update['message']
  749. text = get_cleaned_text(update, bot, [])
  750. if (
  751. 'from' not in update
  752. or 'id' not in update['from']
  753. ):
  754. return False
  755. with bot.db as db:
  756. user_record = db['users'].find_one(
  757. telegram_id=update['from']['id']
  758. )
  759. if not bot.authorization_function(
  760. update=update,
  761. user_record=user_record,
  762. authorization_level=2
  763. ):
  764. return False
  765. return text == allowed_command.strip('/')
  766. return criterion
  767. async def get_last_commit():
  768. """Get last commit hash and davtelepot version."""
  769. try:
  770. _subprocess = await asyncio.create_subprocess_exec(
  771. 'git', 'rev-parse', 'HEAD',
  772. stdout=asyncio.subprocess.PIPE,
  773. stderr=asyncio.subprocess.STDOUT
  774. )
  775. stdout, _ = await _subprocess.communicate()
  776. last_commit = stdout.decode().strip()
  777. except Exception as e:
  778. last_commit = f"{e}"
  779. if last_commit.startswith("fatal: not a git repository"):
  780. last_commit = "-"
  781. return last_commit
  782. async def get_new_versions(bot: Bot,
  783. notification_interval: datetime.timedelta = None) -> dict:
  784. """Get new versions of packages in bot.packages.
  785. Result: {"name": {"current": "0.1", "new": "0.2"}}
  786. """
  787. if notification_interval is None:
  788. notification_interval = datetime.timedelta(seconds=0)
  789. news = dict()
  790. for package in bot.packages:
  791. package_web_page = CachedPage.get(
  792. f'https://pypi.python.org/pypi/{package.__name__}/json',
  793. cache_time=2,
  794. mode='json'
  795. )
  796. web_page = await package_web_page.get_page()
  797. if web_page is None or isinstance(web_page, Exception):
  798. logging.error(f"Cannot get updates for {package.__name__}, "
  799. "skipping...")
  800. continue
  801. new_version = web_page['info']['version']
  802. current_version = package.__version__
  803. notification_record = bot.db['updates_notifications'].find_one(
  804. package=package.__name__,
  805. order_by=['-id'],
  806. _limit=1
  807. )
  808. if (
  809. new_version != current_version
  810. and (notification_record is None
  811. or notification_record['notified_at']
  812. < datetime.datetime.now() - notification_interval)
  813. ):
  814. news[package.__name__] = {
  815. 'current': current_version,
  816. 'new': new_version
  817. }
  818. return news
  819. async def _version_command(bot: Bot, update, user_record):
  820. last_commit = await get_last_commit()
  821. text = bot.get_message(
  822. 'admin', 'version_command', 'header',
  823. last_commit=last_commit,
  824. update=update, user_record=user_record
  825. ) + '\n\n'
  826. text += '\n'.join(
  827. f"<b>{package.__name__}</b>: "
  828. f"<code>{package.__version__}</code>"
  829. for package in bot.packages
  830. )
  831. temporary_message = await bot.send_message(
  832. text=text + '\n\n⏳ Checking for updates... ☑️',
  833. update=update,
  834. send_default_keyboard=False
  835. )
  836. news = await get_new_versions(bot=bot)
  837. if not news:
  838. text += '\n\n⌛️ All packages are updated! ✅'
  839. else:
  840. text += '\n\n' + bot.get_message(
  841. 'admin', 'updates_available', 'header',
  842. user_record=user_record
  843. ) + '\n\n'
  844. text += '\n'.join(
  845. f"<b>{package}</b>: "
  846. f"<code>{versions['current']}</code> —> "
  847. f"<code>{versions['new']}</code>"
  848. for package, versions in news.items()
  849. )
  850. await bot.edit_message_text(
  851. text=text,
  852. update=temporary_message
  853. )
  854. async def notify_new_version(bot: Bot):
  855. """Notify `bot` administrators about new versions.
  856. Notify admins when last commit and/or davtelepot version change.
  857. """
  858. last_commit = await get_last_commit()
  859. old_record = bot.db['version_history'].find_one(
  860. order_by=['-id']
  861. )
  862. current_versions = {
  863. f"{package.__name__}_version": package.__version__
  864. for package in bot.packages
  865. }
  866. current_versions['last_commit'] = last_commit
  867. if old_record is None:
  868. old_record = dict(
  869. updated_at=datetime.datetime.min,
  870. )
  871. for name in current_versions.keys():
  872. if name not in old_record:
  873. old_record[name] = None
  874. if any(
  875. old_record[name] != current_version
  876. for name, current_version in current_versions.items()
  877. ):
  878. bot.db['version_history'].insert(
  879. dict(
  880. updated_at=datetime.datetime.now(),
  881. **current_versions
  882. )
  883. )
  884. for admin in bot.administrators:
  885. text = bot.get_message(
  886. 'admin', 'new_version', 'title',
  887. user_record=admin
  888. ) + '\n\n'
  889. if last_commit != old_record['last_commit']:
  890. text += bot.get_message(
  891. 'admin', 'new_version', 'last_commit',
  892. old_record=old_record,
  893. new_record=current_versions,
  894. user_record=admin
  895. ) + '\n\n'
  896. text += '\n'.join(
  897. f"<b>{name[:-len('_version')]}</b>: "
  898. f"<code>{old_record[name]}</code> —> "
  899. f"<code>{current_version}</code>"
  900. for name, current_version in current_versions.items()
  901. if name not in ('last_commit', )
  902. and current_version != old_record[name]
  903. )
  904. await bot.send_message(
  905. chat_id=admin['telegram_id'],
  906. disable_notification=True,
  907. text=text
  908. )
  909. return
  910. async def get_package_updates(bot: Bot,
  911. monitoring_interval: Union[
  912. int, datetime.timedelta
  913. ] = 60 * 60,
  914. notification_interval: Union[
  915. int, datetime.timedelta
  916. ] = 60 * 60 * 24):
  917. if isinstance(monitoring_interval, datetime.timedelta):
  918. monitoring_interval = monitoring_interval.total_seconds()
  919. if type(notification_interval) is int:
  920. notification_interval = datetime.timedelta(
  921. seconds=notification_interval
  922. )
  923. while 1:
  924. news = await get_new_versions(bot=bot,
  925. notification_interval=notification_interval)
  926. if news:
  927. for admin in bot.administrators:
  928. text = bot.get_message(
  929. 'admin', 'updates_available', 'header',
  930. user_record=admin
  931. ) + '\n\n'
  932. text += '\n'.join(
  933. f"<b>{package}</b>: "
  934. f"<code>{versions['current']}</code> —> "
  935. f"<code>{versions['new']}</code>"
  936. for package, versions in news.items()
  937. )
  938. await bot.send_message(
  939. chat_id=admin['telegram_id'],
  940. disable_notification=True,
  941. text=text
  942. )
  943. bot.db['updates_notifications'].insert_many(
  944. [
  945. {
  946. "package": package,
  947. "version": information['new'],
  948. 'notified_at': datetime.datetime.now()
  949. }
  950. for package, information in news.items()
  951. ]
  952. )
  953. await asyncio.sleep(monitoring_interval)
  954. async def _send_start_messages(bot: Bot):
  955. """Send restart messages at restart."""
  956. for restart_message in bot.db['restart_messages'].find(sent=None):
  957. asyncio.ensure_future(
  958. bot.send_message(
  959. **{
  960. key: val
  961. for key, val in restart_message.items()
  962. if key in (
  963. 'chat_id',
  964. 'text',
  965. 'parse_mode',
  966. 'reply_to_message_id'
  967. )
  968. }
  969. )
  970. )
  971. bot.db['restart_messages'].update(
  972. dict(
  973. sent=datetime.datetime.now(),
  974. id=restart_message['id']
  975. ),
  976. ['id'],
  977. ensure=True
  978. )
  979. return
  980. async def _load_talking_sessions(bot: Bot):
  981. sessions = []
  982. for session in bot.db.query(
  983. """SELECT *
  984. FROM talking_sessions
  985. WHERE NOT cancelled
  986. """
  987. ):
  988. sessions.append(
  989. dict(
  990. other_user_record=bot.db['users'].find_one(
  991. id=session['user']
  992. ),
  993. admin_record=bot.db['users'].find_one(
  994. id=session['admin']
  995. ),
  996. )
  997. )
  998. for session in sessions:
  999. await start_session(
  1000. bot=bot,
  1001. other_user_record=session['other_user_record'],
  1002. admin_record=session['admin_record']
  1003. )
  1004. def get_current_commands(bot: Bot, language: str = None) -> List[dict]:
  1005. return sorted(
  1006. [
  1007. {
  1008. 'command': name,
  1009. 'description': bot.get_message(
  1010. messages=information['description'],
  1011. language=language
  1012. )
  1013. }
  1014. for name, information in bot.commands.items()
  1015. if 'description' in information
  1016. and information['description']
  1017. and 'authorization_level' in information
  1018. and information['authorization_level'] in ('registered_user', 'everybody',)
  1019. ],
  1020. key=(lambda c: c['command'])
  1021. )
  1022. def get_custom_commands(bot: Bot, language: str = None) -> List[dict]:
  1023. additional_commands = [
  1024. {
  1025. 'command': record['command'],
  1026. 'description': record['description']
  1027. }
  1028. for record in bot.db['bot_father_commands'].find(
  1029. cancelled=None,
  1030. hidden=False
  1031. )
  1032. ]
  1033. hidden_commands_names = [
  1034. record['command']
  1035. for record in bot.db['bot_father_commands'].find(
  1036. cancelled=None,
  1037. hidden=True
  1038. )
  1039. ]
  1040. return sorted(
  1041. [
  1042. command
  1043. for command in (get_current_commands(bot=bot, language=language)
  1044. + additional_commands)
  1045. if command['command'] not in hidden_commands_names
  1046. ],
  1047. key=(lambda c: c['command'])
  1048. )
  1049. async def _father_command(bot, language):
  1050. modes = [
  1051. {
  1052. key: (
  1053. bot.get_message(messages=val,
  1054. language=language)
  1055. if isinstance(val, dict)
  1056. else val
  1057. )
  1058. for key, val in mode.items()
  1059. }
  1060. for mode in bot.messages['admin']['father_command']['modes']
  1061. ]
  1062. text = "\n\n".join(
  1063. [
  1064. bot.get_message(
  1065. 'admin', 'father_command', 'title',
  1066. language=language
  1067. )
  1068. ] + [
  1069. "{m[symbol]} {m[name]}\n{m[description]}".format(m=mode)
  1070. for mode in modes
  1071. ]
  1072. )
  1073. reply_markup = make_inline_keyboard(
  1074. [
  1075. make_button(
  1076. text="{m[symbol]} {m[name]}".format(m=mode),
  1077. prefix='father:///',
  1078. delimiter='|',
  1079. data=[mode['id']]
  1080. )
  1081. for mode in modes
  1082. ],
  1083. 2
  1084. )
  1085. return dict(
  1086. text=text,
  1087. reply_markup=reply_markup
  1088. )
  1089. def browse_bot_father_settings_records(bot: Bot,
  1090. language: str,
  1091. page: int = 0) -> Tuple[str, str, dict]:
  1092. """Return a reply keyboard to edit bot father settings records."""
  1093. result, text, reply_markup = '', '', None
  1094. records = list(
  1095. bot.db['bot_father_commands'].find(
  1096. cancelled=None,
  1097. _limit=(rows_number_limit + 1),
  1098. _offset=(page * rows_number_limit)
  1099. )
  1100. )
  1101. for record in bot.db.query(
  1102. "SELECT COUNT(*) AS c "
  1103. "FROM bot_father_commands "
  1104. "WHERE cancelled IS NULL"
  1105. ):
  1106. records_count = record['c']
  1107. break
  1108. else:
  1109. records_count = 0
  1110. text = bot.get_message(
  1111. 'admin', 'father_command', 'settings', 'browse_records',
  1112. language=language,
  1113. record_interval=((page * rows_number_limit + 1) if records else 0,
  1114. min((page + 1) * rows_number_limit, len(records)),
  1115. records_count),
  1116. commands_list='\n'.join(
  1117. f"{'➖' if record['hidden'] else '➕'} {record['command']}"
  1118. for record in records[:rows_number_limit]
  1119. )
  1120. )
  1121. buttons = make_lines_of_buttons(
  1122. [
  1123. make_button(
  1124. text=f"{'➖' if record['hidden'] else '➕'} {record['command']}",
  1125. prefix='father:///',
  1126. delimiter='|',
  1127. data=['settings', 'edit', 'select', record['id']]
  1128. )
  1129. for record in records[:rows_number_limit]
  1130. ],
  1131. 3
  1132. )
  1133. buttons += make_lines_of_buttons(
  1134. (
  1135. [
  1136. make_button(
  1137. text='⬅',
  1138. prefix='father:///',
  1139. delimiter='|',
  1140. data=['settings', 'edit', 'go', page - 1]
  1141. )
  1142. ]
  1143. if page > 0
  1144. else []
  1145. ) + [
  1146. make_button(
  1147. text=bot.get_message('admin', 'father_command', 'back',
  1148. language=language),
  1149. prefix='father:///',
  1150. delimiter='|',
  1151. data=['settings']
  1152. )
  1153. ] + (
  1154. [
  1155. make_button(
  1156. text='️➡️',
  1157. prefix='father:///',
  1158. delimiter='|',
  1159. data=['settings', 'edit', 'go', page + 1]
  1160. )
  1161. ]
  1162. if len(records) > rows_number_limit
  1163. else []
  1164. ),
  1165. 3
  1166. )
  1167. reply_markup = dict(
  1168. inline_keyboard=buttons
  1169. )
  1170. return result, text, reply_markup
  1171. def get_bot_father_settings_editor(mode: str,
  1172. record: OrderedDict = None):
  1173. """Get a coroutine to edit or create a record in bot father settings table.
  1174. Modes:
  1175. - add
  1176. - hide
  1177. """
  1178. async def bot_father_settings_editor(bot: Bot, update: dict,
  1179. language: str):
  1180. """Edit or create a record in bot father settings table."""
  1181. nonlocal record
  1182. if record is not None:
  1183. record_id = record['id']
  1184. else:
  1185. record_id = None
  1186. # Cancel if user used /cancel command, or remove trailing forward_slash
  1187. input_text = update['text']
  1188. if input_text.startswith('/'):
  1189. if language not in bot.messages['admin']['cancel']['lower']:
  1190. language = bot.default_language
  1191. if input_text.lower().endswith(bot.messages['admin']['cancel']['lower'][language]):
  1192. return bot.get_message(
  1193. 'admin', 'cancel', 'done',
  1194. language=language
  1195. )
  1196. else:
  1197. input_text = input_text[1:]
  1198. if record is None:
  1199. # Use regex compiled pattern to search for command and description
  1200. re_search = command_description_parser.search(input_text)
  1201. if re_search is None:
  1202. return bot.get_message(
  1203. 'admin', 'error', 'text',
  1204. language=language
  1205. )
  1206. re_search = re_search.groupdict()
  1207. command = re_search['command'].lower()
  1208. description = re_search['description']
  1209. else:
  1210. command = record['command']
  1211. description = input_text
  1212. error = None
  1213. # A description (str 3-256) is required
  1214. if mode in ('add', 'edit'):
  1215. if description is None or len(description) < 3:
  1216. error = 'missing_description'
  1217. elif type(description) is str and len(description) > 255:
  1218. error = 'description_too_long'
  1219. elif mode == 'add':
  1220. duplicate = bot.db['bot_father_commands'].find_one(
  1221. command=command,
  1222. cancelled=None
  1223. )
  1224. if duplicate:
  1225. error = 'duplicate_record'
  1226. if error:
  1227. text = bot.get_message(
  1228. 'admin', 'father_command', 'settings', 'modes',
  1229. 'add', 'error', error,
  1230. language=language
  1231. )
  1232. reply_markup = make_inline_keyboard(
  1233. [
  1234. make_button(
  1235. text=bot.get_message(
  1236. 'admin', 'father_command', 'back',
  1237. language=language
  1238. ),
  1239. prefix='father:///',
  1240. delimiter='|',
  1241. data=['settings']
  1242. )
  1243. ]
  1244. )
  1245. else:
  1246. table = bot.db['bot_father_commands']
  1247. new_record = dict(
  1248. command=command,
  1249. description=description,
  1250. hidden=(mode == 'hide'),
  1251. cancelled=None
  1252. )
  1253. if record_id is None:
  1254. record_id = table.insert(
  1255. new_record
  1256. )
  1257. else:
  1258. new_record['id'] = record_id
  1259. table.upsert(
  1260. new_record,
  1261. ['id']
  1262. )
  1263. text = bot.get_message(
  1264. 'admin', 'father_command', 'settings', 'modes',
  1265. mode, ('edit' if 'id' in new_record else 'add'), 'done',
  1266. command=command,
  1267. description=(description if description else '-'),
  1268. language=language
  1269. )
  1270. reply_markup = make_inline_keyboard(
  1271. [
  1272. make_button(
  1273. text=bot.get_message(
  1274. 'admin', 'father_command', 'settings', 'modes',
  1275. 'edit', 'button',
  1276. language=language
  1277. ),
  1278. prefix='father:///',
  1279. delimiter='|',
  1280. data=['settings', 'edit', 'select', record_id]
  1281. ), make_button(
  1282. text=bot.get_message(
  1283. 'admin', 'father_command', 'back',
  1284. language=language
  1285. ),
  1286. prefix='father:///',
  1287. delimiter='|',
  1288. data=['settings']
  1289. )
  1290. ],
  1291. 2
  1292. )
  1293. asyncio.ensure_future(
  1294. bot.delete_message(update=update)
  1295. )
  1296. return dict(
  1297. text=text,
  1298. reply_markup=reply_markup
  1299. )
  1300. return bot_father_settings_editor
  1301. async def edit_bot_father_settings_via_message(bot: Bot,
  1302. user_record: OrderedDict,
  1303. language: str,
  1304. mode: str,
  1305. record: OrderedDict = None):
  1306. result, text, reply_markup = '', '', None
  1307. modes = bot.messages['admin']['father_command']['settings']['modes']
  1308. if mode not in modes:
  1309. result = bot.get_message(
  1310. 'admin', 'father_command', 'error',
  1311. language=language
  1312. )
  1313. else:
  1314. result = bot.get_message(
  1315. ('add' if record is None else 'edit'), 'popup',
  1316. messages=modes[mode],
  1317. language=language,
  1318. command=(record['command'] if record is not None else None)
  1319. )
  1320. text = bot.get_message(
  1321. ('add' if record is None else 'edit'), 'text',
  1322. messages=modes[mode],
  1323. language=language,
  1324. command=(record['command'] if record is not None else None)
  1325. )
  1326. reply_markup = make_inline_keyboard(
  1327. [
  1328. make_button(
  1329. text=bot.get_message(
  1330. 'admin', 'cancel', 'button',
  1331. language=language,
  1332. ),
  1333. prefix='father:///',
  1334. delimiter='|',
  1335. data=['cancel']
  1336. )
  1337. ]
  1338. )
  1339. bot.set_individual_text_message_handler(
  1340. get_bot_father_settings_editor(mode=mode, record=record),
  1341. user_id=user_record['telegram_id'],
  1342. )
  1343. return result, text, reply_markup
  1344. async def _father_button(bot: Bot, user_record: OrderedDict,
  1345. language: str, data: list):
  1346. """Handle BotFather button.
  1347. Operational modes
  1348. - main: back to main page (see _father_command)
  1349. - get: show commands stored by @BotFather
  1350. - set: edit commands stored by @BotFather
  1351. """
  1352. result, text, reply_markup = '', '', None
  1353. command, *data = data
  1354. if command == 'cancel':
  1355. bot.remove_individual_text_message_handler(user_id=user_record['telegram_id'])
  1356. result = text = bot.get_message(
  1357. 'admin', 'cancel', 'done',
  1358. language=language
  1359. )
  1360. reply_markup = make_inline_keyboard(
  1361. [
  1362. make_button(
  1363. text=bot.get_message('admin', 'father_command', 'back',
  1364. language=language),
  1365. prefix='father:///',
  1366. delimiter='|',
  1367. data=['main']
  1368. )
  1369. ]
  1370. )
  1371. elif command == 'del':
  1372. if not Confirmator.get('del_bot_father_commands',
  1373. confirm_timedelta=3
  1374. ).confirm(user_record['id']):
  1375. return bot.get_message(
  1376. 'admin', 'confirm',
  1377. language=language
  1378. )
  1379. stored_commands = await bot.getMyCommands()
  1380. if not len(stored_commands):
  1381. text = bot.get_message(
  1382. 'admin', 'father_command', 'del', 'no_change',
  1383. language=language
  1384. )
  1385. else:
  1386. if isinstance(
  1387. await bot.setMyCommands([]),
  1388. Exception
  1389. ):
  1390. text = bot.get_message(
  1391. 'admin', 'father_command', 'del', 'error',
  1392. language=language
  1393. )
  1394. else:
  1395. text = bot.get_message(
  1396. 'admin', 'father_command', 'del', 'done',
  1397. language=language
  1398. )
  1399. reply_markup = make_inline_keyboard(
  1400. [
  1401. make_button(
  1402. text=bot.get_message('admin', 'father_command', 'back',
  1403. language=language),
  1404. prefix='father:///',
  1405. delimiter='|',
  1406. data=['main']
  1407. )
  1408. ]
  1409. )
  1410. elif command == 'get':
  1411. commands = await bot.getMyCommands()
  1412. if len(commands) == 0:
  1413. commands = bot.get_message(
  1414. 'admin', 'father_command', 'get', 'empty',
  1415. language=language,
  1416. commands=commands
  1417. )
  1418. else:
  1419. commands = '<code>' + '\n'.join(
  1420. "{c[command]} - {c[description]}".format(c=command)
  1421. for command in commands
  1422. ) + '</code>'
  1423. text = bot.get_message(
  1424. 'admin', 'father_command', 'get', 'panel',
  1425. language=language,
  1426. commands=commands
  1427. )
  1428. reply_markup = make_inline_keyboard(
  1429. [
  1430. make_button(
  1431. text=bot.get_message('admin', 'father_command', 'back',
  1432. language=language),
  1433. prefix='father:///',
  1434. delimiter='|',
  1435. data=['main']
  1436. )
  1437. ]
  1438. )
  1439. elif command == 'main':
  1440. return dict(
  1441. text='',
  1442. edit=(await _father_command(bot=bot, language=language))
  1443. )
  1444. elif command == 'set':
  1445. stored_commands = await bot.getMyCommands()
  1446. current_commands = get_custom_commands(bot=bot, language=language)
  1447. if len(data) > 0 and data[0] == 'confirm':
  1448. if not Confirmator.get('set_bot_father_commands',
  1449. confirm_timedelta=3
  1450. ).confirm(user_record['id']):
  1451. return bot.get_message(
  1452. 'admin', 'confirm',
  1453. language=language
  1454. )
  1455. if stored_commands == current_commands:
  1456. text = bot.get_message(
  1457. 'admin', 'father_command', 'set', 'no_change',
  1458. language=language
  1459. )
  1460. else:
  1461. if isinstance(
  1462. await bot.setMyCommands(current_commands),
  1463. Exception
  1464. ):
  1465. text = bot.get_message(
  1466. 'admin', 'father_command', 'set', 'error',
  1467. language=language
  1468. )
  1469. else:
  1470. text = bot.get_message(
  1471. 'admin', 'father_command', 'set', 'done',
  1472. language=language
  1473. )
  1474. reply_markup = make_inline_keyboard(
  1475. [
  1476. make_button(
  1477. text=bot.get_message('admin', 'father_command', 'back',
  1478. language=language),
  1479. prefix='father:///',
  1480. delimiter='|',
  1481. data=['main']
  1482. )
  1483. ]
  1484. )
  1485. else:
  1486. stored_commands_names = [c['command'] for c in stored_commands]
  1487. current_commands_names = [c['command'] for c in current_commands]
  1488. # Show preview of new, edited and removed commands
  1489. # See 'legend' in bot.messages['admin']['father_command']['set']
  1490. text = bot.get_message(
  1491. 'admin', 'father_command', 'set', 'header',
  1492. language=language
  1493. ) + '\n\n' + '\n\n'.join([
  1494. '\n'.join(
  1495. ('✅ ' if c in stored_commands
  1496. else '☑️ ' if c['command'] not in stored_commands_names
  1497. else '✏️') + c['command']
  1498. for c in current_commands
  1499. ),
  1500. '\n'.join(
  1501. f'❌ {command}'
  1502. for command in stored_commands_names
  1503. if command not in current_commands_names
  1504. ),
  1505. bot.get_message(
  1506. 'admin', 'father_command', 'set', 'legend',
  1507. language=language
  1508. )
  1509. ])
  1510. reply_markup = make_inline_keyboard(
  1511. [
  1512. make_button(
  1513. text=bot.get_message('admin', 'father_command', 'set',
  1514. 'button',
  1515. language=language),
  1516. prefix='father:///',
  1517. delimiter='|',
  1518. data=['set', 'confirm']
  1519. )
  1520. ] + [
  1521. make_button(
  1522. text=bot.get_message('admin', 'father_command', 'back',
  1523. language=language),
  1524. prefix='father:///',
  1525. delimiter='|',
  1526. data=['main']
  1527. )
  1528. ],
  1529. 1
  1530. )
  1531. elif command == 'settings':
  1532. if len(data) == 0:
  1533. additional_commands = '\n'.join(
  1534. f"{record['command']} - {record['description']}"
  1535. for record in bot.db['bot_father_commands'].find(
  1536. cancelled=None,
  1537. hidden=False
  1538. )
  1539. )
  1540. if not additional_commands:
  1541. additional_commands = '-'
  1542. hidden_commands = '\n'.join(
  1543. f"{record['command']}"
  1544. for record in bot.db['bot_father_commands'].find(
  1545. cancelled=None,
  1546. hidden=True
  1547. )
  1548. )
  1549. if not hidden_commands:
  1550. hidden_commands = '-'
  1551. text = bot.get_message(
  1552. 'admin', 'father_command', 'settings', 'panel',
  1553. language=language,
  1554. additional_commands=additional_commands,
  1555. hidden_commands=hidden_commands
  1556. )
  1557. modes = bot.messages['admin']['father_command']['settings']['modes']
  1558. reply_markup = make_inline_keyboard(
  1559. [
  1560. make_button(
  1561. text=modes[code]['symbol'] + ' ' + bot.get_message(
  1562. messages=modes[code]['name'],
  1563. language=language
  1564. ),
  1565. prefix='father:///',
  1566. delimiter='|',
  1567. data=['settings', code]
  1568. )
  1569. for code, mode in modes.items()
  1570. ] + [
  1571. make_button(
  1572. text=bot.get_message('admin', 'father_command', 'back',
  1573. language=language),
  1574. prefix='father:///',
  1575. delimiter='|',
  1576. data=['main']
  1577. )
  1578. ],
  1579. 2
  1580. )
  1581. elif data[0] in ('add', 'hide', ):
  1582. result, text, reply_markup = await edit_bot_father_settings_via_message(
  1583. bot=bot,
  1584. user_record=user_record,
  1585. language=language,
  1586. mode=data[0]
  1587. )
  1588. elif data[0] == 'edit':
  1589. if len(data) > 2 and data[1] == 'select':
  1590. selected_record = bot.db['bot_father_commands'].find_one(id=data[2])
  1591. if selected_record is None:
  1592. return bot.get_message(
  1593. 'admin', 'error',
  1594. language=language
  1595. )
  1596. if len(data) == 3:
  1597. text = bot.get_message(
  1598. 'admin', 'father_command', 'settings',
  1599. 'modes', 'edit', 'panel', 'text',
  1600. language=language,
  1601. command=selected_record['command'],
  1602. description=selected_record['description'],
  1603. )
  1604. reply_markup = make_inline_keyboard(
  1605. [
  1606. make_button(
  1607. text=bot.get_message(
  1608. 'admin', 'father_command', 'settings',
  1609. 'modes', 'edit', 'panel',
  1610. 'edit_description', 'button',
  1611. language=language,
  1612. ),
  1613. prefix='father:///',
  1614. delimiter='|',
  1615. data=['settings', 'edit', 'select',
  1616. selected_record['id'], 'edit_descr']
  1617. ),
  1618. make_button(
  1619. text=bot.get_message(
  1620. 'admin', 'father_command', 'settings',
  1621. 'modes', 'edit', 'panel',
  1622. 'delete', 'button',
  1623. language=language,
  1624. ),
  1625. prefix='father:///',
  1626. delimiter='|',
  1627. data=['settings', 'edit', 'select',
  1628. selected_record['id'], 'del']
  1629. ),
  1630. make_button(
  1631. text=bot.get_message(
  1632. 'admin', 'father_command', 'back',
  1633. language=language,
  1634. ),
  1635. prefix='father:///',
  1636. delimiter='|',
  1637. data=['settings', 'edit']
  1638. )
  1639. ],
  1640. 2
  1641. )
  1642. elif len(data) > 3 and data[3] == 'edit_descr':
  1643. result, text, reply_markup = await edit_bot_father_settings_via_message(
  1644. bot=bot,
  1645. user_record=user_record,
  1646. language=language,
  1647. mode=data[0],
  1648. record=selected_record
  1649. )
  1650. elif len(data) > 3 and data[3] == 'del':
  1651. if not Confirmator.get('set_bot_father_commands',
  1652. confirm_timedelta=3
  1653. ).confirm(user_record['id']):
  1654. result = bot.get_message(
  1655. 'admin', 'confirm',
  1656. language=language
  1657. )
  1658. else:
  1659. bot.db['bot_father_commands'].update(
  1660. dict(
  1661. id=selected_record['id'],
  1662. cancelled=True
  1663. ),
  1664. ['id']
  1665. )
  1666. result = bot.get_message(
  1667. 'admin', 'father_command', 'settings',
  1668. 'modes', 'edit', 'panel', 'delete',
  1669. 'done', 'popup',
  1670. language=language
  1671. )
  1672. text = bot.get_message(
  1673. 'admin', 'father_command', 'settings',
  1674. 'modes', 'edit', 'panel', 'delete',
  1675. 'done', 'text',
  1676. language=language
  1677. )
  1678. reply_markup = make_inline_keyboard(
  1679. [
  1680. make_button(
  1681. text=bot.get_message(
  1682. 'admin', 'father_command',
  1683. 'back',
  1684. language=language
  1685. ),
  1686. prefix='father:///',
  1687. delimiter='|',
  1688. data=['settings']
  1689. )
  1690. ],
  1691. 1
  1692. )
  1693. elif len(data) == 1 or data[1] == 'go':
  1694. result, text, reply_markup = browse_bot_father_settings_records(
  1695. bot=bot,
  1696. language=language,
  1697. page=(data[2] if len(data) > 2 else 0)
  1698. )
  1699. if text:
  1700. return dict(
  1701. text=result,
  1702. edit=dict(
  1703. text=text,
  1704. reply_markup=reply_markup
  1705. )
  1706. )
  1707. return result
  1708. def init(telegram_bot: Bot,
  1709. talk_messages: dict = None,
  1710. admin_messages: dict = None,
  1711. packages: List[types.ModuleType] = None):
  1712. """Assign parsers, commands, buttons and queries to given `bot`."""
  1713. if packages is None:
  1714. packages = []
  1715. telegram_bot.packages.extend(
  1716. filter(lambda package: package not in telegram_bot.packages,
  1717. packages)
  1718. )
  1719. asyncio.ensure_future(get_package_updates(telegram_bot))
  1720. if talk_messages is None:
  1721. talk_messages = messages.default_talk_messages
  1722. telegram_bot.messages['talk'] = talk_messages
  1723. if admin_messages is None:
  1724. admin_messages = messages.default_admin_messages
  1725. telegram_bot.messages['admin'] = admin_messages
  1726. db = telegram_bot.db
  1727. if 'bot_father_commands' not in db.tables:
  1728. table = db.create_table(
  1729. table_name='bot_father_commands'
  1730. )
  1731. table.create_column(
  1732. 'command',
  1733. db.types.string
  1734. )
  1735. table.create_column(
  1736. 'description',
  1737. db.types.string
  1738. )
  1739. table.create_column(
  1740. 'hidden',
  1741. db.types.boolean
  1742. )
  1743. table.create_column(
  1744. 'cancelled',
  1745. db.types.boolean
  1746. )
  1747. if 'talking_sessions' not in db.tables:
  1748. table = db.create_table(
  1749. table_name='users'
  1750. )
  1751. table.create_column(
  1752. 'user',
  1753. db.types.integer
  1754. )
  1755. table.create_column(
  1756. 'admin',
  1757. db.types.integer
  1758. )
  1759. table.create_column(
  1760. 'cancelled',
  1761. db.types.integer
  1762. )
  1763. for exception in [
  1764. get_maintenance_exception_criterion(telegram_bot, command)
  1765. for command in ['stop', 'restart', 'maintenance']
  1766. ]:
  1767. telegram_bot.allow_during_maintenance(exception)
  1768. # Tasks to complete before starting bot
  1769. @telegram_bot.additional_task(when='BEFORE')
  1770. async def load_talking_sessions():
  1771. return await _load_talking_sessions(bot=telegram_bot)
  1772. @telegram_bot.additional_task(when='BEFORE', bot=telegram_bot)
  1773. async def notify_version(bot):
  1774. return await notify_new_version(bot=bot)
  1775. @telegram_bot.additional_task('BEFORE')
  1776. async def send_restart_messages():
  1777. return await _send_start_messages(bot=telegram_bot)
  1778. # Administration commands
  1779. @telegram_bot.command(command='/db',
  1780. aliases=[],
  1781. show_in_keyboard=False,
  1782. description=admin_messages[
  1783. 'db_command']['description'],
  1784. authorization_level='admin')
  1785. async def send_bot_database(bot, update, user_record):
  1786. return await _send_bot_database(bot, update, user_record)
  1787. @telegram_bot.command(command='/errors',
  1788. aliases=[],
  1789. show_in_keyboard=False,
  1790. description=admin_messages[
  1791. 'errors_command']['description'],
  1792. authorization_level='admin')
  1793. async def errors_command(bot, update, user_record):
  1794. return await _errors_command(bot, update, user_record)
  1795. @telegram_bot.command(command='/father',
  1796. aliases=[],
  1797. show_in_keyboard=False,
  1798. **{
  1799. key: value
  1800. for key, value in admin_messages['father_command'].items()
  1801. if key in ('description', )
  1802. },
  1803. authorization_level='admin')
  1804. async def father_command(bot, language):
  1805. return await _father_command(bot=bot, language=language)
  1806. @telegram_bot.button(prefix='father:///',
  1807. separator='|',
  1808. authorization_level='admin')
  1809. async def query_button(bot, user_record, language, data):
  1810. return await _father_button(bot=bot,
  1811. user_record=user_record,
  1812. language=language,
  1813. data=data)
  1814. @telegram_bot.command(command='/log',
  1815. aliases=[],
  1816. show_in_keyboard=False,
  1817. description=admin_messages[
  1818. 'log_command']['description'],
  1819. authorization_level='admin')
  1820. async def log_command(bot, update, user_record):
  1821. return await _log_command(bot, update, user_record)
  1822. @telegram_bot.command(command='/maintenance', aliases=[],
  1823. show_in_keyboard=False,
  1824. description=admin_messages[
  1825. 'maintenance_command']['description'],
  1826. authorization_level='admin')
  1827. async def maintenance_command(bot, update, user_record):
  1828. return await _maintenance_command(bot, update, user_record)
  1829. @telegram_bot.command(command='/query',
  1830. aliases=[],
  1831. show_in_keyboard=False,
  1832. description=admin_messages[
  1833. 'query_command']['description'],
  1834. authorization_level='admin')
  1835. async def query_command(bot, update, user_record):
  1836. return await _query_command(bot, update, user_record)
  1837. @telegram_bot.button(prefix='db_query:///',
  1838. separator='|',
  1839. description=admin_messages[
  1840. 'query_command']['description'],
  1841. authorization_level='admin')
  1842. async def query_button(bot, update, user_record, data):
  1843. return await _query_button(bot, update, user_record, data)
  1844. @telegram_bot.command(command='/restart',
  1845. aliases=[],
  1846. show_in_keyboard=False,
  1847. description=admin_messages[
  1848. 'restart_command']['description'],
  1849. authorization_level='admin')
  1850. async def restart_command(bot, update, user_record):
  1851. return await _restart_command(bot, update, user_record)
  1852. @telegram_bot.command(command='/select',
  1853. aliases=[],
  1854. show_in_keyboard=False,
  1855. description=admin_messages[
  1856. 'select_command']['description'],
  1857. authorization_level='admin')
  1858. async def select_command(bot, update, user_record):
  1859. return await _query_command(bot, update, user_record)
  1860. @telegram_bot.command(command='/stop',
  1861. aliases=[],
  1862. show_in_keyboard=False,
  1863. description=admin_messages[
  1864. 'stop_command']['description'],
  1865. authorization_level='admin')
  1866. async def stop_command(bot, update, user_record):
  1867. return await _stop_command(bot, update, user_record)
  1868. @telegram_bot.button(prefix='stop:///',
  1869. separator='|',
  1870. description=admin_messages[
  1871. 'stop_command']['description'],
  1872. authorization_level='admin')
  1873. async def stop_button(bot, update, user_record, data):
  1874. return await _stop_button(bot, update, user_record, data)
  1875. @telegram_bot.command(command='/talk',
  1876. aliases=[],
  1877. show_in_keyboard=False,
  1878. description=admin_messages[
  1879. 'talk_command']['description'],
  1880. authorization_level='admin')
  1881. async def talk_command(bot, update, user_record):
  1882. return await _talk_command(bot, update, user_record)
  1883. @telegram_bot.button(prefix='talk:///',
  1884. separator='|',
  1885. authorization_level='admin')
  1886. async def talk_button(bot, update, user_record, data):
  1887. return await _talk_button(bot, update, user_record, data)
  1888. @telegram_bot.command(command='/version',
  1889. aliases=[],
  1890. **{key: admin_messages['version_command'][key]
  1891. for key in ('reply_keyboard_button',
  1892. 'description',
  1893. 'help_section',)
  1894. },
  1895. show_in_keyboard=False,
  1896. authorization_level='admin')
  1897. async def version_command(bot, update, user_record):
  1898. return await _version_command(bot=bot,
  1899. update=update,
  1900. user_record=user_record)