Queer European MD passionate about IT

administration_tools.py 69 KB

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