Queer European MD passionate about IT

bot.py 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313
  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. from collections import OrderedDict
  8. import io
  9. import logging
  10. import os
  11. import re
  12. # Third party modules
  13. from aiohttp import web
  14. # Project modules
  15. from api import TelegramBot, TelegramError
  16. from database import ObjectWithDatabase
  17. from utilities import escape_html_chars, get_secure_key, make_lines_of_buttons
  18. # Do not log aiohttp `INFO` and `DEBUG` levels
  19. logging.getLogger('aiohttp').setLevel(logging.WARNING)
  20. class Bot(TelegramBot, ObjectWithDatabase):
  21. """Simple Bot object, providing methods corresponding to Telegram bot API.
  22. Multiple Bot() instances may be run together, along with a aiohttp web app.
  23. """
  24. bots = []
  25. _path = '.'
  26. runner = None
  27. local_host = 'localhost'
  28. port = 3000
  29. final_state = 0
  30. _maintenance_message = ("I am currently under maintenance!\n"
  31. "Please retry later...")
  32. _authorization_denied_message = None
  33. _unknown_command_message = None
  34. TELEGRAM_MESSAGES_MAX_LEN = 4096
  35. def __init__(
  36. self, token, hostname='', certificate=None, max_connections=40,
  37. allowed_updates=[], database_url='bot.db'
  38. ):
  39. """Init a bot instance.
  40. token : str
  41. Telegram bot API token.
  42. hostname : str
  43. Domain (or public IP address) for webhooks.
  44. certificate : str
  45. Path to domain certificate.
  46. max_connections : int (1 - 100)
  47. Maximum number of HTTPS connections allowed.
  48. allowed_updates : List(str)
  49. Allowed update types (empty list to allow all).
  50. """
  51. # Append `self` to class list of instances
  52. self.__class__.bots.append(self)
  53. # Call superclasses constructors with proper arguments
  54. TelegramBot.__init__(self, token)
  55. ObjectWithDatabase.__init__(self, database_url=database_url)
  56. self._path = None
  57. self._offset = 0
  58. self._hostname = hostname
  59. self._certificate = certificate
  60. self._max_connections = max_connections
  61. self._allowed_updates = allowed_updates
  62. self._session_token = get_secure_key(length=10)
  63. self._name = None
  64. self._telegram_id = None
  65. # The following routing table associates each type of Telegram `update`
  66. # with a Bot method to be invoked on it.
  67. self.routing_table = {
  68. 'message': self.message_router,
  69. 'edited_message': self.edited_message_handler,
  70. 'channel_post': self.channel_post_handler,
  71. 'edited_channel_post': self.edited_channel_post_handler,
  72. 'inline_query': self.inline_query_handler,
  73. 'chosen_inline_result': self.chosen_inline_result_handler,
  74. 'callback_query': self.callback_query_handler,
  75. 'shipping_query': self.shipping_query_handler,
  76. 'pre_checkout_query': self.pre_checkout_query_handler,
  77. 'poll': self.poll_handler,
  78. }
  79. self.message_handlers = {
  80. 'text': self.text_message_handler,
  81. 'audio': self.audio_message_handler,
  82. 'document': self.document_message_handler,
  83. 'animation': self.animation_message_handler,
  84. 'game': self.game_message_handler,
  85. 'photo': self.photo_message_handler,
  86. 'sticker': self.sticker_message_handler,
  87. 'video': self.video_message_handler,
  88. 'voice': self.voice_message_handler,
  89. 'video_note': self.video_note_message_handler,
  90. 'contact': self.contact_message_handler,
  91. 'location': self.location_message_handler,
  92. 'venue': self.venue_message_handler,
  93. 'poll': self.poll_message_handler,
  94. 'new_chat_members': self.new_chat_members_message_handler,
  95. 'left_chat_member': self.left_chat_member_message_handler,
  96. 'new_chat_title': self.new_chat_title_message_handler,
  97. 'new_chat_photo': self.new_chat_photo_message_handler,
  98. 'delete_chat_photo': self.delete_chat_photo_message_handler,
  99. 'group_chat_created': self.group_chat_created_message_handler,
  100. 'supergroup_chat_created': (
  101. self.supergroup_chat_created_message_handler
  102. ),
  103. 'channel_chat_created': self.channel_chat_created_message_handler,
  104. 'migrate_to_chat_id': self.migrate_to_chat_id_message_handler,
  105. 'migrate_from_chat_id': self.migrate_from_chat_id_message_handler,
  106. 'pinned_message': self.pinned_message_message_handler,
  107. 'invoice': self.invoice_message_handler,
  108. 'successful_payment': self.successful_payment_message_handler,
  109. 'connected_website': self.connected_website_message_handler,
  110. 'passport_data': self.passport_data_message_handler
  111. }
  112. self.individual_text_message_handlers = dict()
  113. self.commands = dict()
  114. self.command_aliases = dict()
  115. self.text_message_parsers = OrderedDict()
  116. self._unknown_command_message = None
  117. self._under_maintenance = False
  118. self._allowed_during_maintenance = []
  119. self._maintenance_message = None
  120. # Default chat_id getter: same chat as update
  121. self.get_chat_id = lambda update: (
  122. update['message']['chat']['id']
  123. if 'message' in update and 'chat' in update['message']
  124. else update['chat']['id']
  125. if 'chat' in update
  126. else None
  127. )
  128. # Message to be returned if user is not allowed to call method
  129. self._authorization_denied_message = None
  130. # Default authorization function (always return True)
  131. self.authorization_function = lambda update, authorization_level: True
  132. self.default_reply_keyboard_elements = []
  133. self._default_keyboard = dict()
  134. return
  135. @property
  136. def path(self):
  137. """Path where files should be looked for.
  138. If no instance path is set, return class path.
  139. """
  140. return self._path or self.__class__._path
  141. @classmethod
  142. def set_class_path(csl, path):
  143. """Set class path attribute."""
  144. csl._path = path
  145. def set_path(self, path):
  146. """Set instance path attribute."""
  147. self._path = path
  148. @property
  149. def hostname(self):
  150. """Hostname for the webhook URL.
  151. It must be a public domain or IP address. Port may be specified.
  152. A custom webhook url, including bot token and a random token, will be
  153. generated for Telegram to post new updates.
  154. """
  155. return self._hostname
  156. @property
  157. def webhook_url(self):
  158. """URL where Telegram servers should post new updates.
  159. It must be a public domain name or IP address. Port may be specified.
  160. """
  161. if not self.hostname:
  162. return ''
  163. return (
  164. f"{self.hostname}/webhook/{self.token}_{self.session_token}/"
  165. )
  166. @property
  167. def webhook_local_address(self):
  168. """Local address where Telegram updates are routed by revers proxy."""
  169. return (
  170. f"/webhook/{self.token}_{self.session_token}/"
  171. )
  172. @property
  173. def certificate(self):
  174. """Public certificate for `webhook_url`.
  175. May be self-signed
  176. """
  177. return self._certificate
  178. @property
  179. def max_connections(self):
  180. """Maximum number of simultaneous HTTPS connections allowed.
  181. Telegram will open as many connections as possible to boost bot’s
  182. throughput, lower values limit the load on bot‘s server.
  183. """
  184. return self._max_connections
  185. @property
  186. def allowed_updates(self):
  187. """List of update types to be retrieved.
  188. Empty list to allow all updates.
  189. """
  190. return self._allowed_updates
  191. @property
  192. def name(self):
  193. """Bot name."""
  194. return self._name
  195. @property
  196. def telegram_id(self):
  197. """Telegram id of this bot."""
  198. return self._telegram_id
  199. @property
  200. def session_token(self):
  201. """Return a token generated with the current instantiation."""
  202. return self._session_token
  203. @property
  204. def offset(self):
  205. """Return last update id.
  206. Useful to ignore repeated updates and restore original update order.
  207. """
  208. return self._offset
  209. @property
  210. def under_maintenance(self):
  211. """Return True if bot is under maintenance.
  212. While under maintenance, bot will reply `self.maintenance_message` to
  213. any update, except those which `self.is_allowed_during_maintenance`
  214. returns True for.
  215. """
  216. return self._under_maintenance
  217. @property
  218. def allowed_during_maintenance(self):
  219. """Return the list of criteria to allow an update during maintenance.
  220. If any of this criteria returns True on an update, that update will be
  221. handled even during maintenance.
  222. """
  223. return self._allowed_during_maintenance
  224. @property
  225. def maintenance_message(self):
  226. """Message to be returned if bot is under maintenance.
  227. If instance message is not set, class message is returned.
  228. """
  229. if self._maintenance_message:
  230. return self._maintenance_message
  231. if self.__class__.maintenance_message:
  232. return self.__class__._maintenance_message
  233. return ("I am currently under maintenance!\n"
  234. "Please retry later...")
  235. @property
  236. def authorization_denied_message(self):
  237. """Return this text if user is unauthorized to make a request.
  238. If instance message is not set, class message is returned.
  239. """
  240. if self._authorization_denied_message:
  241. return self._authorization_denied_message
  242. return self.__class__._authorization_denied_message
  243. @property
  244. def default_keyboard(self):
  245. """Get the default keyboard.
  246. It is sent when reply_markup is left blank and chat is private.
  247. """
  248. return self._default_keyboard
  249. @property
  250. def unknown_command_message(self):
  251. """Message to be returned if user sends an unknown command.
  252. If instance message is not set, class message is returned.
  253. """
  254. if self._unknown_command_message:
  255. return self._unknown_command_message
  256. return self.__class__._unknown_command_message
  257. async def message_router(self, update):
  258. """Route Telegram `message` update to appropriate message handler."""
  259. for key, value in update.items():
  260. if key in self.message_handlers:
  261. return await self.message_handlers[key](update)
  262. logging.error(
  263. f"The following message update was received: {update}\n"
  264. "However, this message type is unknown."
  265. )
  266. async def edited_message_handler(self, update):
  267. """Handle Telegram `edited_message` update."""
  268. logging.info(
  269. f"The following update was received: {update}\n"
  270. "However, this edited_message handler does nothing yet."
  271. )
  272. return
  273. async def channel_post_handler(self, update):
  274. """Handle Telegram `channel_post` update."""
  275. logging.info(
  276. f"The following update was received: {update}\n"
  277. "However, this channel_post handler does nothing yet."
  278. )
  279. return
  280. async def edited_channel_post_handler(self, update):
  281. """Handle Telegram `edited_channel_post` update."""
  282. logging.info(
  283. f"The following update was received: {update}\n"
  284. "However, this edited_channel_post handler does nothing yet."
  285. )
  286. return
  287. async def inline_query_handler(self, update):
  288. """Handle Telegram `inline_query` update."""
  289. logging.info(
  290. f"The following update was received: {update}\n"
  291. "However, this inline_query handler does nothing yet."
  292. )
  293. return
  294. async def chosen_inline_result_handler(self, update):
  295. """Handle Telegram `chosen_inline_result` update."""
  296. logging.info(
  297. f"The following update was received: {update}\n"
  298. "However, this chosen_inline_result handler does nothing yet."
  299. )
  300. return
  301. async def callback_query_handler(self, update):
  302. """Handle Telegram `callback_query` update."""
  303. logging.info(
  304. f"The following update was received: {update}\n"
  305. "However, this callback_query handler does nothing yet."
  306. )
  307. return
  308. async def shipping_query_handler(self, update):
  309. """Handle Telegram `shipping_query` update."""
  310. logging.info(
  311. f"The following update was received: {update}\n"
  312. "However, this shipping_query handler does nothing yet."
  313. )
  314. return
  315. async def pre_checkout_query_handler(self, update):
  316. """Handle Telegram `pre_checkout_query` update."""
  317. logging.info(
  318. f"The following update was received: {update}\n"
  319. "However, this pre_checkout_query handler does nothing yet."
  320. )
  321. return
  322. async def poll_handler(self, update):
  323. """Handle Telegram `poll` update."""
  324. logging.info(
  325. f"The following update was received: {update}\n"
  326. "However, this poll handler does nothing yet."
  327. )
  328. return
  329. async def text_message_handler(self, update):
  330. """Handle `text` message update."""
  331. replier, reply = None, None
  332. text = update['text'].lower()
  333. user_id = update['from']['id'] if 'from' in update else None
  334. if user_id in self.individual_text_message_handlers:
  335. replier = self.individual_text_message_handlers[user_id]
  336. del self.individual_text_message_handlers[user_id]
  337. elif text.startswith('/'): # Handle commands
  338. # A command must always start with the ‘/’ symbol and may not be
  339. # longer than 32 characters.
  340. # Commands can use latin letters, numbers and underscores.
  341. command = re.search(
  342. r"([A-z_1-9]){1,32}", # Command pattern (without leading `/`)
  343. text
  344. ).group(0) # Get the first group of characters matching pattern
  345. if command in self.commands:
  346. replier = self.commands[command]['function']
  347. elif 'chat' in update and update['chat']['id'] > 0:
  348. reply = self.unknown_command_message
  349. else: # Handle command aliases and text parsers
  350. # Aliases are case insensitive: text and alias are both .lower()
  351. for alias, function in self.command_aliases.items():
  352. if text.startswith(alias.lower()):
  353. replier = function
  354. break
  355. # Text message update parsers
  356. for check_function, parser in self.text_message_parsers.items():
  357. if (
  358. parser['argument'] == 'text'
  359. and check_function(text)
  360. ) or (
  361. parser['argument'] == 'update'
  362. and check_function(update)
  363. ):
  364. replier = parser['function']
  365. break
  366. if replier:
  367. if asyncio.iscoroutinefunction(replier):
  368. reply = await replier(update)
  369. else:
  370. reply = replier(update)
  371. if reply:
  372. if type(reply) is str:
  373. reply = dict(text=reply)
  374. try:
  375. if 'text' in reply:
  376. return await self.send_message(update=update, **reply)
  377. if 'photo' in reply:
  378. return await self.send_photo(update=update, **reply)
  379. except Exception as e:
  380. logging.error(
  381. f"Failed to handle text message:\n{e}",
  382. exc_info=True
  383. )
  384. return
  385. async def audio_message_handler(self, update):
  386. """Handle `audio` message update."""
  387. logging.info(
  388. "A audio message update was received, "
  389. "but this handler does nothing yet."
  390. )
  391. async def document_message_handler(self, update):
  392. """Handle `document` message update."""
  393. logging.info(
  394. "A document message update was received, "
  395. "but this handler does nothing yet."
  396. )
  397. async def animation_message_handler(self, update):
  398. """Handle `animation` message update."""
  399. logging.info(
  400. "A animation message update was received, "
  401. "but this handler does nothing yet."
  402. )
  403. async def game_message_handler(self, update):
  404. """Handle `game` message update."""
  405. logging.info(
  406. "A game message update was received, "
  407. "but this handler does nothing yet."
  408. )
  409. async def photo_message_handler(self, update):
  410. """Handle `photo` message update."""
  411. logging.info(
  412. "A photo message update was received, "
  413. "but this handler does nothing yet."
  414. )
  415. async def sticker_message_handler(self, update):
  416. """Handle `sticker` message update."""
  417. logging.info(
  418. "A sticker message update was received, "
  419. "but this handler does nothing yet."
  420. )
  421. async def video_message_handler(self, update):
  422. """Handle `video` message update."""
  423. logging.info(
  424. "A video message update was received, "
  425. "but this handler does nothing yet."
  426. )
  427. async def voice_message_handler(self, update):
  428. """Handle `voice` message update."""
  429. logging.info(
  430. "A voice message update was received, "
  431. "but this handler does nothing yet."
  432. )
  433. async def video_note_message_handler(self, update):
  434. """Handle `video_note` message update."""
  435. logging.info(
  436. "A video_note message update was received, "
  437. "but this handler does nothing yet."
  438. )
  439. async def contact_message_handler(self, update):
  440. """Handle `contact` message update."""
  441. logging.info(
  442. "A contact message update was received, "
  443. "but this handler does nothing yet."
  444. )
  445. async def location_message_handler(self, update):
  446. """Handle `location` message update."""
  447. logging.info(
  448. "A location message update was received, "
  449. "but this handler does nothing yet."
  450. )
  451. async def venue_message_handler(self, update):
  452. """Handle `venue` message update."""
  453. logging.info(
  454. "A venue message update was received, "
  455. "but this handler does nothing yet."
  456. )
  457. async def poll_message_handler(self, update):
  458. """Handle `poll` message update."""
  459. logging.info(
  460. "A poll message update was received, "
  461. "but this handler does nothing yet."
  462. )
  463. async def new_chat_members_message_handler(self, update):
  464. """Handle `new_chat_members` message update."""
  465. logging.info(
  466. "A new_chat_members message update was received, "
  467. "but this handler does nothing yet."
  468. )
  469. async def left_chat_member_message_handler(self, update):
  470. """Handle `left_chat_member` message update."""
  471. logging.info(
  472. "A left_chat_member message update was received, "
  473. "but this handler does nothing yet."
  474. )
  475. async def new_chat_title_message_handler(self, update):
  476. """Handle `new_chat_title` message update."""
  477. logging.info(
  478. "A new_chat_title message update was received, "
  479. "but this handler does nothing yet."
  480. )
  481. async def new_chat_photo_message_handler(self, update):
  482. """Handle `new_chat_photo` message update."""
  483. logging.info(
  484. "A new_chat_photo message update was received, "
  485. "but this handler does nothing yet."
  486. )
  487. async def delete_chat_photo_message_handler(self, update):
  488. """Handle `delete_chat_photo` message update."""
  489. logging.info(
  490. "A delete_chat_photo message update was received, "
  491. "but this handler does nothing yet."
  492. )
  493. async def group_chat_created_message_handler(self, update):
  494. """Handle `group_chat_created` message update."""
  495. logging.info(
  496. "A group_chat_created message update was received, "
  497. "but this handler does nothing yet."
  498. )
  499. async def supergroup_chat_created_message_handler(self, update):
  500. """Handle `supergroup_chat_created` message update."""
  501. logging.info(
  502. "A supergroup_chat_created message update was received, "
  503. "but this handler does nothing yet."
  504. )
  505. async def channel_chat_created_message_handler(self, update):
  506. """Handle `channel_chat_created` message update."""
  507. logging.info(
  508. "A channel_chat_created message update was received, "
  509. "but this handler does nothing yet."
  510. )
  511. async def migrate_to_chat_id_message_handler(self, update):
  512. """Handle `migrate_to_chat_id` message update."""
  513. logging.info(
  514. "A migrate_to_chat_id message update was received, "
  515. "but this handler does nothing yet."
  516. )
  517. async def migrate_from_chat_id_message_handler(self, update):
  518. """Handle `migrate_from_chat_id` message update."""
  519. logging.info(
  520. "A migrate_from_chat_id message update was received, "
  521. "but this handler does nothing yet."
  522. )
  523. async def pinned_message_message_handler(self, update):
  524. """Handle `pinned_message` message update."""
  525. logging.info(
  526. "A pinned_message message update was received, "
  527. "but this handler does nothing yet."
  528. )
  529. async def invoice_message_handler(self, update):
  530. """Handle `invoice` message update."""
  531. logging.info(
  532. "A invoice message update was received, "
  533. "but this handler does nothing yet."
  534. )
  535. async def successful_payment_message_handler(self, update):
  536. """Handle `successful_payment` message update."""
  537. logging.info(
  538. "A successful_payment message update was received, "
  539. "but this handler does nothing yet."
  540. )
  541. async def connected_website_message_handler(self, update):
  542. """Handle `connected_website` message update."""
  543. logging.info(
  544. "A connected_website message update was received, "
  545. "but this handler does nothing yet."
  546. )
  547. async def passport_data_message_handler(self, update):
  548. """Handle `passport_data` message update."""
  549. logging.info(
  550. "A passport_data message update was received, "
  551. "but this handler does nothing yet."
  552. )
  553. @staticmethod
  554. def split_message_text(text, limit=None, parse_mode='HTML'):
  555. r"""Split text if it hits telegram limits for text messages.
  556. Split at `\n` if possible.
  557. Add a `[...]` at the end and beginning of split messages,
  558. with proper code markdown.
  559. """
  560. if parse_mode == 'HTML':
  561. text = escape_html_chars(text)
  562. tags = (
  563. ('`', '`')
  564. if parse_mode == 'Markdown'
  565. else ('<code>', '</code>')
  566. if parse_mode.lower() == 'html'
  567. else ('', '')
  568. )
  569. if limit is None:
  570. limit = Bot.TELEGRAM_MESSAGES_MAX_LEN - 100
  571. # Example text: "lines\nin\nreversed\order"
  572. text = text.split("\n")[::-1] # ['order', 'reversed', 'in', 'lines']
  573. text_part_number = 0
  574. while len(text) > 0:
  575. temp = []
  576. text_part_number += 1
  577. while (
  578. len(text) > 0
  579. and len(
  580. "\n".join(temp + [text[-1]])
  581. ) < limit
  582. ):
  583. # Append lines of `text` in order (`.pop` returns the last
  584. # line in text) until the addition of the next line would hit
  585. # the `limit`.
  586. temp.append(text.pop())
  587. # If graceful split failed (last line was longer than limit)
  588. if len(temp) == 0:
  589. # Force split last line
  590. temp.append(text[-1][:limit])
  591. text[-1] = text[-1][limit:]
  592. text_chunk = "\n".join(temp) # Re-join this group of lines
  593. prefix, suffix = '', ''
  594. is_last = len(text) > 0
  595. if text_part_number > 1:
  596. prefix = f"{tags[0]}[...]{tags[1]}\n"
  597. if is_last:
  598. suffix = f"\n{tags[0]}[...]{tags[1]}"
  599. yield (prefix + text_chunk + suffix), is_last
  600. return
  601. async def send_message(self, chat_id=None, text=None,
  602. parse_mode='HTML',
  603. disable_web_page_preview=None,
  604. disable_notification=None,
  605. reply_to_message_id=None,
  606. reply_markup=None,
  607. update=dict(),
  608. reply_to_update=False,
  609. send_default_keyboard=True):
  610. """Send text via message(s).
  611. This method wraps lower-level `TelegramBot.sendMessage` method.
  612. Pass an `update` to extract `chat_id` and `message_id` from it.
  613. Set `reply_to_update` = True to reply to `update['message_id']`.
  614. Set `send_default_keyboard` = False to avoid sending default keyboard
  615. as reply_markup (only those messages can be edited, which were
  616. sent with no reply markup or with an inline keyboard).
  617. """
  618. if 'message' in update:
  619. update = update['message']
  620. if chat_id is None and 'chat' in update:
  621. chat_id = self.get_chat_id(update)
  622. if reply_to_update and 'message_id' in update:
  623. reply_to_message_id = update['message_id']
  624. if (
  625. send_default_keyboard
  626. and reply_markup is None
  627. and type(chat_id) is int
  628. and chat_id > 0
  629. and text != self.authorization_denied_message
  630. ):
  631. reply_markup = self.default_keyboard
  632. if not text:
  633. return
  634. parse_mode = str(parse_mode)
  635. text_chunks = self.split_message_text(
  636. text=text,
  637. limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 100,
  638. parse_mode=parse_mode
  639. )
  640. for text_chunk, is_last in text_chunks:
  641. _reply_markup = (reply_markup if is_last else None)
  642. sent_message_update = await self.sendMessage(
  643. chat_id=chat_id,
  644. text=text_chunk,
  645. parse_mode=parse_mode,
  646. disable_web_page_preview=disable_web_page_preview,
  647. disable_notification=disable_notification,
  648. reply_to_message_id=reply_to_message_id,
  649. reply_markup=_reply_markup
  650. )
  651. return sent_message_update
  652. async def send_photo(self, chat_id=None, photo=None,
  653. caption=None,
  654. parse_mode=None,
  655. disable_notification=None,
  656. reply_to_message_id=None,
  657. reply_markup=None,
  658. update=dict(),
  659. reply_to_update=False,
  660. send_default_keyboard=True,
  661. use_stored_file_id=True,
  662. is_second_try=False):
  663. """Send photos.
  664. This method wraps lower-level `TelegramBot.sendPhoto` method.
  665. Pass an `update` to extract `chat_id` and `message_id` from it.
  666. Set `reply_to_update` = True to reply to `update['message_id']`.
  667. Set `send_default_keyboard` = False to avoid sending default keyboard
  668. as reply_markup (only those messages can be edited, which were
  669. sent with no reply markup or with an inline keyboard).
  670. If photo was already sent by this bot and `use_stored_file_id` is set
  671. to True, use file_id (it is faster and recommended).
  672. """
  673. if 'message' in update:
  674. update = update['message']
  675. if chat_id is None and 'chat' in update:
  676. chat_id = self.get_chat_id(update)
  677. if reply_to_update and 'message_id' in update:
  678. reply_to_message_id = update['message_id']
  679. if (
  680. send_default_keyboard
  681. and reply_markup is None
  682. and type(chat_id) is int
  683. and chat_id > 0
  684. and caption != self.authorization_denied_message
  685. ):
  686. reply_markup = self.default_keyboard
  687. if type(photo) is str:
  688. photo_path = photo
  689. with self.db as db:
  690. already_sent = db['sent_pictures'].find_one(
  691. path=photo_path,
  692. errors=False
  693. )
  694. if already_sent and use_stored_file_id:
  695. photo = already_sent['file_id']
  696. already_sent = True
  697. else:
  698. already_sent = False
  699. if not any(
  700. [
  701. photo.startswith(url_starter)
  702. for url_starter in ('http', 'www',)
  703. ]
  704. ): # If `photo` is not a url but a local file path
  705. try:
  706. with io.BytesIO() as buffered_picture:
  707. with open(
  708. os.path.join(self.path, photo_path),
  709. 'rb' # Read bytes
  710. ) as photo_file:
  711. buffered_picture.write(photo_file.read())
  712. photo = buffered_picture.getvalue()
  713. except FileNotFoundError:
  714. photo = None
  715. else:
  716. use_stored_file_id = False
  717. if photo is None:
  718. logging.error("Photo is None, `send_photo` returning...")
  719. return
  720. sent_update = None
  721. try:
  722. sent_update = await self.sendPhoto(
  723. chat_id=chat_id,
  724. photo=photo,
  725. caption=caption,
  726. parse_mode=parse_mode,
  727. disable_notification=disable_notification,
  728. reply_to_message_id=reply_to_message_id,
  729. reply_markup=reply_markup
  730. )
  731. if isinstance(sent_update, Exception):
  732. raise Exception("sendPhoto API call failed!")
  733. except Exception as e:
  734. logging.error(f"Error sending photo\n{e}")
  735. if already_sent:
  736. with self.db as db:
  737. db['sent_pictures'].update(
  738. dict(
  739. path=photo_path,
  740. errors=True
  741. ),
  742. ['path']
  743. )
  744. if not is_second_try:
  745. logging.info("Trying again (only once)...")
  746. sent_update = await self.send_photo(
  747. chat_id=chat_id,
  748. photo=photo,
  749. caption=caption,
  750. parse_mode=parse_mode,
  751. disable_notification=disable_notification,
  752. reply_to_message_id=reply_to_message_id,
  753. reply_markup=reply_markup,
  754. update=update,
  755. reply_to_update=reply_to_update,
  756. send_default_keyboard=send_default_keyboard,
  757. use_stored_file_id=use_stored_file_id,
  758. is_second_try=True
  759. )
  760. if (
  761. type(sent_update) is dict
  762. and 'photo' in sent_update
  763. and len(sent_update['photo']) > 0
  764. and 'file_id' in sent_update['photo'][0]
  765. and (not already_sent)
  766. and use_stored_file_id
  767. ):
  768. with self.db as db:
  769. db['sent_pictures'].insert(
  770. dict(
  771. path=photo_path,
  772. file_id=sent_update['photo'][0]['file_id'],
  773. errors=False
  774. )
  775. )
  776. return sent_update
  777. async def answer_inline_query(self,
  778. inline_query_id=None,
  779. results=[],
  780. cache_time=None,
  781. is_personal=None,
  782. next_offset=None,
  783. switch_pm_text=None,
  784. switch_pm_parameter=None):
  785. """Answer inline queries.
  786. This method wraps lower-level `answerInlineQuery` method.
  787. """
  788. # If results is a string, cast to proper type
  789. return await self.answerInlineQuery(
  790. inline_query_id=inline_query_id,
  791. results=results,
  792. cache_time=cache_time,
  793. is_personal=is_personal,
  794. next_offset=next_offset,
  795. switch_pm_text=switch_pm_text,
  796. switch_pm_parameter=switch_pm_parameter,
  797. )
  798. @classmethod
  799. def set_class_maintenance_message(cls, maintenance_message):
  800. """Set class maintenance message.
  801. It will be returned if bot is under maintenance, unless and instance
  802. `_maintenance_message` is set.
  803. """
  804. cls._maintenance_message = maintenance_message
  805. def set_maintenance_message(self, maintenance_message):
  806. """Set instance maintenance message.
  807. It will be returned if bot is under maintenance.
  808. If instance message is None, default class message is used.
  809. """
  810. self._maintenance_message = maintenance_message
  811. def change_maintenance_status(self, maintenance_message=None, status=None):
  812. """Put the bot under maintenance or end it.
  813. While in maintenance, bot will reply to users with maintenance_message
  814. with a few exceptions.
  815. If status is not set, it is by default the opposite of the current one.
  816. Optionally, `maintenance_message` may be set.
  817. """
  818. if status is None:
  819. status = not self.under_maintenance
  820. assert type(status) is bool, "status must be a boolean value!"
  821. self._under_maintenance = status
  822. if maintenance_message:
  823. self.set_maintenance_message(maintenance_message)
  824. return self._under_maintenance # Return new status
  825. def is_allowed_during_maintenance(self, update):
  826. """Return True if update is allowed during maintenance.
  827. An update is allowed if any of the criteria in
  828. `self.allowed_during_maintenance` returns True called on it.
  829. """
  830. for criterion in self.allowed_during_maintenance:
  831. if criterion(update):
  832. return True
  833. return False
  834. def allow_during_maintenance(self, criterion):
  835. """Add a criterion to allow certain updates during maintenance.
  836. `criterion` must be a function taking a Telegram `update` dictionary
  837. and returning a boolean.
  838. ```# Example of criterion
  839. def allow_text_messages(update):
  840. if 'message' in update and 'text' in update['message']:
  841. return True
  842. return False
  843. ```
  844. """
  845. self._allowed_during_maintenance.append(criterion)
  846. async def handle_update_during_maintenance(self, update):
  847. """Handle an update while bot is under maintenance.
  848. Handle all types of updates.
  849. """
  850. if (
  851. 'message' in update
  852. and 'chat' in update['message']
  853. and update['message']['chat']['id'] > 0
  854. ):
  855. return await self.send_message(
  856. text=self.maintenance_message,
  857. update=update['message'],
  858. reply_to_update=True
  859. )
  860. elif 'callback_query' in update:
  861. pass
  862. elif 'inline_query' in update:
  863. await self.answer_inline_query(
  864. update['inline_query']['id'],
  865. self.maintenance_message,
  866. cache_time=30,
  867. is_personal=False,
  868. )
  869. return
  870. @classmethod
  871. def set_class_authorization_denied_message(csl, message):
  872. """Set class authorization denied message.
  873. It will be returned if user is unauthorized to make a request.
  874. """
  875. csl._authorization_denied_message = message
  876. def set_authorization_denied_message(self, message):
  877. """Set instance authorization denied message.
  878. If instance message is None, default class message is used.
  879. """
  880. self._authorization_denied_message = message
  881. def set_authorization_function(self, authorization_function):
  882. """Set a custom authorization_function.
  883. It should evaluate True if user is authorized to perform a specific
  884. action and False otherwise.
  885. It should take update and role and return a Boolean.
  886. Default authorization_function always evaluates True.
  887. """
  888. self.authorization_function = authorization_function
  889. @classmethod
  890. def set_class_unknown_command_message(cls, unknown_command_message):
  891. """Set class unknown command message.
  892. It will be returned if user sends an unknown command in private chat.
  893. """
  894. cls._unknown_command_message = unknown_command_message
  895. def set_unknown_command_message(self, unknown_command_message):
  896. """Set instance unknown command message.
  897. It will be returned if user sends an unknown command in private chat.
  898. If instance message is None, default class message is used.
  899. """
  900. self._unknown_command_message = unknown_command_message
  901. def set_chat_id_getter(self, getter):
  902. """Set chat_id getter.
  903. It must be a function that takes an update and returns the proper
  904. chat_id.
  905. """
  906. assert callable(getter), "Chat id getter must be a function!"
  907. self.get_chat_id = getter
  908. @staticmethod
  909. def get_identifier_from_update_or_user_id(user_id=None, update=None):
  910. """Get telegram id of user given an update.
  911. Result itself may be passed as either parameter (for backward
  912. compatibility).
  913. """
  914. identifier = user_id or update
  915. assert identifier is not None, (
  916. "Provide a user_id or update object to get a user identifier."
  917. )
  918. if isinstance(identifier, dict) and 'from' in identifier:
  919. identifier = identifier['from']['id']
  920. assert type(identifier) is int, (
  921. "Unable to find a user identifier."
  922. )
  923. return identifier
  924. def set_individual_text_message_handler(self, handler,
  925. update=None, user_id=None):
  926. """Set a custom text message handler for the user.
  927. Any text message update from the user will be handled by this custom
  928. handler instead of default handlers for commands, aliases and text.
  929. Custom handlers last one single use, but they can call this method and
  930. set themselves as next custom text message handler.
  931. """
  932. identifier = self.get_identifier_from_update_or_user_id(
  933. user_id=user_id,
  934. update=update
  935. )
  936. assert callable(handler), (f"Handler `{handler.name}` is not "
  937. "callable. Custom text message handler "
  938. "could not be set.")
  939. self.individual_text_message_handlers[identifier] = handler
  940. return
  941. def remove_individual_text_message_handler(self,
  942. update=None, user_id=None):
  943. """Remove a custom text message handler for the user.
  944. Any text message update from the user will be handled by default
  945. handlers for commands, aliases and text.
  946. """
  947. identifier = self.get_identifier_from_update_or_user_id(
  948. user_id=user_id,
  949. update=update
  950. )
  951. if identifier in self.individual_text_message_handlers:
  952. del self.individual_text_message_handlers[identifier]
  953. return
  954. def set_default_keyboard(self, keyboard='set_default'):
  955. """Set a default keyboard for the bot.
  956. If a keyboard is not passed as argument, a default one is generated,
  957. based on aliases of commands.
  958. """
  959. if keyboard == 'set_default':
  960. buttons = [
  961. dict(
  962. text=x
  963. )
  964. for x in self.default_reply_keyboard_elements
  965. ]
  966. if len(buttons) == 0:
  967. self._default_keyboard = None
  968. else:
  969. self._default_keyboard = dict(
  970. keyboard=make_lines_of_buttons(
  971. buttons,
  972. (2 if len(buttons) < 4 else 3) # Row length
  973. ),
  974. resize_keyboard=True
  975. )
  976. else:
  977. self._default_keyboard = keyboard
  978. return
  979. async def webhook_feeder(self, request):
  980. """Handle incoming HTTP `request`s.
  981. Get data, feed webhook and return and OK message.
  982. """
  983. update = await request.json()
  984. asyncio.ensure_future(
  985. self.route_update(update)
  986. )
  987. return web.Response(
  988. body='OK'.encode('utf-8')
  989. )
  990. async def get_me(self):
  991. """Get bot information.
  992. Restart bots if bot can't be got.
  993. """
  994. try:
  995. me = await self.getMe()
  996. if isinstance(me, Exception):
  997. raise me
  998. elif me is None:
  999. raise Exception('getMe returned None')
  1000. self._name = me["username"]
  1001. self._telegram_id = me['id']
  1002. except Exception as e:
  1003. logging.error(
  1004. f"API getMe method failed, information about this bot could "
  1005. f"not be retrieved. Restarting in 5 minutes...\n\n"
  1006. f"Error information:\n{e}"
  1007. )
  1008. await asyncio.sleep(5*60)
  1009. self.__class__.stop(
  1010. 65,
  1011. f"Information aformation about this bot could "
  1012. f"not be retrieved. Restarting..."
  1013. )
  1014. def setup(self):
  1015. """Make bot ask for updates and handle responses."""
  1016. self.set_default_keyboard()
  1017. if not self.webhook_url:
  1018. asyncio.ensure_future(self.get_updates())
  1019. else:
  1020. asyncio.ensure_future(self.set_webhook())
  1021. self.__class__.app.router.add_route(
  1022. 'POST', self.webhook_local_address, self.webhook_feeder
  1023. )
  1024. async def close_sessions(self):
  1025. """Close open sessions."""
  1026. for session_name, session in self.sessions.items():
  1027. await session.close()
  1028. async def set_webhook(self, url=None, certificate=None,
  1029. max_connections=None, allowed_updates=None):
  1030. """Set a webhook if token is valid."""
  1031. # Return if token is invalid
  1032. await self.get_me()
  1033. if self.name is None:
  1034. return
  1035. webhook_was_set = await self.setWebhook(
  1036. url=url, certificate=certificate, max_connections=max_connections,
  1037. allowed_updates=allowed_updates
  1038. ) # `setWebhook` API method returns `True` on success
  1039. webhook_information = await self.getWebhookInfo()
  1040. webhook_information['url'] = webhook_information['url'].replace(
  1041. self.token, "<BOT_TOKEN>"
  1042. ).replace(
  1043. self.session_token, "<SESSION_TOKEN>"
  1044. )
  1045. if webhook_was_set:
  1046. logging.info(
  1047. f"Webhook was set correctly.\n"
  1048. f"Webhook information: {webhook_information}"
  1049. )
  1050. else:
  1051. logging.error(
  1052. f"Failed to set webhook!\n"
  1053. f"Webhook information: {webhook_information}"
  1054. )
  1055. async def get_updates(self, timeout=30, limit=100, allowed_updates=None,
  1056. error_cooldown=10):
  1057. """Get updates using long polling.
  1058. timeout : int
  1059. Timeout set for Telegram servers. Make sure that connection timeout
  1060. is greater than `timeout`.
  1061. limit : int (1 - 100)
  1062. Max number of updates to be retrieved.
  1063. allowed_updates : List(str)
  1064. List of update types to be retrieved.
  1065. Empty list to allow all updates.
  1066. None to fallback to class default.
  1067. """
  1068. # Return if token is invalid
  1069. await self.get_me()
  1070. if self.name is None:
  1071. return
  1072. # Set custom list of allowed updates or fallback to class default list
  1073. if allowed_updates is None:
  1074. allowed_updates = self.allowed_updates
  1075. await self.deleteWebhook() # Remove eventually active webhook
  1076. update = None # Do not update offset if no update is received
  1077. while True:
  1078. updates = await self.getUpdates(
  1079. offset=self._offset,
  1080. timeout=timeout,
  1081. limit=limit,
  1082. allowed_updates=allowed_updates
  1083. )
  1084. if updates is None:
  1085. continue
  1086. elif isinstance(updates, TelegramError):
  1087. logging.error(
  1088. f"Waiting {error_cooldown} seconds before trying again..."
  1089. )
  1090. await asyncio.sleep(error_cooldown)
  1091. continue
  1092. for update in updates:
  1093. asyncio.ensure_future(self.route_update(update))
  1094. if update is not None:
  1095. self._offset = update['update_id'] + 1
  1096. async def route_update(self, update):
  1097. """Pass `update` to proper method.
  1098. Update objects have two keys:
  1099. - `update_id` (which is used as offset while retrieving new updates)
  1100. - One and only one of the following
  1101. `message`
  1102. `edited_message`
  1103. `channel_post`
  1104. `edited_channel_post`
  1105. `inline_query`
  1106. `chosen_inline_result`
  1107. `callback_query`
  1108. `shipping_query`
  1109. `pre_checkout_query`
  1110. `poll`
  1111. """
  1112. if (
  1113. self.under_maintenance
  1114. and not self.is_allowed_during_maintenance(update)
  1115. ):
  1116. return await self.handle_update_during_maintenance(update)
  1117. for key, value in update.items():
  1118. if key in self.routing_table:
  1119. return await self.routing_table[key](value)
  1120. logging.error(f"Unknown type of update.\n{update}")
  1121. @classmethod
  1122. async def start_app(cls):
  1123. """Start running `aiohttp.web.Application`.
  1124. It will route webhook-received updates and other custom paths.
  1125. """
  1126. assert cls.local_host is not None, "Invalid local host"
  1127. assert cls.port is not None, "Invalid port"
  1128. cls.runner = web.AppRunner(cls.app)
  1129. await cls.runner.setup()
  1130. cls.server = web.TCPSite(cls.runner, cls.local_host, cls.port)
  1131. await cls.server.start()
  1132. logging.info(f"App running at http://{cls.local_host}:{cls.port}")
  1133. @classmethod
  1134. async def stop_app(cls):
  1135. """Close bot sessions and cleanup."""
  1136. for bot in cls.bots:
  1137. await bot.close_sessions()
  1138. await cls.runner.cleanup()
  1139. @classmethod
  1140. def stop(cls, message, final_state=0):
  1141. """Log a final `message`, stop loop and set exiting `code`.
  1142. All bots and the web app will be terminated gracefully.
  1143. The final state may be retrieved to get information about what stopped
  1144. the bots.
  1145. """
  1146. logging.info(message)
  1147. cls.final_state = final_state
  1148. cls.loop.stop()
  1149. return
  1150. @classmethod
  1151. def run(cls, local_host=None, port=None):
  1152. """Run aiohttp web app and all Bot instances.
  1153. Each bot will receive updates via long polling or webhook according to
  1154. its initialization parameters.
  1155. A single aiohttp.web.Application instance will be run (cls.app) on
  1156. local_host:port and it may serve custom-defined routes as well.
  1157. """
  1158. if local_host is not None:
  1159. cls.local_host = local_host
  1160. if port is not None:
  1161. cls.port = port
  1162. for bot in cls.bots:
  1163. bot.setup()
  1164. asyncio.ensure_future(cls.start_app())
  1165. try:
  1166. cls.loop.run_forever()
  1167. except KeyboardInterrupt:
  1168. logging.info("Stopped by KeyboardInterrupt")
  1169. except Exception as e:
  1170. logging.error(f"{e}", exc_info=True)
  1171. finally:
  1172. cls.loop.run_until_complete(cls.stop_app())
  1173. return cls.final_state