Queer European MD passionate about IT

custombot.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  1. """WARNING: this is only a legacy module, written for backward compatibility.
  2. For newer versions use `bot.py`.
  3. This module used to rely on third party `telepot` library by Nick Lee
  4. (@Nickoala).
  5. The `telepot` repository was archived in may 2019 and will no longer be listed
  6. in requirements. To run legacy code, install telepot manually.
  7. `pip install telepot`
  8. Subclass of third party telepot.aio.Bot, providing the following features.
  9. - It prevents hitting Telegram flood limits by waiting
  10. between text and photo messages.
  11. - It provides command, parser, button and other decorators to associate
  12. common Telegram actions with custom handlers.
  13. - It supports multiple bots running in the same script
  14. and allows communications between them
  15. as well as complete independency from each other.
  16. - Each bot is associated with a sqlite database
  17. using dataset, a third party library.
  18. Please note that you need Python3.5+ to run async code
  19. Check requirements.txt for third party dependencies.
  20. """
  21. # Standard library modules
  22. import asyncio
  23. from collections import OrderedDict
  24. import datetime
  25. import inspect
  26. import logging
  27. import os
  28. # Third party modules
  29. import davtelepot.bot
  30. # Project modules
  31. from .utilities import (
  32. get_secure_key, extract, sleep_until
  33. )
  34. class Bot(davtelepot.bot.Bot):
  35. """Legacy adapter for backward compatibility.
  36. Old description:
  37. telepot.aio.Bot (async Telegram bot framework) convenient subclass.
  38. === General functioning ===
  39. - While Bot.run() coroutine is executed, HTTP get requests are made
  40. to Telegram servers asking for new messages for each Bot instance.
  41. - Each message causes the proper Bot instance method coroutine
  42. to be awaited, according to its flavour (see routing_table)
  43. -- For example, chat messages cause `Bot().on_chat_message(message)`
  44. to be awaited.
  45. - This even-processing coroutine ensures the proper handling function
  46. a future and returns.
  47. -- That means that simpler tasks are completed before slower ones,
  48. since handling functions are not awaited but scheduled
  49. by `asyncio.ensure_future(handling_function(...))`
  50. -- For example, chat text messages are handled by
  51. `handle_text_message`, which looks for the proper function
  52. to elaborate the request (in bot's commands and parsers)
  53. - The handling function evaluates an answer, depending on the message
  54. content, and eventually provides a reply
  55. -- For example, `handle_text_message` sends its
  56. answer via `send_message`
  57. - All Bot.instances run simultaneously and faster requests
  58. are completed earlier.
  59. - All uncaught events are ignored.
  60. """
  61. def __init__(self, token, db_name=None):
  62. """Instantiate Bot instance, given a token and a db name."""
  63. davtelepot.bot.Bot.__init__(self, token=token, database_url=db_name)
  64. self.message_handlers['pinned_message'] = self.handle_pinned_message
  65. self.message_handlers['photo'] = self.handle_photo_message
  66. self.message_handlers['location'] = self.handle_location
  67. self.custom_photo_parsers = dict()
  68. self.custom_location_parsers = dict()
  69. self.to_be_obscured = []
  70. self.to_be_destroyed = []
  71. self.chat_actions = dict(
  72. pinned=OrderedDict()
  73. )
  74. self.messages = dict()
  75. @property
  76. def unauthorized_message(self):
  77. """Return this if user is unauthorized to make a request.
  78. This property is deprecated: use `authorization_denied_message`
  79. instead.
  80. """
  81. return self.authorization_denied_message
  82. @property
  83. def maintenance(self):
  84. """Check whether bot is under maintenance.
  85. This property is deprecated: use `under_maintenance` instead.
  86. """
  87. return self.under_maintenance
  88. @classmethod
  89. def set_class_unauthorized_message(csl, unauthorized_message):
  90. """Set class unauthorized message.
  91. This method is deprecated: use `set_class_authorization_denied_message`
  92. instead.
  93. """
  94. return csl.set_class_authorization_denied_message(unauthorized_message)
  95. def set_unauthorized_message(self, unauthorized_message):
  96. """Set instance unauthorized message.
  97. This method is deprecated: use `set_authorization_denied_message`
  98. instead.
  99. """
  100. return self.set_authorization_denied_message(unauthorized_message)
  101. def set_authorization_function(self, authorization_function):
  102. """Set a custom authorization_function.
  103. It should evaluate True if user is authorized to perform a specific
  104. action and False otherwise.
  105. It should take update and role and return a Boolean.
  106. Default authorization_function always evaluates True.
  107. """
  108. def _authorization_function(update, authorization_level,
  109. user_record=None):
  110. privileges = authorization_level # noqa: W0612, this variable
  111. # is used by locals()
  112. return authorization_function(
  113. **{
  114. name: argument
  115. for name, argument in locals().items()
  116. if name in inspect.signature(
  117. authorization_function
  118. ).parameters
  119. }
  120. )
  121. self.authorization_function = _authorization_function
  122. def set_maintenance(self, maintenance_message):
  123. """Put the bot under maintenance or end it.
  124. This method is deprecated: use `change_maintenance_status` instead.
  125. """
  126. bot_in_maintenance = self.change_maintenance_status(
  127. maintenance_message=maintenance_message
  128. )
  129. if bot_in_maintenance:
  130. return (
  131. "<i>Bot has just been put under maintenance!</i>\n\n"
  132. "Until further notice, it will reply to users "
  133. "with the following message:\n\n{}"
  134. ).format(
  135. self.maintenance_message
  136. )
  137. return "<i>Maintenance ended!</i>"
  138. def set_get_chat_id_function(self, get_chat_id_function):
  139. """Set a custom get_chat_id function.
  140. This method is deprecated: use `set_chat_id_getter` instead.
  141. """
  142. return self.set_chat_id_getter(get_chat_id_function)
  143. def get_message(self, *fields, update=None, user_record=None,
  144. language=None, **format_kwargs):
  145. """Given a list of strings (`fields`), return proper message.
  146. If `language` is not passed, it is extracted from `update`.
  147. If `update` is not passed either, `language` is extracted from
  148. `user_record`.
  149. Fall back to English message if language is not available.
  150. Pass `format_kwargs` to format function.
  151. """
  152. if (
  153. language is None
  154. and isinstance(update, dict)
  155. and 'from' in update
  156. and 'language_code' in update['from']
  157. ):
  158. language = update['from']['language_code']
  159. if (
  160. language is None
  161. and isinstance(user_record, dict)
  162. and 'language_code' in user_record
  163. ):
  164. language = user_record['language_code']
  165. if language is None:
  166. language = 'en'
  167. result = self.messages
  168. for field in fields:
  169. if field not in result:
  170. logging.error(
  171. "Please define self.message{f}".format(
  172. f=''.join(
  173. '[\'{field}\']'.format(
  174. field=field
  175. )
  176. for field in fields
  177. )
  178. )
  179. )
  180. return "Invalid message!"
  181. result = result[field]
  182. if language not in result:
  183. language = extract(
  184. language,
  185. ender='-'
  186. )
  187. if language not in result:
  188. language = 'en'
  189. if language not in result:
  190. logging.error(
  191. "Please define self.message{f}['en']".format(
  192. f=''.join(
  193. '[\'{field}\']'.format(
  194. field=field
  195. )
  196. for field in fields
  197. )
  198. )
  199. )
  200. return "Invalid message!"
  201. return result[language].format(
  202. **format_kwargs
  203. )
  204. async def on_chat_message(self, update, user_record=None):
  205. """Handle text message.
  206. This method is deprecated: use `text_message_handler` instead.
  207. """
  208. return await self.text_message_handler(
  209. update=update,
  210. user_record=user_record
  211. )
  212. def set_inline_result_handler(self, user_id, result_id, func):
  213. """Associate a `func` with a `result_id` for `user_id`.
  214. This method is deprecated: use `set_chosen_inline_result_handler`
  215. instead.
  216. """
  217. if not asyncio.iscoroutinefunction(func):
  218. async def _func(update):
  219. return func(update)
  220. else:
  221. _func = func
  222. return self.set_chosen_inline_result_handler(
  223. user_id=user_id,
  224. result_id=result_id,
  225. handler=_func
  226. )
  227. async def handle_pinned_message(self, update, user_record=None):
  228. """Handle pinned message chat action."""
  229. if self.maintenance:
  230. return
  231. answerer = None
  232. for criteria, handler in self.chat_actions['pinned'].items():
  233. if criteria(update):
  234. answerer = handler['function']
  235. break
  236. if answerer is None:
  237. return
  238. elif asyncio.iscoroutinefunction(answerer):
  239. answer = await answerer(update)
  240. else:
  241. answer = answerer(update)
  242. if answer:
  243. try:
  244. return await self.send_message(
  245. answer=answer,
  246. chat_id=update['chat']['id']
  247. )
  248. except Exception as e:
  249. logging.error(
  250. "Failed to process answer:\n{}".format(
  251. e
  252. ),
  253. exc_info=True
  254. )
  255. return
  256. async def handle_photo_message(self, update, user_record=None):
  257. """Handle photo chat message."""
  258. user_id = update['from']['id'] if 'from' in update else None
  259. answerer, answer = None, None
  260. if self.maintenance:
  261. if update['chat']['id'] > 0:
  262. answer = self.maintenance_message
  263. elif user_id in self.custom_photo_parsers:
  264. answerer = self.custom_photo_parsers[user_id]
  265. del self.custom_photo_parsers[user_id]
  266. if answerer:
  267. if asyncio.iscoroutinefunction(answerer):
  268. answer = await answerer(update)
  269. else:
  270. answer = answerer(update)
  271. if answer:
  272. try:
  273. return await self.send_message(answer=answer, chat_id=update)
  274. except Exception as e:
  275. logging.error(
  276. "Failed to process answer:\n{}".format(
  277. e
  278. ),
  279. exc_info=True
  280. )
  281. return
  282. async def handle_location(self, update):
  283. """Handle location sent by user."""
  284. user_id = update['from']['id'] if 'from' in update else None
  285. answerer, answer = None, None
  286. if self.maintenance:
  287. if update['chat']['id'] > 0:
  288. answer = self.maintenance_message
  289. elif user_id in self.custom_location_parsers:
  290. answerer = self.custom_location_parsers[user_id]
  291. del self.custom_location_parsers[user_id]
  292. if answerer:
  293. if asyncio.iscoroutinefunction(answerer):
  294. answer = await answerer(update)
  295. else:
  296. answer = answerer(update)
  297. if answer:
  298. try:
  299. return await self.send_message(answer=answer, chat_id=update)
  300. except Exception as e:
  301. logging.error(
  302. "Failed to process answer:\n{}".format(
  303. e
  304. ),
  305. exc_info=True
  306. )
  307. return
  308. def set_custom_parser(self, parser, update=None, user=None):
  309. """Set a custom parser for the user.
  310. This method is deprecated: use `set_individual_text_message_handler`
  311. instead.
  312. """
  313. return self.set_individual_text_message_handler(
  314. handler=parser,
  315. update=update,
  316. user_id=user
  317. )
  318. def set_custom_photo_parser(self, parser, update=None, user=None):
  319. """Set a custom photo parser for the user.
  320. Any photo chat update coming from the user will be handled by
  321. this custom parser instead of default parsers.
  322. Custom photo parsers last one single use, but their handler can
  323. call this function to provide multiple tries.
  324. """
  325. if user and type(user) is int:
  326. pass
  327. elif type(update) is int:
  328. user = update
  329. elif type(user) is dict:
  330. user = (
  331. user['from']['id']
  332. if 'from' in user
  333. and 'id' in user['from']
  334. else None
  335. )
  336. elif not user and type(update) is dict:
  337. user = (
  338. update['from']['id']
  339. if 'from' in update
  340. and 'id' in update['from']
  341. else None
  342. )
  343. else:
  344. raise TypeError(
  345. 'Invalid user.\nuser: {}\nupdate: {}'.format(
  346. user,
  347. update
  348. )
  349. )
  350. if not type(user) is int:
  351. raise TypeError(
  352. 'User {} is not an int id'.format(
  353. user
  354. )
  355. )
  356. if not callable(parser):
  357. raise TypeError(
  358. 'Parser {} is not a callable'.format(
  359. parser.__name__
  360. )
  361. )
  362. self.custom_photo_parsers[user] = parser
  363. return
  364. def set_custom_location_parser(self, parser, update=None, user=None):
  365. """Set a custom location parser for the user.
  366. Any location chat update coming from the user will be handled by
  367. this custom parser instead of default parsers.
  368. Custom location parsers last one single use, but their handler can
  369. call this function to provide multiple tries.
  370. """
  371. if user and type(user) is int:
  372. pass
  373. elif type(update) is int:
  374. user = update
  375. elif type(user) is dict:
  376. user = (
  377. user['from']['id']
  378. if 'from' in user
  379. and 'id' in user['from']
  380. else None
  381. )
  382. elif not user and type(update) is dict:
  383. user = (
  384. update['from']['id']
  385. if 'from' in update
  386. and 'id' in update['from']
  387. else None
  388. )
  389. else:
  390. raise TypeError(
  391. 'Invalid user.\nuser: {}\nupdate: {}'.format(
  392. user,
  393. update
  394. )
  395. )
  396. if not type(user) is int:
  397. raise TypeError(
  398. 'User {} is not an int id'.format(
  399. user
  400. )
  401. )
  402. if not callable(parser):
  403. raise TypeError(
  404. 'Parser {} is not a callable'.format(
  405. parser.__name__
  406. )
  407. )
  408. self.custom_location_parsers[user] = parser
  409. return
  410. def command(self, command, aliases=None, show_in_keyboard=False,
  411. descr="", auth='admin', description=None,
  412. authorization_level=None):
  413. """Define a bot command.
  414. `descr` and `auth` parameters are deprecated: use `description` and
  415. `authorization_level` instead.
  416. """
  417. authorization_level = authorization_level or auth
  418. description = description or descr
  419. return super().command(
  420. command=command,
  421. aliases=aliases,
  422. show_in_keyboard=show_in_keyboard,
  423. description=description,
  424. authorization_level=authorization_level
  425. )
  426. def parser(self, condition, descr='', auth='admin', argument='text',
  427. description=None,
  428. authorization_level=None):
  429. """Define a message parser.
  430. `descr` and `auth` parameters are deprecated: use `description` and
  431. `authorization_level` instead.
  432. """
  433. authorization_level = authorization_level or auth
  434. description = description or descr
  435. return super().parser(
  436. condition=condition,
  437. description=description,
  438. authorization_level=authorization_level,
  439. argument=argument
  440. )
  441. def pinned(self, condition, descr='', auth='admin'):
  442. """Handle pinned messages.
  443. Decorator: `@bot.pinned(condition)`
  444. If condition evaluates True when run on a pinned_message update,
  445. such decorated function gets called on update.
  446. Conditions are evaluated in order; when one is True,
  447. others will be skipped.
  448. `descr` is a description
  449. `auth` is the lowest authorization level needed to run the command
  450. """
  451. if not callable(condition):
  452. raise TypeError(
  453. 'Condition {c} is not a callable'.format(
  454. c=condition.__name__
  455. )
  456. )
  457. def decorator(func):
  458. if asyncio.iscoroutinefunction(func):
  459. async def decorated(message):
  460. logging.info(
  461. "PINNED MESSAGE MATCHING({c}) @{n} FROM({f})".format(
  462. c=condition.__name__,
  463. n=self.name,
  464. f=(
  465. message['from']
  466. if 'from' in message
  467. else message['chat']
  468. )
  469. )
  470. )
  471. if self.authorization_function(message, auth):
  472. return await func(message)
  473. return
  474. else:
  475. def decorated(message):
  476. logging.info(
  477. "PINNED MESSAGE MATCHING({c}) @{n} FROM({f})".format(
  478. c=condition.__name__,
  479. n=self.name,
  480. f=(
  481. message['from']
  482. if 'from' in message
  483. else message['chat']
  484. )
  485. )
  486. )
  487. if self.authorization_function(message, auth):
  488. return func(message)
  489. return
  490. self.chat_actions['pinned'][condition] = dict(
  491. function=decorated,
  492. descr=descr,
  493. auth=auth
  494. )
  495. return decorator
  496. def button(self, data=None, descr='', auth='admin',
  497. authorization_level=None, prefix=None, description=None,
  498. separator=None):
  499. """Define a bot button.
  500. `descr` and `auth` parameters are deprecated: use `description` and
  501. `authorization_level` instead.
  502. `data` parameter renamed `prefix`.
  503. """
  504. authorization_level = authorization_level or auth
  505. description = description or descr
  506. prefix = prefix or data
  507. return super().button(
  508. prefix=prefix,
  509. separator=separator,
  510. description=description,
  511. authorization_level=authorization_level,
  512. )
  513. def query(self, condition, descr='', auth='admin', description=None,
  514. authorization_level=None):
  515. """Define an inline query.
  516. `descr` and `auth` parameters are deprecated: use `description` and
  517. `authorization_level` instead.
  518. """
  519. authorization_level = authorization_level or auth
  520. description = description or descr
  521. return super().query(
  522. condition=condition,
  523. description=description,
  524. authorization_level=authorization_level,
  525. )
  526. async def edit_message(self, update, *args, **kwargs):
  527. """Edit given update with given *args and **kwargs.
  528. This method is deprecated: use `edit_message_text` instead.
  529. """
  530. return await self.edit_message_text(
  531. *args,
  532. update=update,
  533. **kwargs
  534. )
  535. async def send_message(self, answer=dict(), chat_id=None, text='',
  536. parse_mode="HTML", disable_web_page_preview=None,
  537. disable_notification=None, reply_to_message_id=None,
  538. reply_markup=None, update=dict(),
  539. reply_to_update=False, send_default_keyboard=True):
  540. """Send a message.
  541. This method is deprecated: use `super().send_message` instead.
  542. """
  543. if update is None:
  544. update = dict()
  545. parameters = dict()
  546. for parameter, value in locals().items():
  547. if parameter in ['self', 'answer', 'parameters', '__class__']:
  548. continue
  549. if parameter in answer:
  550. parameters[parameter] = answer[parameter]
  551. else:
  552. parameters[parameter] = value
  553. if type(parameters['chat_id']) is dict:
  554. parameters['update'] = parameters['chat_id']
  555. del parameters['chat_id']
  556. return await super().send_message(**parameters)
  557. async def send_photo(self, chat_id=None, answer=dict(),
  558. photo=None, caption='', parse_mode='HTML',
  559. disable_notification=None, reply_to_message_id=None,
  560. reply_markup=None, use_stored=True,
  561. second_chance=False, use_stored_file_id=None,
  562. update=dict(), reply_to_update=False,
  563. send_default_keyboard=True):
  564. """Send a photo.
  565. This method is deprecated: use `super().send_photo` instead.
  566. """
  567. if update is None:
  568. update = dict()
  569. if use_stored is not None:
  570. use_stored_file_id = use_stored
  571. parameters = dict()
  572. for parameter, value in locals().items():
  573. if parameter in ['self', 'answer', 'parameters', '__class__',
  574. 'second_chance', 'use_stored']:
  575. continue
  576. if parameter in answer:
  577. parameters[parameter] = answer[parameter]
  578. else:
  579. parameters[parameter] = value
  580. if type(parameters['chat_id']) is dict:
  581. parameters['update'] = parameters['chat_id']
  582. del parameters['chat_id']
  583. return await super().send_photo(**parameters)
  584. async def send_and_destroy(self, chat_id, answer,
  585. timer=60, mode='text', **kwargs):
  586. """Send a message or photo and delete it after `timer` seconds."""
  587. if mode == 'text':
  588. sent_message = await self.send_message(
  589. chat_id=chat_id,
  590. answer=answer,
  591. **kwargs
  592. )
  593. elif mode == 'pic':
  594. sent_message = await self.send_photo(
  595. chat_id=chat_id,
  596. answer=answer,
  597. **kwargs
  598. )
  599. if sent_message is None:
  600. return
  601. self.to_be_destroyed.append(sent_message)
  602. await asyncio.sleep(timer)
  603. if await self.delete_message(sent_message):
  604. self.to_be_destroyed.remove(sent_message)
  605. return
  606. async def wait_and_obscure(self, update, when, inline_message_id):
  607. """Obscure messages which can't be deleted.
  608. Obscure an inline_message `timer` seconds after sending it,
  609. by editing its text or caption.
  610. At the moment Telegram won't let bots delete sent inline query results.
  611. """
  612. if type(when) is int:
  613. when = datetime.datetime.now() + datetime.timedelta(seconds=when)
  614. assert type(when) is datetime.datetime, (
  615. "when must be a datetime instance or a number of seconds (int) "
  616. "to be awaited"
  617. )
  618. if 'inline_message_id' not in update:
  619. logging.info(
  620. "This inline query result owns no inline_keyboard, so it "
  621. "can't be modified"
  622. )
  623. return
  624. inline_message_id = update['inline_message_id']
  625. self.to_be_obscured.append(inline_message_id)
  626. await sleep_until(when)
  627. try:
  628. await self.editMessageCaption(
  629. inline_message_id=inline_message_id,
  630. text="Time over"
  631. )
  632. except Exception:
  633. try:
  634. await self.editMessageText(
  635. inline_message_id=inline_message_id,
  636. text="Time over"
  637. )
  638. except Exception as e:
  639. logging.error(
  640. "Couldn't obscure message\n{}\n\n{}".format(
  641. inline_message_id,
  642. e
  643. )
  644. )
  645. self.to_be_obscured.remove(inline_message_id)
  646. return
  647. async def save_picture(self, update, file_name=None, path='img/',
  648. extension='jpg'):
  649. """Store `update` picture as `path`/`file_name`.`extension`."""
  650. if not path.endswith('/'):
  651. path = '{p}/'.format(
  652. p=path
  653. )
  654. if not os.path.isdir(path):
  655. path = '{path}/img/'.format(
  656. path=self.path
  657. )
  658. if file_name is None:
  659. file_name = get_secure_key(length=6)
  660. if file_name.endswith('.'):
  661. file_name = file_name[:-1]
  662. complete_file_name = '{path}{name}.{ext}'.format(
  663. path=self.path,
  664. name=file_name,
  665. ext=extension
  666. )
  667. while os.path.isfile(complete_file_name):
  668. file_name += get_secure_key(length=1)
  669. complete_file_name = '{path}{name}.{ext}'.format(
  670. path=self.path,
  671. name=file_name,
  672. ext=extension
  673. )
  674. try:
  675. await self.download_file(
  676. update['photo'][-1]['file_id'],
  677. complete_file_name
  678. )
  679. except Exception as e:
  680. return dict(
  681. result=1, # Error
  682. file_name=None,
  683. error=e
  684. )
  685. return dict(
  686. result=0, # Success
  687. file_name=complete_file_name,
  688. error=None
  689. )
  690. def stop_bots(self):
  691. """Exit script with code 0.
  692. This method is deprecated: use `Bot.stop` instead.
  693. """
  694. self.__class__.stop(
  695. message=f"Stopping bots via bot `@{self.name}` method.",
  696. final_state=0
  697. )
  698. def restart_bots(self):
  699. """Restart the script exiting with code 65.
  700. This method is deprecated: use `Bot.stop` instead.
  701. """
  702. self.__class__.stop(
  703. message=f"Restarting bots via bot `@{self.name}` method.",
  704. final_state=65
  705. )
  706. async def delete_and_obscure_messages(self):
  707. """Run after stop, before the script exits.
  708. Await final tasks, obscure and delete pending messages,
  709. log current operation (stop/restart).
  710. """
  711. for message in self.to_be_destroyed:
  712. try:
  713. await self.delete_message(message)
  714. except Exception as e:
  715. logging.error(
  716. "Couldn't delete message\n{}\n\n{}".format(
  717. message,
  718. e
  719. )
  720. )
  721. for inline_message_id in self.to_be_obscured:
  722. try:
  723. await self.editMessageCaption(
  724. inline_message_id,
  725. text="Time over"
  726. )
  727. except Exception:
  728. try:
  729. await self.editMessageText(
  730. inline_message_id=inline_message_id,
  731. text="Time over"
  732. )
  733. except Exception as e:
  734. logging.error(
  735. "Couldn't obscure message\n{}\n\n{}".format(
  736. inline_message_id,
  737. e
  738. )
  739. )
  740. @classmethod
  741. def run(cls, loop=None, *args, **kwargs):
  742. """Call this method to run the async bots.
  743. This method is deprecated: use `super(Bot, cls).run` instead.
  744. `loop` must not be determined outside that method.
  745. """
  746. for bot in cls.bots:
  747. bot.additional_task('AFTER')(bot.delete_and_obscure_messages)
  748. return super(Bot, cls).run(*args, **kwargs)