Queer European MD passionate about IT

bot.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. """Provide a simple Bot object, mirroring Telegram API methods.
  2. camelCase methods mirror API directly, while snake_case ones act as middlewares
  3. someway.
  4. """
  5. # Standard library modules
  6. import asyncio
  7. import logging
  8. # Third party modules
  9. from aiohttp import web
  10. # Project modules
  11. from api import TelegramBot, TelegramError
  12. from utilities import get_secure_key
  13. # Do not log aiohttp `INFO` and `DEBUG` levels
  14. logging.getLogger('aiohttp').setLevel(logging.WARNING)
  15. class Bot(TelegramBot):
  16. """Simple Bot object, providing methods corresponding to Telegram bot API.
  17. Multiple Bot() instances may be run together, along with a aiohttp web app.
  18. """
  19. bots = []
  20. runner = None
  21. local_host = 'localhost'
  22. port = 3000
  23. final_state = 0
  24. _maintenance_message = ("I am currently under maintenance!\n"
  25. "Please retry later...")
  26. def __init__(
  27. self, token, hostname='', certificate=None, max_connections=40,
  28. allowed_updates=[]
  29. ):
  30. """Init a bot instance.
  31. token : str
  32. Telegram bot API token.
  33. hostname : str
  34. Domain (or public IP address) for webhooks.
  35. certificate : str
  36. Path to domain certificate.
  37. max_connections : int (1 - 100)
  38. Maximum number of HTTPS connections allowed.
  39. allowed_updates : List(str)
  40. Allowed update types (empty list to allow all).
  41. """
  42. self.__class__.bots.append(self)
  43. super().__init__(token)
  44. self._offset = 0
  45. self._hostname = hostname
  46. self._certificate = certificate
  47. self._max_connections = max_connections
  48. self._allowed_updates = allowed_updates
  49. self._session_token = get_secure_key(length=10)
  50. self._name = None
  51. self._telegram_id = None
  52. # The following routing table associates each type of Telegram `update`
  53. # with a Bot method to be invoked on it.
  54. self.routing_table = {
  55. 'message': self.message_router,
  56. 'edited_message': self.edited_message_handler,
  57. 'channel_post': self.channel_post_handler,
  58. 'edited_channel_post': self.edited_channel_post_handler,
  59. 'inline_query': self.inline_query_handler,
  60. 'chosen_inline_result': self.chosen_inline_result_handler,
  61. 'callback_query': self.callback_query_handler,
  62. 'shipping_query': self.shipping_query_handler,
  63. 'pre_checkout_query': self.pre_checkout_query_handler,
  64. 'poll': self.poll_handler,
  65. }
  66. self.message_handlers = {
  67. 'text': self.text_message_handler,
  68. 'audio': self.audio_message_handler,
  69. 'document': self.document_message_handler,
  70. 'animation': self.animation_message_handler,
  71. 'game': self.game_message_handler,
  72. 'photo': self.photo_message_handler,
  73. 'sticker': self.sticker_message_handler,
  74. 'video': self.video_message_handler,
  75. 'voice': self.voice_message_handler,
  76. 'video_note': self.video_note_message_handler,
  77. 'contact': self.contact_message_handler,
  78. 'location': self.location_message_handler,
  79. 'venue': self.venue_message_handler,
  80. 'poll': self.poll_message_handler,
  81. 'new_chat_members': self.new_chat_members_message_handler,
  82. 'left_chat_member': self.left_chat_member_message_handler,
  83. 'new_chat_title': self.new_chat_title_message_handler,
  84. 'new_chat_photo': self.new_chat_photo_message_handler,
  85. 'delete_chat_photo': self.delete_chat_photo_message_handler,
  86. 'group_chat_created': self.group_chat_created_message_handler,
  87. 'supergroup_chat_created': (
  88. self.supergroup_chat_created_message_handler
  89. ),
  90. 'channel_chat_created': self.channel_chat_created_message_handler,
  91. 'migrate_to_chat_id': self.migrate_to_chat_id_message_handler,
  92. 'migrate_from_chat_id': self.migrate_from_chat_id_message_handler,
  93. 'pinned_message': self.pinned_message_message_handler,
  94. 'invoice': self.invoice_message_handler,
  95. 'successful_payment': self.successful_payment_message_handler,
  96. 'connected_website': self.connected_website_message_handler,
  97. 'passport_data': self.passport_data_message_handler
  98. }
  99. self._under_maintenance = False
  100. self._allowed_during_maintenance = []
  101. self._maintenance_message = None
  102. return
  103. @property
  104. def hostname(self):
  105. """Hostname for the webhook URL.
  106. It must be a public domain or IP address. Port may be specified.
  107. A custom webhook url, including bot token and a random token, will be
  108. generated for Telegram to post new updates.
  109. """
  110. return self._hostname
  111. @property
  112. def webhook_url(self):
  113. """URL where Telegram servers should post new updates.
  114. It must be a public domain name or IP address. Port may be specified.
  115. """
  116. if not self.hostname:
  117. return ''
  118. return (
  119. f"{self.hostname}/webhook/{self.token}_{self.session_token}/"
  120. )
  121. @property
  122. def webhook_local_address(self):
  123. """Local address where Telegram updates are routed by revers proxy."""
  124. return (
  125. f"/webhook/{self.token}_{self.session_token}/"
  126. )
  127. @property
  128. def certificate(self):
  129. """Public certificate for `webhook_url`.
  130. May be self-signed
  131. """
  132. return self._certificate
  133. @property
  134. def max_connections(self):
  135. """Maximum number of simultaneous HTTPS connections allowed.
  136. Telegram will open as many connections as possible to boost bot’s
  137. throughput, lower values limit the load on bot‘s server.
  138. """
  139. return self._max_connections
  140. @property
  141. def allowed_updates(self):
  142. """List of update types to be retrieved.
  143. Empty list to allow all updates.
  144. """
  145. return self._allowed_updates
  146. @property
  147. def name(self):
  148. """Bot name."""
  149. return self._name
  150. @property
  151. def telegram_id(self):
  152. """Telegram id of this bot."""
  153. return self._telegram_id
  154. @property
  155. def session_token(self):
  156. """Return a token generated with the current instantiation."""
  157. return self._session_token
  158. @property
  159. def offset(self):
  160. """Return last update id.
  161. Useful to ignore repeated updates and restore original update order.
  162. """
  163. return self._offset
  164. @property
  165. def under_maintenance(self):
  166. """Return True if bot is under maintenance.
  167. While under maintenance, bot will reply `self.maintenance_message` to
  168. any update, except those which `self.is_allowed_during_maintenance`
  169. returns True for.
  170. """
  171. return self._under_maintenance
  172. @property
  173. def allowed_during_maintenance(self):
  174. """Return the list of criteria to allow an update during maintenance.
  175. If any of this criteria returns True on an update, that update will be
  176. handled even during maintenance.
  177. """
  178. return self._allowed_during_maintenance
  179. @property
  180. def maintenance_message(self):
  181. """Message to be returned if bot is under maintenance.
  182. If instance message is not set, class message is returned.
  183. """
  184. if self._maintenance_message:
  185. return self._maintenance_message
  186. if self.__class__.maintenance_message:
  187. return self.__class__._maintenance_message
  188. return ("I am currently under maintenance!\n"
  189. "Please retry later...")
  190. async def message_router(self, update):
  191. """Route Telegram `message` update to appropriate message handler."""
  192. for key, value in update.items():
  193. if key in self.message_handlers:
  194. return await self.message_handlers[key](update)
  195. logging.error(
  196. f"The following message update was received: {update}\n"
  197. "However, this message type is unknown."
  198. )
  199. async def edited_message_handler(self, update):
  200. """Handle Telegram `edited_message` update."""
  201. logging.info(
  202. f"The following update was received: {update}\n"
  203. "However, this edited_message handler does nothing yet."
  204. )
  205. return
  206. async def channel_post_handler(self, update):
  207. """Handle Telegram `channel_post` update."""
  208. logging.info(
  209. f"The following update was received: {update}\n"
  210. "However, this channel_post handler does nothing yet."
  211. )
  212. return
  213. async def edited_channel_post_handler(self, update):
  214. """Handle Telegram `edited_channel_post` update."""
  215. logging.info(
  216. f"The following update was received: {update}\n"
  217. "However, this edited_channel_post handler does nothing yet."
  218. )
  219. return
  220. async def inline_query_handler(self, update):
  221. """Handle Telegram `inline_query` update."""
  222. logging.info(
  223. f"The following update was received: {update}\n"
  224. "However, this inline_query handler does nothing yet."
  225. )
  226. return
  227. async def chosen_inline_result_handler(self, update):
  228. """Handle Telegram `chosen_inline_result` update."""
  229. logging.info(
  230. f"The following update was received: {update}\n"
  231. "However, this chosen_inline_result handler does nothing yet."
  232. )
  233. return
  234. async def callback_query_handler(self, update):
  235. """Handle Telegram `callback_query` update."""
  236. logging.info(
  237. f"The following update was received: {update}\n"
  238. "However, this callback_query handler does nothing yet."
  239. )
  240. return
  241. async def shipping_query_handler(self, update):
  242. """Handle Telegram `shipping_query` update."""
  243. logging.info(
  244. f"The following update was received: {update}\n"
  245. "However, this shipping_query handler does nothing yet."
  246. )
  247. return
  248. async def pre_checkout_query_handler(self, update):
  249. """Handle Telegram `pre_checkout_query` update."""
  250. logging.info(
  251. f"The following update was received: {update}\n"
  252. "However, this pre_checkout_query handler does nothing yet."
  253. )
  254. return
  255. async def poll_handler(self, update):
  256. """Handle Telegram `poll` update."""
  257. logging.info(
  258. f"The following update was received: {update}\n"
  259. "However, this poll handler does nothing yet."
  260. )
  261. return
  262. async def text_message_handler(self, update):
  263. """Handle `text` message update."""
  264. replier, reply = None, None
  265. text = update['text'].lower()
  266. user_id = update['from']['id'] if 'from' in update else None
  267. if user_id in self.custom_text_message_handlers: # Custom handler
  268. replier = self.custom_text_message_handlers[user_id]
  269. del self.custom_text_message_handlers[user_id]
  270. elif text.startswith('/'): # Command handler
  271. # A command must always start with the ‘/’ symbol and may not be
  272. # longer than 32 characters.
  273. # Commands can use latin letters, numbers and underscores.
  274. print(text)
  275. command = re.search(
  276. r"([A-z_1-9]){1,32}",
  277. text
  278. ).group(0) # Get the first group characters matching pattern
  279. if command in self.commands:
  280. replier = self.commands[command]['function']
  281. elif update['chat']['id'] > 0:
  282. replier = self.unknown_command_message
  283. else: # Check alias and text parsers
  284. logging.info("#TODO alias and text parsers")
  285. if replier:
  286. if asyncio.iscoroutinefunction(replier):
  287. reply = await replier(update)
  288. else:
  289. reply = replier(update)
  290. if reply:
  291. if type(reply) is str:
  292. reply = dict(text=reply)
  293. try:
  294. return await self.send_message(update=update, **reply)
  295. except Exception as e:
  296. logging.error(
  297. f"Failed to handle text message:\n{e}",
  298. exc_info=True
  299. )
  300. return
  301. async def audio_message_handler(self, update):
  302. """Handle `audio` message update."""
  303. logging.info(
  304. "A audio message update was received, "
  305. "but this handler does nothing yet."
  306. )
  307. async def document_message_handler(self, update):
  308. """Handle `document` message update."""
  309. logging.info(
  310. "A document message update was received, "
  311. "but this handler does nothing yet."
  312. )
  313. async def animation_message_handler(self, update):
  314. """Handle `animation` message update."""
  315. logging.info(
  316. "A animation message update was received, "
  317. "but this handler does nothing yet."
  318. )
  319. async def game_message_handler(self, update):
  320. """Handle `game` message update."""
  321. logging.info(
  322. "A game message update was received, "
  323. "but this handler does nothing yet."
  324. )
  325. async def photo_message_handler(self, update):
  326. """Handle `photo` message update."""
  327. logging.info(
  328. "A photo message update was received, "
  329. "but this handler does nothing yet."
  330. )
  331. async def sticker_message_handler(self, update):
  332. """Handle `sticker` message update."""
  333. logging.info(
  334. "A sticker message update was received, "
  335. "but this handler does nothing yet."
  336. )
  337. async def video_message_handler(self, update):
  338. """Handle `video` message update."""
  339. logging.info(
  340. "A video message update was received, "
  341. "but this handler does nothing yet."
  342. )
  343. async def voice_message_handler(self, update):
  344. """Handle `voice` message update."""
  345. logging.info(
  346. "A voice message update was received, "
  347. "but this handler does nothing yet."
  348. )
  349. async def video_note_message_handler(self, update):
  350. """Handle `video_note` message update."""
  351. logging.info(
  352. "A video_note message update was received, "
  353. "but this handler does nothing yet."
  354. )
  355. async def contact_message_handler(self, update):
  356. """Handle `contact` message update."""
  357. logging.info(
  358. "A contact message update was received, "
  359. "but this handler does nothing yet."
  360. )
  361. async def location_message_handler(self, update):
  362. """Handle `location` message update."""
  363. logging.info(
  364. "A location message update was received, "
  365. "but this handler does nothing yet."
  366. )
  367. async def venue_message_handler(self, update):
  368. """Handle `venue` message update."""
  369. logging.info(
  370. "A venue message update was received, "
  371. "but this handler does nothing yet."
  372. )
  373. async def poll_message_handler(self, update):
  374. """Handle `poll` message update."""
  375. logging.info(
  376. "A poll message update was received, "
  377. "but this handler does nothing yet."
  378. )
  379. async def new_chat_members_message_handler(self, update):
  380. """Handle `new_chat_members` message update."""
  381. logging.info(
  382. "A new_chat_members message update was received, "
  383. "but this handler does nothing yet."
  384. )
  385. async def left_chat_member_message_handler(self, update):
  386. """Handle `left_chat_member` message update."""
  387. logging.info(
  388. "A left_chat_member message update was received, "
  389. "but this handler does nothing yet."
  390. )
  391. async def new_chat_title_message_handler(self, update):
  392. """Handle `new_chat_title` message update."""
  393. logging.info(
  394. "A new_chat_title message update was received, "
  395. "but this handler does nothing yet."
  396. )
  397. async def new_chat_photo_message_handler(self, update):
  398. """Handle `new_chat_photo` message update."""
  399. logging.info(
  400. "A new_chat_photo message update was received, "
  401. "but this handler does nothing yet."
  402. )
  403. async def delete_chat_photo_message_handler(self, update):
  404. """Handle `delete_chat_photo` message update."""
  405. logging.info(
  406. "A delete_chat_photo message update was received, "
  407. "but this handler does nothing yet."
  408. )
  409. async def group_chat_created_message_handler(self, update):
  410. """Handle `group_chat_created` message update."""
  411. logging.info(
  412. "A group_chat_created message update was received, "
  413. "but this handler does nothing yet."
  414. )
  415. async def supergroup_chat_created_message_handler(self, update):
  416. """Handle `supergroup_chat_created` message update."""
  417. logging.info(
  418. "A supergroup_chat_created message update was received, "
  419. "but this handler does nothing yet."
  420. )
  421. async def channel_chat_created_message_handler(self, update):
  422. """Handle `channel_chat_created` message update."""
  423. logging.info(
  424. "A channel_chat_created message update was received, "
  425. "but this handler does nothing yet."
  426. )
  427. async def migrate_to_chat_id_message_handler(self, update):
  428. """Handle `migrate_to_chat_id` message update."""
  429. logging.info(
  430. "A migrate_to_chat_id message update was received, "
  431. "but this handler does nothing yet."
  432. )
  433. async def migrate_from_chat_id_message_handler(self, update):
  434. """Handle `migrate_from_chat_id` message update."""
  435. logging.info(
  436. "A migrate_from_chat_id message update was received, "
  437. "but this handler does nothing yet."
  438. )
  439. async def pinned_message_message_handler(self, update):
  440. """Handle `pinned_message` message update."""
  441. logging.info(
  442. "A pinned_message message update was received, "
  443. "but this handler does nothing yet."
  444. )
  445. async def invoice_message_handler(self, update):
  446. """Handle `invoice` message update."""
  447. logging.info(
  448. "A invoice message update was received, "
  449. "but this handler does nothing yet."
  450. )
  451. async def successful_payment_message_handler(self, update):
  452. """Handle `successful_payment` message update."""
  453. logging.info(
  454. "A successful_payment message update was received, "
  455. "but this handler does nothing yet."
  456. )
  457. async def connected_website_message_handler(self, update):
  458. """Handle `connected_website` message update."""
  459. logging.info(
  460. "A connected_website message update was received, "
  461. "but this handler does nothing yet."
  462. )
  463. async def passport_data_message_handler(self, update):
  464. """Handle `passport_data` message update."""
  465. logging.info(
  466. "A passport_data message update was received, "
  467. "but this handler does nothing yet."
  468. )
  469. @classmethod
  470. def set_class_maintenance_message(cls, maintenance_message):
  471. """Set class maintenance message.
  472. It will be returned if bot is under maintenance, unless and instance
  473. `_maintenance_message` is set.
  474. """
  475. cls._maintenance_message = maintenance_message
  476. def set_maintenance_message(self, maintenance_message):
  477. """Set instance maintenance message.
  478. It will be returned if bot is under maintenance.
  479. If instance message is None, default class message is used.
  480. """
  481. self._maintenance_message = maintenance_message
  482. def change_maintenance_status(self, maintenance_message=None, status=None):
  483. """Put the bot under maintenance or end it.
  484. While in maintenance, bot will reply to users with maintenance_message
  485. with a few exceptions.
  486. If status is not set, it is by default the opposite of the current one.
  487. Optionally, `maintenance_message` may be set.
  488. """
  489. if status is None:
  490. status = not self.under_maintenance
  491. assert type(status) is bool, "status must be a boolean value!"
  492. self._under_maintenance = status
  493. if maintenance_message:
  494. self.set_maintenance_message(maintenance_message)
  495. return self._under_maintenance # Return new status
  496. def is_allowed_during_maintenance(self, update):
  497. """Return True if update is allowed during maintenance.
  498. An update is allowed if any of the criteria in
  499. `self.allowed_during_maintenance` returns True called on it.
  500. """
  501. for criterion in self.allowed_during_maintenance:
  502. if criterion(update):
  503. return True
  504. return False
  505. def allow_during_maintenance(self, criterion):
  506. """Add a criterion to allow certain updates during maintenance.
  507. `criterion` must be a function taking a Telegram `update` dictionary
  508. and returning a boolean.
  509. ```# Example of criterion
  510. def allow_text_messages(update):
  511. if 'message' in update and 'text' in update['message']:
  512. return True
  513. return False
  514. ```
  515. """
  516. self._allowed_during_maintenance.append(criterion)
  517. async def handle_update_during_maintenance(self, update):
  518. """Handle an update while bot is under maintenance.
  519. Handle all types of updates.
  520. """
  521. if (
  522. 'message' in update
  523. and 'chat' in update['message']
  524. and update['message']['chat']['id'] > 0
  525. ):
  526. return await self.send_message(
  527. text=self.maintenance_message,
  528. update=update['message'],
  529. reply_to_update=True
  530. )
  531. elif 'callback_query' in update:
  532. pass
  533. elif 'inline_query' in update:
  534. await self.answer_inline_query(
  535. update['inline_query']['id'],
  536. self.maintenance_message,
  537. cache_time=30,
  538. is_personal=False,
  539. )
  540. return
  541. async def webhook_feeder(self, request):
  542. """Handle incoming HTTP `request`s.
  543. Get data, feed webhook and return and OK message.
  544. """
  545. update = await request.json()
  546. asyncio.ensure_future(
  547. self.route_update(update)
  548. )
  549. return web.Response(
  550. body='OK'.encode('utf-8')
  551. )
  552. async def get_me(self):
  553. """Get bot information.
  554. Restart bots if bot can't be got.
  555. """
  556. try:
  557. me = await self.getMe()
  558. if isinstance(me, Exception):
  559. raise me
  560. elif me is None:
  561. raise Exception('getMe returned None')
  562. self._name = me["username"]
  563. self._telegram_id = me['id']
  564. except Exception as e:
  565. logging.error(
  566. f"Information about bot with token {self.token} could not "
  567. f"be got. Restarting in 5 minutes...\n\n"
  568. f"Error information:\n{e}"
  569. )
  570. await asyncio.sleep(5*60)
  571. self.__class__.stop(
  572. 65,
  573. f"Information about bot with token {self.token} could not "
  574. "be got. Restarting..."
  575. )
  576. def setup(self):
  577. """Make bot ask for updates and handle responses."""
  578. if not self.webhook_url:
  579. asyncio.ensure_future(self.get_updates())
  580. else:
  581. asyncio.ensure_future(self.set_webhook())
  582. self.__class__.app.router.add_route(
  583. 'POST', self.webhook_local_address, self.webhook_feeder
  584. )
  585. async def close_sessions(self):
  586. """Close open sessions."""
  587. for session_name, session in self.sessions.items():
  588. await session.close()
  589. async def set_webhook(self, url=None, certificate=None,
  590. max_connections=None, allowed_updates=None):
  591. """Set a webhook if token is valid."""
  592. # Return if token is invalid
  593. await self.get_me()
  594. if self.name is None:
  595. return
  596. webhook_was_set = await self.setWebhook(
  597. url=url, certificate=certificate, max_connections=max_connections,
  598. allowed_updates=allowed_updates
  599. ) # `setWebhook` API method returns `True` on success
  600. webhook_information = await self.getWebhookInfo()
  601. if webhook_was_set:
  602. logging.info(
  603. f"Webhook was set correctly.\n"
  604. f"Webhook information: {webhook_information}"
  605. )
  606. else:
  607. logging.error(
  608. f"Failed to set webhook!\n"
  609. f"Webhook information: {webhook_information}"
  610. )
  611. async def get_updates(self, timeout=30, limit=100, allowed_updates=None,
  612. error_cooldown=10):
  613. """Get updates using long polling.
  614. timeout : int
  615. Timeout set for Telegram servers. Make sure that connection timeout
  616. is greater than `timeout`.
  617. limit : int (1 - 100)
  618. Max number of updates to be retrieved.
  619. allowed_updates : List(str)
  620. List of update types to be retrieved.
  621. Empty list to allow all updates.
  622. None to fallback to class default.
  623. """
  624. # Return if token is invalid
  625. await self.get_me()
  626. if self.name is None:
  627. return
  628. # Set custom list of allowed updates or fallback to class default list
  629. if allowed_updates is None:
  630. allowed_updates = self.allowed_updates
  631. await self.deleteWebhook() # Remove eventually active webhook
  632. update = None # Do not update offset if no update is received
  633. while True:
  634. updates = await self.getUpdates(
  635. offset=self._offset,
  636. timeout=timeout,
  637. limit=limit,
  638. allowed_updates=allowed_updates
  639. )
  640. if updates is None:
  641. continue
  642. elif isinstance(updates, TelegramError):
  643. logging.error(
  644. f"Waiting {error_cooldown} seconds before trying again..."
  645. )
  646. await asyncio.sleep(error_cooldown)
  647. continue
  648. for update in updates:
  649. asyncio.ensure_future(self.route_update(update))
  650. if update is not None:
  651. self._offset = update['update_id'] + 1
  652. async def route_update(self, update):
  653. """Pass `update` to proper method.
  654. Update objects have two keys:
  655. - `update_id` (which is used as offset while retrieving new updates)
  656. - One and only one of the following
  657. `message`
  658. `edited_message`
  659. `channel_post`
  660. `edited_channel_post`
  661. `inline_query`
  662. `chosen_inline_result`
  663. `callback_query`
  664. `shipping_query`
  665. `pre_checkout_query`
  666. `poll`
  667. """
  668. if (
  669. self.under_maintenance
  670. and not self.is_allowed_during_maintenance(update)
  671. ):
  672. return await self.handle_update_during_maintenance(update)
  673. for key, value in update.items():
  674. if key in self.routing_table:
  675. return await self.routing_table[key](value)
  676. logging.error(f"Unknown type of update.\n{update}")
  677. @classmethod
  678. async def start_app(cls):
  679. """Start running `aiohttp.web.Application`.
  680. It will route webhook-received updates and other custom paths.
  681. """
  682. assert cls.local_host is not None, "Invalid local host"
  683. assert cls.port is not None, "Invalid port"
  684. cls.runner = web.AppRunner(cls.app)
  685. await cls.runner.setup()
  686. cls.server = web.TCPSite(cls.runner, cls.local_host, cls.port)
  687. await cls.server.start()
  688. logging.info(f"App running at http://{cls.local_host}:{cls.port}")
  689. @classmethod
  690. async def stop_app(cls):
  691. """Close bot sessions and cleanup."""
  692. for bot in cls.bots:
  693. await bot.close_sessions()
  694. await cls.runner.cleanup()
  695. @classmethod
  696. def stop(cls, message, final_state=0):
  697. """Log a final `message`, stop loop and set exiting `code`.
  698. All bots and the web app will be terminated gracefully.
  699. The final state may be retrieved to get information about what stopped
  700. the bots.
  701. """
  702. logging.info(message)
  703. cls.final_state = final_state
  704. cls.loop.stop()
  705. return
  706. @classmethod
  707. def run(cls, local_host=None, port=None):
  708. """Run aiohttp web app and all Bot instances.
  709. Each bot will receive updates via long polling or webhook according to
  710. its initialization parameters.
  711. A single aiohttp.web.Application instance will be run (cls.app) on
  712. local_host:port and it may serve custom-defined routes as well.
  713. """
  714. if local_host is not None:
  715. cls.local_host = local_host
  716. if port is not None:
  717. cls.port = port
  718. for bot in cls.bots:
  719. bot.setup()
  720. asyncio.ensure_future(cls.start_app())
  721. try:
  722. cls.loop.run_forever()
  723. except KeyboardInterrupt:
  724. logging.info("Stopped by KeyboardInterrupt")
  725. except Exception as e:
  726. logging.error(f"{e}", exc_info=True)
  727. finally:
  728. cls.loop.run_until_complete(cls.stop_app())
  729. return cls.final_state