Queer European MD passionate about IT

administration_tools.py 74 KB

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