Queer European MD passionate about IT

administration_tools.py 71 KB

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