Queer European MD passionate about IT

administration_tools.py 71 KB

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