Queer European MD passionate about IT

administration_tools.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181
  1. """Administration tools for telegram bots.
  2. Usage:
  3. ```
  4. import davtelepot
  5. my_bot = davtelepot.Bot.get('my_token', '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 types
  15. from typing import Union, List
  16. # Third party modules
  17. from sqlalchemy.exc import ResourceClosedError
  18. # Project modules
  19. from . import messages
  20. from .bot import Bot
  21. from .utilities import (
  22. async_wrapper, CachedPage, Confirmator, extract, get_cleaned_text,
  23. get_user, escape_html_chars, line_drawing_unordered_list, make_button,
  24. make_inline_keyboard, remove_html_tags, send_part_of_text_file,
  25. send_csv_file
  26. )
  27. async def _forward_to(update,
  28. bot: Bot,
  29. sender,
  30. addressee,
  31. is_admin=False):
  32. if update['text'].lower() in ['stop'] and is_admin:
  33. with bot.db as db:
  34. admin_record = db['users'].find_one(
  35. telegram_id=sender
  36. )
  37. session_record = db['talking_sessions'].find_one(
  38. admin=admin_record['id'],
  39. cancelled=0
  40. )
  41. other_user_record = db['users'].find_one(
  42. id=session_record['user']
  43. )
  44. await end_session(
  45. bot=bot,
  46. other_user_record=other_user_record,
  47. admin_record=admin_record
  48. )
  49. else:
  50. bot.set_individual_text_message_handler(
  51. await async_wrapper(
  52. _forward_to,
  53. sender=sender,
  54. addressee=addressee,
  55. is_admin=is_admin
  56. ),
  57. sender
  58. )
  59. await bot.forward_message(
  60. chat_id=addressee,
  61. update=update
  62. )
  63. return
  64. def get_talk_panel(bot: Bot,
  65. update,
  66. user_record=None,
  67. text: str = ''):
  68. """Return text and reply markup of talk panel.
  69. `text` may be:
  70. - `user_id` as string
  71. - `username` as string
  72. - `''` (empty string) for main menu (default)
  73. """
  74. users = []
  75. if len(text):
  76. with bot.db as db:
  77. if text.isnumeric():
  78. users = list(
  79. db['users'].find(id=int(text))
  80. )
  81. else:
  82. users = list(
  83. db.query(
  84. "SELECT * "
  85. "FROM users "
  86. "WHERE COALESCE( "
  87. " first_name || last_name || username, "
  88. " last_name || username, "
  89. " first_name || username, "
  90. " username, "
  91. " first_name || last_name, "
  92. " last_name, "
  93. " first_name "
  94. f") LIKE '%{text}%' "
  95. "ORDER BY LOWER( "
  96. " COALESCE( "
  97. " first_name || last_name || username, "
  98. " last_name || username, "
  99. " first_name || username, "
  100. " username, "
  101. " first_name || last_name, "
  102. " last_name, "
  103. " first_name "
  104. " ) "
  105. ") "
  106. "LIMIT 26"
  107. )
  108. )
  109. if len(text) == 0:
  110. text = (
  111. bot.get_message(
  112. 'talk',
  113. 'help_text',
  114. update=update,
  115. user_record=user_record,
  116. q=escape_html_chars(
  117. remove_html_tags(text)
  118. )
  119. )
  120. )
  121. reply_markup = make_inline_keyboard(
  122. [
  123. make_button(
  124. bot.get_message(
  125. 'talk', 'search_button',
  126. update=update, user_record=user_record
  127. ),
  128. prefix='talk:///',
  129. data=['search']
  130. )
  131. ],
  132. 1
  133. )
  134. elif len(users) == 0:
  135. text = (
  136. bot.get_message(
  137. 'talk',
  138. 'user_not_found',
  139. update=update,
  140. user_record=user_record,
  141. q=escape_html_chars(
  142. remove_html_tags(text)
  143. )
  144. )
  145. )
  146. reply_markup = make_inline_keyboard(
  147. [
  148. make_button(
  149. bot.get_message(
  150. 'talk', 'search_button',
  151. update=update, user_record=user_record
  152. ),
  153. prefix='talk:///',
  154. data=['search']
  155. )
  156. ],
  157. 1
  158. )
  159. else:
  160. text = "{header}\n\n{u}{etc}".format(
  161. header=bot.get_message(
  162. 'talk', 'select_user',
  163. update=update, user_record=user_record
  164. ),
  165. u=line_drawing_unordered_list(
  166. [
  167. get_user(user)
  168. for user in users[:25]
  169. ]
  170. ),
  171. etc=(
  172. '\n\n[...]'
  173. if len(users) > 25
  174. else ''
  175. )
  176. )
  177. reply_markup = make_inline_keyboard(
  178. [
  179. make_button(
  180. '👤 {u}'.format(
  181. u=get_user(
  182. {
  183. key: val
  184. for key, val in user.items()
  185. if key in ('first_name',
  186. 'last_name',
  187. 'username')
  188. }
  189. )
  190. ),
  191. prefix='talk:///',
  192. data=[
  193. 'select',
  194. user['id']
  195. ]
  196. )
  197. for user in users[:25]
  198. ],
  199. 2
  200. )
  201. return text, reply_markup
  202. async def _talk_command(bot: Bot,
  203. update,
  204. user_record):
  205. text = get_cleaned_text(
  206. update,
  207. bot,
  208. ['talk']
  209. )
  210. text, reply_markup = get_talk_panel(bot=bot, update=update,
  211. user_record=user_record, text=text)
  212. return dict(
  213. text=text,
  214. parse_mode='HTML',
  215. reply_markup=reply_markup,
  216. )
  217. async def start_session(bot: Bot,
  218. other_user_record,
  219. admin_record):
  220. """Start talking session between user and admin.
  221. Register session in database, so it gets loaded before message_loop starts.
  222. Send a notification both to admin and user, set custom parsers and return.
  223. """
  224. with bot.db as db:
  225. db['talking_sessions'].insert(
  226. dict(
  227. user=other_user_record['id'],
  228. admin=admin_record['id'],
  229. cancelled=0
  230. )
  231. )
  232. await bot.send_message(
  233. chat_id=other_user_record['telegram_id'],
  234. text=bot.get_message(
  235. 'talk', 'user_warning',
  236. user_record=other_user_record,
  237. u=get_user(admin_record)
  238. )
  239. )
  240. await bot.send_message(
  241. chat_id=admin_record['telegram_id'],
  242. text=bot.get_message(
  243. 'talk', 'admin_warning',
  244. user_record=admin_record,
  245. u=get_user(other_user_record)
  246. ),
  247. reply_markup=make_inline_keyboard(
  248. [
  249. make_button(
  250. bot.get_message(
  251. 'talk', 'stop',
  252. user_record=admin_record
  253. ),
  254. prefix='talk:///',
  255. data=['stop', other_user_record['id']]
  256. )
  257. ]
  258. )
  259. )
  260. bot.set_individual_text_message_handler(
  261. await async_wrapper(
  262. _forward_to,
  263. sender=other_user_record['telegram_id'],
  264. addressee=admin_record['telegram_id'],
  265. is_admin=False
  266. ),
  267. other_user_record['telegram_id']
  268. )
  269. bot.set_individual_text_message_handler(
  270. await async_wrapper(
  271. _forward_to,
  272. sender=admin_record['telegram_id'],
  273. addressee=other_user_record['telegram_id'],
  274. is_admin=True
  275. ),
  276. admin_record['telegram_id']
  277. )
  278. return
  279. async def end_session(bot: Bot,
  280. other_user_record,
  281. admin_record):
  282. """End talking session between user and admin.
  283. Cancel session in database, so it will not be loaded anymore.
  284. Send a notification both to admin and user, clear custom parsers
  285. and return.
  286. """
  287. with bot.db as db:
  288. db['talking_sessions'].update(
  289. dict(
  290. admin=admin_record['id'],
  291. cancelled=1
  292. ),
  293. ['admin']
  294. )
  295. await bot.send_message(
  296. chat_id=other_user_record['telegram_id'],
  297. text=bot.get_message(
  298. 'talk', 'user_session_ended',
  299. user_record=other_user_record,
  300. u=get_user(admin_record)
  301. )
  302. )
  303. await bot.send_message(
  304. chat_id=admin_record['telegram_id'],
  305. text=bot.get_message(
  306. 'talk', 'admin_session_ended',
  307. user_record=admin_record,
  308. u=get_user(other_user_record)
  309. ),
  310. )
  311. for record in (admin_record, other_user_record,):
  312. bot.remove_individual_text_message_handler(record['telegram_id'])
  313. return
  314. async def _talk_button(bot: Bot,
  315. update,
  316. user_record,
  317. data):
  318. telegram_id = user_record['telegram_id']
  319. command, *arguments = data
  320. result, text, reply_markup = '', '', None
  321. if command == 'search':
  322. bot.set_individual_text_message_handler(
  323. await async_wrapper(
  324. _talk_command,
  325. ),
  326. update
  327. )
  328. text = bot.get_message(
  329. 'talk', 'instructions',
  330. update=update, user_record=user_record
  331. )
  332. reply_markup = None
  333. elif command == 'select':
  334. if (
  335. len(arguments) < 1
  336. or type(arguments[0]) is not int
  337. ):
  338. result = "Errore!"
  339. else:
  340. with bot.db as db:
  341. other_user_record = db['users'].find_one(
  342. id=arguments[0]
  343. )
  344. admin_record = db['users'].find_one(
  345. telegram_id=telegram_id
  346. )
  347. await start_session(
  348. bot,
  349. other_user_record=other_user_record,
  350. admin_record=admin_record
  351. )
  352. elif command == 'stop':
  353. if (
  354. len(arguments) < 1
  355. or type(arguments[0]) is not int
  356. ):
  357. result = "Errore!"
  358. elif not Confirmator.get('stop_bots').confirm(telegram_id):
  359. result = bot.get_message(
  360. 'talk', 'end_session',
  361. update=update, user_record=user_record
  362. )
  363. else:
  364. with bot.db as db:
  365. other_user_record = db['users'].find_one(
  366. id=arguments[0]
  367. )
  368. admin_record = db['users'].find_one(
  369. telegram_id=telegram_id
  370. )
  371. await end_session(
  372. bot,
  373. other_user_record=other_user_record,
  374. admin_record=admin_record
  375. )
  376. text = "Session ended."
  377. reply_markup = None
  378. if text:
  379. return dict(
  380. text=result,
  381. edit=dict(
  382. text=text,
  383. parse_mode='HTML',
  384. reply_markup=reply_markup,
  385. disable_web_page_preview=True
  386. )
  387. )
  388. return result
  389. async def _restart_command(bot: Bot,
  390. update,
  391. user_record):
  392. with bot.db as db:
  393. db['restart_messages'].insert(
  394. dict(
  395. text=bot.get_message(
  396. 'admin', 'restart_command', 'restart_completed_message',
  397. update=update, user_record=user_record
  398. ),
  399. chat_id=update['chat']['id'],
  400. parse_mode='HTML',
  401. reply_to_message_id=update['message_id'],
  402. sent=None
  403. )
  404. )
  405. await bot.reply(
  406. update=update,
  407. text=bot.get_message(
  408. 'admin', 'restart_command', 'restart_scheduled_message',
  409. update=update, user_record=user_record
  410. )
  411. )
  412. bot.__class__.stop(message='=== RESTART ===', final_state=65)
  413. return
  414. async def _stop_command(bot: Bot,
  415. update,
  416. user_record):
  417. text = bot.get_message(
  418. 'admin', 'stop_command', 'text',
  419. update=update, user_record=user_record
  420. )
  421. reply_markup = make_inline_keyboard(
  422. [
  423. make_button(
  424. text=bot.get_message(
  425. 'admin', 'stop_button', 'stop_text',
  426. update=update, user_record=user_record
  427. ),
  428. prefix='stop:///',
  429. data=['stop']
  430. ),
  431. make_button(
  432. text=bot.get_message(
  433. 'admin', 'stop_button', 'cancel',
  434. update=update, user_record=user_record
  435. ),
  436. prefix='stop:///',
  437. data=['cancel']
  438. )
  439. ],
  440. 1
  441. )
  442. return dict(
  443. text=text,
  444. parse_mode='HTML',
  445. reply_markup=reply_markup
  446. )
  447. async def stop_bots(bot: Bot):
  448. """Stop bots in `bot` class."""
  449. await asyncio.sleep(2)
  450. bot.__class__.stop(message='=== STOP ===', final_state=0)
  451. return
  452. async def _stop_button(bot: Bot,
  453. update,
  454. user_record,
  455. data: List[Union[int, str]]):
  456. result, text, reply_markup = '', '', None
  457. telegram_id = user_record['telegram_id']
  458. command = data[0] if len(data) > 0 else 'None'
  459. if command == 'stop':
  460. if not Confirmator.get('stop_bots').confirm(telegram_id):
  461. return bot.get_message(
  462. 'admin', 'stop_button', 'confirm',
  463. update=update, user_record=user_record
  464. )
  465. text = bot.get_message(
  466. 'admin', 'stop_button', 'stopping',
  467. update=update, user_record=user_record
  468. )
  469. result = text
  470. # Do not stop bots immediately, otherwise callback query
  471. # will never be answered
  472. asyncio.ensure_future(stop_bots(bot))
  473. elif command == 'cancel':
  474. text = bot.get_message(
  475. 'admin', 'stop_button', 'cancelled',
  476. update=update, user_record=user_record
  477. )
  478. result = text
  479. if text:
  480. return dict(
  481. text=result,
  482. edit=dict(
  483. text=text,
  484. parse_mode='HTML',
  485. reply_markup=reply_markup,
  486. disable_web_page_preview=True
  487. )
  488. )
  489. return result
  490. async def _send_bot_database(bot, update, user_record):
  491. if not all(
  492. [
  493. bot.db_url.endswith('.db'),
  494. bot.db_url.startswith('sqlite:///')
  495. ]
  496. ):
  497. return bot.get_message(
  498. 'admin', 'db_command', 'not_sqlite',
  499. update=update, user_record=user_record,
  500. db_type=bot.db_url.partition(':///')[0]
  501. )
  502. await bot.send_document(
  503. chat_id=user_record['telegram_id'],
  504. document_path=extract(bot.db.url, starter='sqlite:///'),
  505. caption=bot.get_message(
  506. 'admin', 'db_command', 'file_caption',
  507. update=update, user_record=user_record
  508. )
  509. )
  510. return bot.get_message(
  511. 'admin', 'db_command', 'db_sent',
  512. update=update, user_record=user_record
  513. )
  514. async def _query_command(bot, update, user_record):
  515. query = get_cleaned_text(
  516. update,
  517. bot,
  518. ['query', ]
  519. )
  520. query_id = None
  521. if len(query) == 0:
  522. return bot.get_message(
  523. 'admin', 'query_command', 'help',
  524. update=update, user_record=user_record
  525. )
  526. try:
  527. with bot.db as db:
  528. record = db.query(query)
  529. try:
  530. record = list(record)
  531. except ResourceClosedError:
  532. record = bot.get_message(
  533. 'admin', 'query_command', 'no_iterable',
  534. update=update, user_record=user_record
  535. )
  536. query_id = db['queries'].upsert(
  537. dict(
  538. query=query
  539. ),
  540. ['query']
  541. )
  542. if query_id is True:
  543. query_id = db['queries'].find_one(
  544. query=query
  545. )['id']
  546. result = json.dumps(record, indent=2)
  547. if len(result) > 500:
  548. result = (
  549. f"{result[:200]}\n" # First 200 characters
  550. f"[...]\n" # Interruption symbol
  551. f"{result[-200:]}" # Last 200 characters
  552. )
  553. except Exception as e:
  554. result = "{first_line}\n{e}".format(
  555. first_line=bot.get_message(
  556. 'admin', 'query_command', 'exception',
  557. update=update, user_record=user_record
  558. ),
  559. e=e
  560. )
  561. result = (
  562. "<b>{first_line}</b>\n".format(
  563. first_line=bot.get_message(
  564. 'admin', 'query_command', 'result',
  565. update=update, user_record=user_record
  566. )
  567. )
  568. + f"<code>{query}</code>\n\n"
  569. f"{result}"
  570. )
  571. if query_id:
  572. reply_markup = make_inline_keyboard(
  573. [
  574. make_button(
  575. text='CSV',
  576. prefix='db_query:///',
  577. data=['csv', query_id]
  578. )
  579. ],
  580. 1
  581. )
  582. else:
  583. reply_markup = None
  584. return dict(
  585. chat_id=update['chat']['id'],
  586. text=result,
  587. parse_mode='HTML',
  588. reply_markup=reply_markup
  589. )
  590. async def _query_button(bot, update, user_record, data):
  591. result, text, reply_markup = '', '', None
  592. command = data[0] if len(data) else 'default'
  593. error_message = bot.get_message(
  594. 'admin', 'query_button', 'error',
  595. user_record=user_record, update=update
  596. )
  597. if command == 'csv':
  598. if not len(data) > 1:
  599. return error_message
  600. if len(data) > 1:
  601. with bot.db as db:
  602. query_record = db['queries'].find_one(id=data[1])
  603. if query_record is None or 'query' not in query_record:
  604. return error_message
  605. await send_csv_file(
  606. bot=bot,
  607. chat_id=update['from']['id'],
  608. query=query_record['query'],
  609. file_name=bot.get_message(
  610. 'admin', 'query_button', 'file_name',
  611. user_record=user_record, update=update
  612. ),
  613. update=update,
  614. user_record=user_record
  615. )
  616. if text:
  617. return dict(
  618. text=result,
  619. edit=dict(
  620. text=text,
  621. reply_markup=reply_markup
  622. )
  623. )
  624. return result
  625. async def _log_command(bot, update, user_record):
  626. if bot.log_file_path is None:
  627. return bot.get_message(
  628. 'admin', 'log_command', 'no_log',
  629. update=update, user_record=user_record
  630. )
  631. # Always send log file in private chat
  632. chat_id = update['from']['id']
  633. text = get_cleaned_text(update, bot, ['log'])
  634. reversed_ = 'r' not in text
  635. text = text.strip('r')
  636. if text.isnumeric():
  637. limit = int(text)
  638. else:
  639. limit = 100
  640. if limit is None:
  641. sent = await bot.send_document(
  642. chat_id=chat_id,
  643. document_path=bot.log_file_path,
  644. caption=bot.get_message(
  645. 'admin', 'log_command', 'here_is_log_file',
  646. update=update, user_record=user_record
  647. )
  648. )
  649. else:
  650. sent = await send_part_of_text_file(
  651. bot=bot,
  652. update=update,
  653. user_record=user_record,
  654. chat_id=chat_id,
  655. file_path=bot.log_file_path,
  656. file_name=bot.log_file_name,
  657. caption=bot.get_message(
  658. 'admin', 'log_command', (
  659. 'log_file_last_lines'
  660. if reversed_
  661. else 'log_file_first_lines'
  662. ),
  663. update=update, user_record=user_record,
  664. lines=limit
  665. ),
  666. reversed_=reversed_,
  667. limit=limit
  668. )
  669. if isinstance(sent, Exception):
  670. return bot.get_message(
  671. 'admin', 'log_command', 'sending_failure',
  672. update=update, user_record=user_record,
  673. e=sent
  674. )
  675. return
  676. async def _errors_command(bot, update, user_record):
  677. # Always send errors log file in private chat
  678. chat_id = update['from']['id']
  679. if bot.errors_file_path is None:
  680. return bot.get_message(
  681. 'admin', 'errors_command', 'no_log',
  682. update=update, user_record=user_record
  683. )
  684. await bot.sendChatAction(chat_id=chat_id, action='upload_document')
  685. try:
  686. # Check that error log is not empty
  687. with open(bot.errors_file_path, 'r') as errors_file:
  688. for _ in errors_file:
  689. break
  690. else:
  691. return bot.get_message(
  692. 'admin', 'errors_command', 'empty_log',
  693. update=update, user_record=user_record
  694. )
  695. # Send error log
  696. sent = await bot.send_document(
  697. # Always send log file in private chat
  698. chat_id=chat_id,
  699. document_path=bot.errors_file_path,
  700. caption=bot.get_message(
  701. 'admin', 'errors_command', 'here_is_log_file',
  702. update=update, user_record=user_record
  703. )
  704. )
  705. # Reset error log
  706. with open(bot.errors_file_path, 'w') as errors_file:
  707. errors_file.write('')
  708. except Exception as e:
  709. sent = e
  710. # Notify failure
  711. if isinstance(sent, Exception):
  712. return bot.get_message(
  713. 'admin', 'errors_command', 'sending_failure',
  714. update=update, user_record=user_record,
  715. e=sent
  716. )
  717. return
  718. async def _maintenance_command(bot, update, user_record):
  719. maintenance_message = get_cleaned_text(update, bot, ['maintenance'])
  720. if maintenance_message.startswith('{'):
  721. maintenance_message = json.loads(maintenance_message)
  722. maintenance_status = bot.change_maintenance_status(
  723. maintenance_message=maintenance_message
  724. )
  725. if maintenance_status:
  726. return bot.get_message(
  727. 'admin', 'maintenance_command', 'maintenance_started',
  728. update=update, user_record=user_record,
  729. message=bot.maintenance_message
  730. )
  731. return bot.get_message(
  732. 'admin', 'maintenance_command', 'maintenance_ended',
  733. update=update, user_record=user_record
  734. )
  735. def get_maintenance_exception_criterion(bot, allowed_command):
  736. """Get a criterion to allow a type of updates during maintenance.
  737. `bot` : davtelepot.bot.Bot() instance
  738. `allowed_command` : str (command to be allowed during maintenance)
  739. """
  740. def criterion(update):
  741. if 'message' not in update:
  742. return False
  743. update = update['message']
  744. text = get_cleaned_text(update, bot, [])
  745. if (
  746. 'from' not in update
  747. or 'id' not in update['from']
  748. ):
  749. return False
  750. with bot.db as db:
  751. user_record = db['users'].find_one(
  752. telegram_id=update['from']['id']
  753. )
  754. if not bot.authorization_function(
  755. update=update,
  756. user_record=user_record,
  757. authorization_level=2
  758. ):
  759. return False
  760. return text == allowed_command.strip('/')
  761. return criterion
  762. async def get_last_commit():
  763. """Get last commit hash and davtelepot version."""
  764. try:
  765. _subprocess = await asyncio.create_subprocess_exec(
  766. 'git', 'rev-parse', 'HEAD',
  767. stdout=asyncio.subprocess.PIPE,
  768. stderr=asyncio.subprocess.STDOUT
  769. )
  770. stdout, _ = await _subprocess.communicate()
  771. last_commit = stdout.decode().strip()
  772. except Exception as e:
  773. last_commit = f"{e}"
  774. if last_commit.startswith("fatal: not a git repository"):
  775. last_commit = "-"
  776. return last_commit
  777. async def _version_command(bot: Bot, update, user_record):
  778. last_commit = await get_last_commit()
  779. text = bot.get_message(
  780. 'admin', 'version_command', 'header',
  781. last_commit=last_commit,
  782. update=update, user_record=user_record
  783. ) + '\n\n'
  784. text += '\n'.join(
  785. f"<b>{package.__name__}</b>: "
  786. f"<code>{package.__version__}</code>"
  787. for package in bot.packages
  788. )
  789. return text
  790. async def notify_new_version(bot: Bot):
  791. """Notify `bot` administrators about new versions.
  792. Notify admins when last commit and/or davtelepot version change.
  793. """
  794. last_commit = await get_last_commit()
  795. old_record = bot.db['version_history'].find_one(
  796. order_by=['-id']
  797. )
  798. current_versions = {
  799. f"{package.__name__}_version": package.__version__
  800. for package in bot.packages
  801. }
  802. current_versions['last_commit'] = last_commit
  803. if old_record is None:
  804. old_record = dict(
  805. updated_at=datetime.datetime.min,
  806. )
  807. for name in current_versions.keys():
  808. if name not in old_record:
  809. old_record[name] = None
  810. if any(
  811. old_record[name] != current_version
  812. for name, current_version in current_versions.items()
  813. ):
  814. bot.db['version_history'].insert(
  815. dict(
  816. updated_at=datetime.datetime.now(),
  817. **current_versions
  818. )
  819. )
  820. for admin in bot.administrators:
  821. text = bot.get_message(
  822. 'admin', 'new_version', 'title',
  823. user_record=admin
  824. ) + '\n\n'
  825. if last_commit != old_record['last_commit']:
  826. text += bot.get_message(
  827. 'admin', 'new_version', 'last_commit',
  828. old_record=old_record,
  829. new_record=current_versions,
  830. user_record=admin
  831. ) + '\n\n'
  832. text += '\n'.join(
  833. f"<b>{name[:-len('_version')]}</b>: "
  834. f"<code>{old_record[name]}</code> —> "
  835. f"<code>{current_version}</code>"
  836. for name, current_version in current_versions.items()
  837. if name not in ('last_commit', )
  838. and current_version != old_record[name]
  839. )
  840. await bot.send_message(
  841. chat_id=admin['telegram_id'],
  842. disable_notification=True,
  843. text=text
  844. )
  845. return
  846. async def get_package_updates(bot: Bot,
  847. monitoring_interval: Union[
  848. int, datetime.timedelta
  849. ] = 60 * 60,
  850. notification_interval: Union[
  851. int, datetime.timedelta
  852. ] = 60 * 60 * 24):
  853. if isinstance(monitoring_interval, datetime.timedelta):
  854. monitoring_interval = monitoring_interval.total_seconds()
  855. if type(notification_interval) is int:
  856. notification_interval = datetime.timedelta(
  857. seconds=notification_interval
  858. )
  859. while 1:
  860. news = dict()
  861. for package in bot.packages:
  862. package_web_page = CachedPage.get(
  863. f'https://pypi.python.org/pypi/{package.__name__}/json',
  864. cache_time=2,
  865. mode='json'
  866. )
  867. web_page = await package_web_page.get_page()
  868. if web_page is None or isinstance(web_page, Exception):
  869. logging.error(f"Cannot get updates for {package.__name__}, "
  870. "skipping...")
  871. continue
  872. new_version = web_page['info']['version']
  873. current_version = package.__version__
  874. notification_record = bot.db['updates_notifications'].find_one(
  875. package=package.__name__,
  876. order_by=['-id'],
  877. _limit=1
  878. )
  879. if (
  880. new_version != current_version
  881. and (notification_record is None
  882. or notification_record['notified_at']
  883. < datetime.datetime.now() - notification_interval)
  884. ):
  885. news[package.__name__] = {
  886. 'current': current_version,
  887. 'new': new_version
  888. }
  889. if news:
  890. for admin in bot.administrators:
  891. text = bot.get_message(
  892. 'admin', 'updates_available', 'header',
  893. user_record=admin
  894. ) + '\n\n'
  895. text += '\n'.join(
  896. f"<b>{package}</b>: "
  897. f"<code>{versions['current']}</code> —> "
  898. f"<code>{versions['new']}</code>"
  899. for package, versions in news.items()
  900. )
  901. await bot.send_message(
  902. chat_id=admin['telegram_id'],
  903. disable_notification=True,
  904. text=text
  905. )
  906. bot.db['updates_notifications'].insert_many(
  907. [
  908. {
  909. "package": package,
  910. "version": information['new'],
  911. 'notified_at': datetime.datetime.now()
  912. }
  913. for package, information in news.items()
  914. ]
  915. )
  916. await asyncio.sleep(monitoring_interval)
  917. def init(telegram_bot: Bot,
  918. talk_messages: dict = None,
  919. admin_messages: dict = None,
  920. packages: List[types.ModuleType] = None):
  921. """Assign parsers, commands, buttons and queries to given `bot`."""
  922. if packages is None:
  923. packages = []
  924. telegram_bot.packages.extend(
  925. filter(lambda package: package not in telegram_bot.packages,
  926. packages)
  927. )
  928. asyncio.ensure_future(get_package_updates(telegram_bot))
  929. if talk_messages is None:
  930. talk_messages = messages.default_talk_messages
  931. telegram_bot.messages['talk'] = talk_messages
  932. if admin_messages is None:
  933. admin_messages = messages.default_admin_messages
  934. telegram_bot.messages['admin'] = admin_messages
  935. db = telegram_bot.db
  936. if 'talking_sessions' not in db.tables:
  937. db['talking_sessions'].insert(
  938. dict(
  939. user=0,
  940. admin=0,
  941. cancelled=1
  942. )
  943. )
  944. allowed_during_maintenance = [
  945. get_maintenance_exception_criterion(telegram_bot, command)
  946. for command in ['stop', 'restart', 'maintenance']
  947. ]
  948. @telegram_bot.additional_task(when='BEFORE')
  949. async def load_talking_sessions():
  950. sessions = []
  951. for session in db.query(
  952. """SELECT *
  953. FROM talking_sessions
  954. WHERE NOT cancelled
  955. """
  956. ):
  957. sessions.append(
  958. dict(
  959. other_user_record=db['users'].find_one(
  960. id=session['user']
  961. ),
  962. admin_record=db['users'].find_one(
  963. id=session['admin']
  964. ),
  965. )
  966. )
  967. for session in sessions:
  968. await start_session(
  969. bot=telegram_bot,
  970. other_user_record=session['other_user_record'],
  971. admin_record=session['admin_record']
  972. )
  973. @telegram_bot.command(command='/talk',
  974. aliases=[],
  975. show_in_keyboard=False,
  976. description=admin_messages[
  977. 'talk_command']['description'],
  978. authorization_level='admin')
  979. async def talk_command(bot, update, user_record):
  980. return await _talk_command(bot, update, user_record)
  981. @telegram_bot.button(prefix='talk:///',
  982. separator='|',
  983. authorization_level='admin')
  984. async def talk_button(bot, update, user_record, data):
  985. return await _talk_button(bot, update, user_record, data)
  986. @telegram_bot.command(command='/restart',
  987. aliases=[],
  988. show_in_keyboard=False,
  989. description=admin_messages[
  990. 'restart_command']['description'],
  991. authorization_level='admin')
  992. async def restart_command(bot, update, user_record):
  993. return await _restart_command(bot, update, user_record)
  994. @telegram_bot.additional_task('BEFORE')
  995. async def send_restart_messages():
  996. """Send restart messages at restart."""
  997. for restart_message in db['restart_messages'].find(sent=None):
  998. asyncio.ensure_future(
  999. telegram_bot.send_message(
  1000. **{
  1001. key: val
  1002. for key, val in restart_message.items()
  1003. if key in (
  1004. 'chat_id',
  1005. 'text',
  1006. 'parse_mode',
  1007. 'reply_to_message_id'
  1008. )
  1009. }
  1010. )
  1011. )
  1012. db['restart_messages'].update(
  1013. dict(
  1014. sent=datetime.datetime.now(),
  1015. id=restart_message['id']
  1016. ),
  1017. ['id'],
  1018. ensure=True
  1019. )
  1020. return
  1021. @telegram_bot.command(command='/stop',
  1022. aliases=[],
  1023. show_in_keyboard=False,
  1024. description=admin_messages[
  1025. 'stop_command']['description'],
  1026. authorization_level='admin')
  1027. async def stop_command(bot, update, user_record):
  1028. return await _stop_command(bot, update, user_record)
  1029. @telegram_bot.button(prefix='stop:///',
  1030. separator='|',
  1031. description=admin_messages[
  1032. 'stop_command']['description'],
  1033. authorization_level='admin')
  1034. async def stop_button(bot, update, user_record, data):
  1035. return await _stop_button(bot, update, user_record, data)
  1036. @telegram_bot.command(command='/db',
  1037. aliases=[],
  1038. show_in_keyboard=False,
  1039. description=admin_messages[
  1040. 'db_command']['description'],
  1041. authorization_level='admin')
  1042. async def send_bot_database(bot, update, user_record):
  1043. return await _send_bot_database(bot, update, user_record)
  1044. @telegram_bot.command(command='/query',
  1045. aliases=[],
  1046. show_in_keyboard=False,
  1047. description=admin_messages[
  1048. 'query_command']['description'],
  1049. authorization_level='admin')
  1050. async def query_command(bot, update, user_record):
  1051. return await _query_command(bot, update, user_record)
  1052. @telegram_bot.command(command='/select',
  1053. aliases=[],
  1054. show_in_keyboard=False,
  1055. description=admin_messages[
  1056. 'select_command']['description'],
  1057. authorization_level='admin')
  1058. async def select_command(bot, update, user_record):
  1059. return await _query_command(bot, update, user_record)
  1060. @telegram_bot.button(prefix='db_query:///',
  1061. separator='|',
  1062. description=admin_messages[
  1063. 'query_command']['description'],
  1064. authorization_level='admin')
  1065. async def query_button(bot, update, user_record, data):
  1066. return await _query_button(bot, update, user_record, data)
  1067. @telegram_bot.command(command='/log',
  1068. aliases=[],
  1069. show_in_keyboard=False,
  1070. description=admin_messages[
  1071. 'log_command']['description'],
  1072. authorization_level='admin')
  1073. async def log_command(bot, update, user_record):
  1074. return await _log_command(bot, update, user_record)
  1075. @telegram_bot.command(command='/errors',
  1076. aliases=[],
  1077. show_in_keyboard=False,
  1078. description=admin_messages[
  1079. 'errors_command']['description'],
  1080. authorization_level='admin')
  1081. async def errors_command(bot, update, user_record):
  1082. return await _errors_command(bot, update, user_record)
  1083. for exception in allowed_during_maintenance:
  1084. telegram_bot.allow_during_maintenance(exception)
  1085. @telegram_bot.command(command='/maintenance', aliases=[],
  1086. show_in_keyboard=False,
  1087. description=admin_messages[
  1088. 'maintenance_command']['description'],
  1089. authorization_level='admin')
  1090. async def maintenance_command(bot, update, user_record):
  1091. return await _maintenance_command(bot, update, user_record)
  1092. @telegram_bot.command(command='/version',
  1093. aliases=[],
  1094. **{key: admin_messages['version_command'][key]
  1095. for key in ('reply_keyboard_button',
  1096. 'description',
  1097. 'help_section',)
  1098. },
  1099. show_in_keyboard=False,
  1100. authorization_level='admin')
  1101. async def version_command(bot, update, user_record):
  1102. return await _version_command(bot=bot,
  1103. update=update,
  1104. user_record=user_record)
  1105. @telegram_bot.additional_task(when='BEFORE', bot=telegram_bot)
  1106. async def notify_version(bot):
  1107. return await notify_new_version(bot=bot)