Queer European MD passionate about IT

administration_tools.py 36 KB

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