Queer European MD passionate about IT

administration_tools.py 69 KB

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