Queer European MD passionate about IT

bot.py 72 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910
  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. Usage
  5. ```
  6. import sys
  7. from davtelepot.bot import Bot
  8. from data.passwords import my_token, my_other_token
  9. long_polling_bot = Bot(token=my_token, database_url='my_db')
  10. webhook_bot = Bot(token=my_other_token, hostname='example.com',
  11. certificate='path/to/certificate.pem',
  12. database_url='my_other_db')
  13. @long_polling_bot.command('/foo')
  14. async def foo_command(bot, update, user_record):
  15. return "Bar!"
  16. @webhook_bot.command('/bar')
  17. async def bar_command(bot, update, user_record):
  18. return "Foo!"
  19. exit_state = Bot.run(
  20. local_host='127.0.0.5',
  21. port=8552
  22. )
  23. sys.exit(exit_state)
  24. ```
  25. """
  26. # Standard library modules
  27. import asyncio
  28. from collections import OrderedDict
  29. import io
  30. import inspect
  31. import logging
  32. import os
  33. import re
  34. # Third party modules
  35. from aiohttp import web
  36. # Project modules
  37. from davtelepot.api import TelegramBot, TelegramError
  38. from davtelepot.database import ObjectWithDatabase
  39. from davtelepot.utilities import (
  40. escape_html_chars, extract, get_secure_key, make_inline_query_answer,
  41. make_lines_of_buttons, remove_html_tags
  42. )
  43. # Do not log aiohttp `INFO` and `DEBUG` levels
  44. logging.getLogger('aiohttp').setLevel(logging.WARNING)
  45. class Bot(TelegramBot, ObjectWithDatabase):
  46. """Simple Bot object, providing methods corresponding to Telegram bot API.
  47. Multiple Bot() instances may be run together, along with a aiohttp web app.
  48. """
  49. bots = []
  50. _path = '.'
  51. runner = None
  52. local_host = 'localhost'
  53. port = 3000
  54. final_state = 0
  55. _maintenance_message = ("I am currently under maintenance!\n"
  56. "Please retry later...")
  57. _authorization_denied_message = None
  58. _unknown_command_message = None
  59. TELEGRAM_MESSAGES_MAX_LEN = 4096
  60. _default_inline_query_answer = [
  61. dict(
  62. type='article',
  63. id=0,
  64. title="I cannot answer this query, sorry",
  65. input_message_content=dict(
  66. message_text="I'm sorry "
  67. "but I could not find an answer for your query."
  68. )
  69. )
  70. ]
  71. def __init__(
  72. self, token, hostname='', certificate=None, max_connections=40,
  73. allowed_updates=[], database_url='bot.db'
  74. ):
  75. """Init a bot instance.
  76. token : str
  77. Telegram bot API token.
  78. hostname : str
  79. Domain (or public IP address) for webhooks.
  80. certificate : str
  81. Path to domain certificate.
  82. max_connections : int (1 - 100)
  83. Maximum number of HTTPS connections allowed.
  84. allowed_updates : List(str)
  85. Allowed update types (empty list to allow all).
  86. """
  87. # Append `self` to class list of instances
  88. self.__class__.bots.append(self)
  89. # Call superclasses constructors with proper arguments
  90. TelegramBot.__init__(self, token)
  91. ObjectWithDatabase.__init__(self, database_url=database_url)
  92. self._path = None
  93. self.preliminary_tasks = []
  94. self.final_tasks = []
  95. self._offset = 0
  96. self._hostname = hostname
  97. self._certificate = certificate
  98. self._max_connections = max_connections
  99. self._allowed_updates = allowed_updates
  100. self._session_token = get_secure_key(length=10)
  101. self._name = None
  102. self._telegram_id = None
  103. # The following routing table associates each type of Telegram `update`
  104. # with a Bot method to be invoked on it.
  105. self.routing_table = {
  106. 'message': self.message_router,
  107. 'edited_message': self.edited_message_handler,
  108. 'channel_post': self.channel_post_handler,
  109. 'edited_channel_post': self.edited_channel_post_handler,
  110. 'inline_query': self.inline_query_handler,
  111. 'chosen_inline_result': self.chosen_inline_result_handler,
  112. 'callback_query': self.callback_query_handler,
  113. 'shipping_query': self.shipping_query_handler,
  114. 'pre_checkout_query': self.pre_checkout_query_handler,
  115. 'poll': self.poll_handler,
  116. }
  117. # Different message update types need different handlers
  118. self.message_handlers = {
  119. 'text': self.text_message_handler,
  120. 'audio': self.audio_message_handler,
  121. 'document': self.document_message_handler,
  122. 'animation': self.animation_message_handler,
  123. 'game': self.game_message_handler,
  124. 'photo': self.photo_message_handler,
  125. 'sticker': self.sticker_message_handler,
  126. 'video': self.video_message_handler,
  127. 'voice': self.voice_message_handler,
  128. 'video_note': self.video_note_message_handler,
  129. 'contact': self.contact_message_handler,
  130. 'location': self.location_message_handler,
  131. 'venue': self.venue_message_handler,
  132. 'poll': self.poll_message_handler,
  133. 'new_chat_members': self.new_chat_members_message_handler,
  134. 'left_chat_member': self.left_chat_member_message_handler,
  135. 'new_chat_title': self.new_chat_title_message_handler,
  136. 'new_chat_photo': self.new_chat_photo_message_handler,
  137. 'delete_chat_photo': self.delete_chat_photo_message_handler,
  138. 'group_chat_created': self.group_chat_created_message_handler,
  139. 'supergroup_chat_created': (
  140. self.supergroup_chat_created_message_handler
  141. ),
  142. 'channel_chat_created': self.channel_chat_created_message_handler,
  143. 'migrate_to_chat_id': self.migrate_to_chat_id_message_handler,
  144. 'migrate_from_chat_id': self.migrate_from_chat_id_message_handler,
  145. 'pinned_message': self.pinned_message_message_handler,
  146. 'invoice': self.invoice_message_handler,
  147. 'successful_payment': self.successful_payment_message_handler,
  148. 'connected_website': self.connected_website_message_handler,
  149. 'passport_data': self.passport_data_message_handler
  150. }
  151. # Special text message handlers: individual, commands, aliases, parsers
  152. self.individual_text_message_handlers = dict()
  153. self.commands = OrderedDict()
  154. self.command_aliases = OrderedDict()
  155. self._unknown_command_message = None
  156. self.text_message_parsers = OrderedDict()
  157. # Callback query-related properties
  158. self.callback_handlers = OrderedDict()
  159. self._callback_data_separator = None
  160. # Inline query-related properties
  161. self.inline_query_handlers = OrderedDict()
  162. self._default_inline_query_answer = None
  163. self.chosen_inline_result_handlers = dict()
  164. # Maintenance properties
  165. self._under_maintenance = False
  166. self._allowed_during_maintenance = []
  167. self._maintenance_message = None
  168. # Default chat_id getter: same chat as update
  169. self.get_chat_id = lambda update: (
  170. update['message']['chat']['id']
  171. if 'message' in update and 'chat' in update['message']
  172. else update['chat']['id']
  173. if 'chat' in update
  174. else None
  175. )
  176. # Message to be returned if user is not allowed to call method
  177. self._authorization_denied_message = None
  178. # Default authorization function (always return True)
  179. self.authorization_function = (
  180. lambda update, user_record=None, authorization_level='user': True
  181. )
  182. self.default_reply_keyboard_elements = []
  183. self._default_keyboard = dict()
  184. return
  185. @property
  186. def path(self):
  187. """Path where files should be looked for.
  188. If no instance path is set, return class path.
  189. """
  190. return self._path or self.__class__._path
  191. @classmethod
  192. def set_class_path(csl, path):
  193. """Set class path attribute."""
  194. csl._path = path
  195. def set_path(self, path):
  196. """Set instance path attribute."""
  197. self._path = path
  198. @property
  199. def hostname(self):
  200. """Hostname for the webhook URL.
  201. It must be a public domain or IP address. Port may be specified.
  202. A custom webhook url, including bot token and a random token, will be
  203. generated for Telegram to post new updates.
  204. """
  205. return self._hostname
  206. @property
  207. def webhook_url(self):
  208. """URL where Telegram servers should post new updates.
  209. It must be a public domain name or IP address. Port may be specified.
  210. """
  211. if not self.hostname:
  212. return ''
  213. return (
  214. f"{self.hostname}/webhook/{self.token}_{self.session_token}/"
  215. )
  216. @property
  217. def webhook_local_address(self):
  218. """Local address where Telegram updates are routed by revers proxy."""
  219. return (
  220. f"/webhook/{self.token}_{self.session_token}/"
  221. )
  222. @property
  223. def certificate(self):
  224. """Public certificate for `webhook_url`.
  225. May be self-signed
  226. """
  227. return self._certificate
  228. @property
  229. def max_connections(self):
  230. """Maximum number of simultaneous HTTPS connections allowed.
  231. Telegram will open as many connections as possible to boost bot’s
  232. throughput, lower values limit the load on bot‘s server.
  233. """
  234. return self._max_connections
  235. @property
  236. def allowed_updates(self):
  237. """List of update types to be retrieved.
  238. Empty list to allow all updates.
  239. """
  240. return self._allowed_updates
  241. @property
  242. def name(self):
  243. """Bot name."""
  244. return self._name
  245. @property
  246. def telegram_id(self):
  247. """Telegram id of this bot."""
  248. return self._telegram_id
  249. @property
  250. def session_token(self):
  251. """Return a token generated with the current instantiation."""
  252. return self._session_token
  253. @property
  254. def offset(self):
  255. """Return last update id.
  256. Useful to ignore repeated updates and restore original update order.
  257. """
  258. return self._offset
  259. @property
  260. def under_maintenance(self):
  261. """Return True if bot is under maintenance.
  262. While under maintenance, bot will reply `self.maintenance_message` to
  263. any update, except those which `self.is_allowed_during_maintenance`
  264. returns True for.
  265. """
  266. return self._under_maintenance
  267. @property
  268. def allowed_during_maintenance(self):
  269. """Return the list of criteria to allow an update during maintenance.
  270. If any of this criteria returns True on an update, that update will be
  271. handled even during maintenance.
  272. """
  273. return self._allowed_during_maintenance
  274. @property
  275. def maintenance_message(self):
  276. """Message to be returned if bot is under maintenance.
  277. If instance message is not set, class message is returned.
  278. """
  279. if self._maintenance_message:
  280. return self._maintenance_message
  281. if self.__class__.maintenance_message:
  282. return self.__class__._maintenance_message
  283. return ("I am currently under maintenance!\n"
  284. "Please retry later...")
  285. @property
  286. def authorization_denied_message(self):
  287. """Return this text if user is unauthorized to make a request.
  288. If instance message is not set, class message is returned.
  289. """
  290. if self._authorization_denied_message:
  291. return self._authorization_denied_message
  292. return self.__class__._authorization_denied_message
  293. @property
  294. def default_keyboard(self):
  295. """Get the default keyboard.
  296. It is sent when reply_markup is left blank and chat is private.
  297. """
  298. return self._default_keyboard
  299. @property
  300. def unknown_command_message(self):
  301. """Message to be returned if user sends an unknown command.
  302. If instance message is not set, class message is returned.
  303. """
  304. if self._unknown_command_message:
  305. message = self._unknown_command_message
  306. else:
  307. message = self.__class__._unknown_command_message
  308. return message.format(bot=self)
  309. @property
  310. def callback_data_separator(self):
  311. """Separator between callback data elements.
  312. Example of callback_data: 'my_button_prefix:///1|4|test'
  313. Prefix: `my_button_prefix:///`
  314. Separator: `|` <--- this is returned
  315. Data: `['1', '4', 'test']`
  316. """
  317. return self._callback_data_separator
  318. def set_callback_data_separator(self, separator):
  319. """Set a callback_data separator.
  320. See property `callback_data_separator` for details.
  321. """
  322. assert type(separator) is str, "Separator must be a string!"
  323. self._callback_data_separator = separator
  324. @property
  325. def default_inline_query_answer(self):
  326. """Answer to be returned if inline query returned None.
  327. If instance default answer is not set, class one is returned.
  328. """
  329. if self._default_inline_query_answer:
  330. return self._default_inline_query_answer
  331. return self.__class__._default_inline_query_answer
  332. @classmethod
  333. def set_class_default_inline_query_answer(cls,
  334. default_inline_query_answer):
  335. """Set class default inline query answer.
  336. It will be returned if an inline query returned no answer.
  337. """
  338. cls._default_inline_query_answer = make_inline_query_answer(
  339. default_inline_query_answer
  340. )
  341. def set_default_inline_query_answer(self, default_inline_query_answer):
  342. """Set a custom default_inline_query_answer.
  343. It will be returned when no answer is found for an inline query.
  344. If instance answer is None, default class answer is used.
  345. """
  346. self._default_inline_query_answer = make_inline_query_answer(
  347. default_inline_query_answer
  348. )
  349. async def message_router(self, update, user_record):
  350. """Route Telegram `message` update to appropriate message handler."""
  351. for key, value in update.items():
  352. if key in self.message_handlers:
  353. return await self.message_handlers[key](update, user_record)
  354. logging.error(
  355. f"The following message update was received: {update}\n"
  356. "However, this message type is unknown."
  357. )
  358. async def edited_message_handler(self, update, user_record):
  359. """Handle Telegram `edited_message` update."""
  360. logging.info(
  361. f"The following update was received: {update}\n"
  362. "However, this edited_message handler does nothing yet."
  363. )
  364. return
  365. async def channel_post_handler(self, update, user_record):
  366. """Handle Telegram `channel_post` update."""
  367. logging.info(
  368. f"The following update was received: {update}\n"
  369. "However, this channel_post handler does nothing yet."
  370. )
  371. return
  372. async def edited_channel_post_handler(self, update, user_record):
  373. """Handle Telegram `edited_channel_post` update."""
  374. logging.info(
  375. f"The following update was received: {update}\n"
  376. "However, this edited_channel_post handler does nothing yet."
  377. )
  378. return
  379. async def inline_query_handler(self, update, user_record):
  380. """Handle Telegram `inline_query` update.
  381. Answer it with results or log errors.
  382. """
  383. query = update['query']
  384. results, switch_pm_text, switch_pm_parameter = None, None, None
  385. for condition, handler in self.inline_query_handlers.items():
  386. if condition(query):
  387. _handler = handler['handler']
  388. results = await _handler(bot=self, update=update,
  389. user_record=user_record)
  390. break
  391. if not results:
  392. results = self.default_inline_query_answer
  393. if type(results) is dict and 'answer' in results:
  394. if 'switch_pm_text' in results:
  395. switch_pm_text = results['switch_pm_text']
  396. if 'switch_pm_parameter' in results:
  397. switch_pm_parameter = results['switch_pm_parameter']
  398. results = results['answer']
  399. try:
  400. await self.answer_inline_query(
  401. update=update,
  402. results=results,
  403. cache_time=10,
  404. is_personal=True,
  405. switch_pm_text=switch_pm_text,
  406. switch_pm_parameter=switch_pm_parameter
  407. )
  408. except Exception as e:
  409. logging.info("Error answering inline query\n{}".format(e))
  410. return
  411. async def chosen_inline_result_handler(self, update, user_record):
  412. """Handle Telegram `chosen_inline_result` update."""
  413. user_id = update['from']['id']
  414. if user_id in self.chosen_inline_result_handlers:
  415. result_id = update['result_id']
  416. handlers = self.chosen_inline_result_handlers[user_id]
  417. if result_id in handlers:
  418. func = handlers[result_id]
  419. if asyncio.iscoroutinefunction(func):
  420. await func(update)
  421. else:
  422. func(update)
  423. return
  424. def set_inline_result_handler(self, user_id, result_id, func):
  425. """Associate a func to a result_id.
  426. When an inline result is chosen having that id, function will
  427. be passed the update as argument.
  428. """
  429. if type(user_id) is dict:
  430. user_id = user_id['from']['id']
  431. assert type(user_id) is int, "user_id must be int!"
  432. # Query result ids are parsed as str by telegram
  433. result_id = str(result_id)
  434. assert callable(func), "func must be a callable"
  435. if user_id not in self.chosen_inline_result_handlers:
  436. self.chosen_inline_result_handlers[user_id] = {}
  437. self.chosen_inline_result_handlers[user_id][result_id] = func
  438. return
  439. async def callback_query_handler(self, update, user_record):
  440. """Handle Telegram `callback_query` update.
  441. A callback query is sent when users press inline keyboard buttons.
  442. Bad clients may send malformed or deceiving callback queries:
  443. never put secrets in buttons and always check request validity!
  444. Get an `answer` from the callback handler associated to the query
  445. prefix and use it to edit the source message (or send new ones
  446. if text is longer than single message limit).
  447. Anyway, the query is answered, otherwise the client would hang and
  448. the bot would look like idle.
  449. """
  450. assert 'data' in update, "Malformed callback query lacking data field."
  451. answer = dict()
  452. data = update['data']
  453. for start_text, handler in self.callback_handlers.items():
  454. if data.startswith(start_text):
  455. _function = handler['handler']
  456. answer = await _function(
  457. bot=self,
  458. update=update,
  459. user_record=user_record
  460. )
  461. break
  462. if type(answer) is str:
  463. answer = dict(text=answer)
  464. assert type(answer) is dict, "Invalid callback query answer."
  465. if 'edit' in answer:
  466. message_identifier = self.get_message_identifier(update)
  467. edit = answer['edit']
  468. method = (
  469. self.edit_message_text if 'text' in edit
  470. else self.editMessageCaption if 'caption' in edit
  471. else self.editMessageReplyMarkup if 'reply_markup' in edit
  472. else (lambda *args, **kwargs: None)
  473. )
  474. try:
  475. await method(**message_identifier, **edit)
  476. except TelegramError as e:
  477. logging.info("Message was not modified:\n{}".format(e))
  478. try:
  479. return await self.answerCallbackQuery(
  480. callback_query_id=update['id'],
  481. **{
  482. key: (val[:180] if key == 'text' else val)
  483. for key, val in answer.items()
  484. if key in ('text', 'show_alert', 'cache_time')
  485. }
  486. )
  487. except TelegramError as e:
  488. logging.error(e)
  489. return
  490. async def shipping_query_handler(self, update, user_record):
  491. """Handle Telegram `shipping_query` update."""
  492. logging.info(
  493. f"The following update was received: {update}\n"
  494. "However, this shipping_query handler does nothing yet."
  495. )
  496. return
  497. async def pre_checkout_query_handler(self, update, user_record):
  498. """Handle Telegram `pre_checkout_query` update."""
  499. logging.info(
  500. f"The following update was received: {update}\n"
  501. "However, this pre_checkout_query handler does nothing yet."
  502. )
  503. return
  504. async def poll_handler(self, update, user_record):
  505. """Handle Telegram `poll` update."""
  506. logging.info(
  507. f"The following update was received: {update}\n"
  508. "However, this poll handler does nothing yet."
  509. )
  510. return
  511. async def text_message_handler(self, update, user_record):
  512. """Handle `text` message update."""
  513. replier, reply = None, None
  514. text = update['text'].lower()
  515. user_id = update['from']['id'] if 'from' in update else None
  516. if user_id in self.individual_text_message_handlers:
  517. replier = self.individual_text_message_handlers[user_id]
  518. del self.individual_text_message_handlers[user_id]
  519. elif text.startswith('/'): # Handle commands
  520. # A command must always start with the ‘/’ symbol and may not be
  521. # longer than 32 characters.
  522. # Commands can use latin letters, numbers and underscores.
  523. command = re.search(
  524. r"([A-z_1-9]){1,32}", # Command pattern (without leading `/`)
  525. text
  526. ).group(0) # Get the first group of characters matching pattern
  527. if command in self.commands:
  528. replier = self.commands[command]['handler']
  529. elif 'chat' in update and update['chat']['id'] > 0:
  530. reply = self.unknown_command_message
  531. else: # Handle command aliases and text parsers
  532. # Aliases are case insensitive: text and alias are both .lower()
  533. for alias, function in self.command_aliases.items():
  534. if text.startswith(alias.lower()):
  535. replier = function
  536. break
  537. # Text message update parsers
  538. for check_function, parser in self.text_message_parsers.items():
  539. if (
  540. parser['argument'] == 'text'
  541. and check_function(text)
  542. ) or (
  543. parser['argument'] == 'update'
  544. and check_function(update)
  545. ):
  546. replier = parser['handler']
  547. break
  548. if replier:
  549. reply = await replier(
  550. bot=self,
  551. update=update,
  552. user_record=user_record
  553. )
  554. if reply:
  555. if type(reply) is str:
  556. reply = dict(text=reply)
  557. try:
  558. if 'text' in reply:
  559. return await self.send_message(update=update, **reply)
  560. if 'photo' in reply:
  561. return await self.send_photo(update=update, **reply)
  562. except Exception as e:
  563. logging.error(
  564. f"Failed to handle text message:\n{e}",
  565. exc_info=True
  566. )
  567. return
  568. async def audio_message_handler(self, update, user_record):
  569. """Handle `audio` message update."""
  570. logging.info(
  571. "A audio message update was received, "
  572. "but this handler does nothing yet."
  573. )
  574. async def document_message_handler(self, update, user_record):
  575. """Handle `document` message update."""
  576. logging.info(
  577. "A document message update was received, "
  578. "but this handler does nothing yet."
  579. )
  580. async def animation_message_handler(self, update, user_record):
  581. """Handle `animation` message update."""
  582. logging.info(
  583. "A animation message update was received, "
  584. "but this handler does nothing yet."
  585. )
  586. async def game_message_handler(self, update, user_record):
  587. """Handle `game` message update."""
  588. logging.info(
  589. "A game message update was received, "
  590. "but this handler does nothing yet."
  591. )
  592. async def photo_message_handler(self, update, user_record):
  593. """Handle `photo` message update."""
  594. logging.info(
  595. "A photo message update was received, "
  596. "but this handler does nothing yet."
  597. )
  598. async def sticker_message_handler(self, update, user_record):
  599. """Handle `sticker` message update."""
  600. logging.info(
  601. "A sticker message update was received, "
  602. "but this handler does nothing yet."
  603. )
  604. async def video_message_handler(self, update, user_record):
  605. """Handle `video` message update."""
  606. logging.info(
  607. "A video message update was received, "
  608. "but this handler does nothing yet."
  609. )
  610. async def voice_message_handler(self, update, user_record):
  611. """Handle `voice` message update."""
  612. logging.info(
  613. "A voice message update was received, "
  614. "but this handler does nothing yet."
  615. )
  616. async def video_note_message_handler(self, update, user_record):
  617. """Handle `video_note` message update."""
  618. logging.info(
  619. "A video_note message update was received, "
  620. "but this handler does nothing yet."
  621. )
  622. async def contact_message_handler(self, update, user_record):
  623. """Handle `contact` message update."""
  624. logging.info(
  625. "A contact message update was received, "
  626. "but this handler does nothing yet."
  627. )
  628. async def location_message_handler(self, update, user_record):
  629. """Handle `location` message update."""
  630. logging.info(
  631. "A location message update was received, "
  632. "but this handler does nothing yet."
  633. )
  634. async def venue_message_handler(self, update, user_record):
  635. """Handle `venue` message update."""
  636. logging.info(
  637. "A venue message update was received, "
  638. "but this handler does nothing yet."
  639. )
  640. async def poll_message_handler(self, update, user_record):
  641. """Handle `poll` message update."""
  642. logging.info(
  643. "A poll message update was received, "
  644. "but this handler does nothing yet."
  645. )
  646. async def new_chat_members_message_handler(self, update, user_record):
  647. """Handle `new_chat_members` message update."""
  648. logging.info(
  649. "A new_chat_members message update was received, "
  650. "but this handler does nothing yet."
  651. )
  652. async def left_chat_member_message_handler(self, update, user_record):
  653. """Handle `left_chat_member` message update."""
  654. logging.info(
  655. "A left_chat_member message update was received, "
  656. "but this handler does nothing yet."
  657. )
  658. async def new_chat_title_message_handler(self, update, user_record):
  659. """Handle `new_chat_title` message update."""
  660. logging.info(
  661. "A new_chat_title message update was received, "
  662. "but this handler does nothing yet."
  663. )
  664. async def new_chat_photo_message_handler(self, update, user_record):
  665. """Handle `new_chat_photo` message update."""
  666. logging.info(
  667. "A new_chat_photo message update was received, "
  668. "but this handler does nothing yet."
  669. )
  670. async def delete_chat_photo_message_handler(self, update, user_record):
  671. """Handle `delete_chat_photo` message update."""
  672. logging.info(
  673. "A delete_chat_photo message update was received, "
  674. "but this handler does nothing yet."
  675. )
  676. async def group_chat_created_message_handler(self, update, user_record):
  677. """Handle `group_chat_created` message update."""
  678. logging.info(
  679. "A group_chat_created message update was received, "
  680. "but this handler does nothing yet."
  681. )
  682. async def supergroup_chat_created_message_handler(self, update,
  683. user_record):
  684. """Handle `supergroup_chat_created` message update."""
  685. logging.info(
  686. "A supergroup_chat_created message update was received, "
  687. "but this handler does nothing yet."
  688. )
  689. async def channel_chat_created_message_handler(self, update, user_record):
  690. """Handle `channel_chat_created` message update."""
  691. logging.info(
  692. "A channel_chat_created message update was received, "
  693. "but this handler does nothing yet."
  694. )
  695. async def migrate_to_chat_id_message_handler(self, update, user_record):
  696. """Handle `migrate_to_chat_id` message update."""
  697. logging.info(
  698. "A migrate_to_chat_id message update was received, "
  699. "but this handler does nothing yet."
  700. )
  701. async def migrate_from_chat_id_message_handler(self, update, user_record):
  702. """Handle `migrate_from_chat_id` message update."""
  703. logging.info(
  704. "A migrate_from_chat_id message update was received, "
  705. "but this handler does nothing yet."
  706. )
  707. async def pinned_message_message_handler(self, update, user_record):
  708. """Handle `pinned_message` message update."""
  709. logging.info(
  710. "A pinned_message message update was received, "
  711. "but this handler does nothing yet."
  712. )
  713. async def invoice_message_handler(self, update, user_record):
  714. """Handle `invoice` message update."""
  715. logging.info(
  716. "A invoice message update was received, "
  717. "but this handler does nothing yet."
  718. )
  719. async def successful_payment_message_handler(self, update, user_record):
  720. """Handle `successful_payment` message update."""
  721. logging.info(
  722. "A successful_payment message update was received, "
  723. "but this handler does nothing yet."
  724. )
  725. async def connected_website_message_handler(self, update, user_record):
  726. """Handle `connected_website` message update."""
  727. logging.info(
  728. "A connected_website message update was received, "
  729. "but this handler does nothing yet."
  730. )
  731. async def passport_data_message_handler(self, update, user_record):
  732. """Handle `passport_data` message update."""
  733. logging.info(
  734. "A passport_data message update was received, "
  735. "but this handler does nothing yet."
  736. )
  737. @staticmethod
  738. def split_message_text(text, limit=None, parse_mode='HTML'):
  739. r"""Split text if it hits telegram limits for text messages.
  740. Split at `\n` if possible.
  741. Add a `[...]` at the end and beginning of split messages,
  742. with proper code markdown.
  743. """
  744. if parse_mode == 'HTML':
  745. text = escape_html_chars(text)
  746. tags = (
  747. ('`', '`')
  748. if parse_mode == 'Markdown'
  749. else ('<code>', '</code>')
  750. if parse_mode.lower() == 'html'
  751. else ('', '')
  752. )
  753. if limit is None:
  754. limit = Bot.TELEGRAM_MESSAGES_MAX_LEN - 100
  755. # Example text: "lines\nin\nreversed\order"
  756. text = text.split("\n")[::-1] # ['order', 'reversed', 'in', 'lines']
  757. text_part_number = 0
  758. while len(text) > 0:
  759. temp = []
  760. text_part_number += 1
  761. while (
  762. len(text) > 0
  763. and len(
  764. "\n".join(temp + [text[-1]])
  765. ) < limit
  766. ):
  767. # Append lines of `text` in order (`.pop` returns the last
  768. # line in text) until the addition of the next line would hit
  769. # the `limit`.
  770. temp.append(text.pop())
  771. # If graceful split failed (last line was longer than limit)
  772. if len(temp) == 0:
  773. # Force split last line
  774. temp.append(text[-1][:limit])
  775. text[-1] = text[-1][limit:]
  776. text_chunk = "\n".join(temp) # Re-join this group of lines
  777. prefix, suffix = '', ''
  778. is_last = len(text) == 0
  779. if text_part_number > 1:
  780. prefix = f"{tags[0]}[...]{tags[1]}\n"
  781. if not is_last:
  782. suffix = f"\n{tags[0]}[...]{tags[1]}"
  783. yield (prefix + text_chunk + suffix), is_last
  784. return
  785. async def send_message(self, chat_id=None, text=None,
  786. parse_mode='HTML',
  787. disable_web_page_preview=None,
  788. disable_notification=None,
  789. reply_to_message_id=None,
  790. reply_markup=None,
  791. update=dict(),
  792. reply_to_update=False,
  793. send_default_keyboard=True):
  794. """Send text via message(s).
  795. This method wraps lower-level `TelegramBot.sendMessage` method.
  796. Pass an `update` to extract `chat_id` and `message_id` from it.
  797. Set `reply_to_update` = True to reply to `update['message_id']`.
  798. Set `send_default_keyboard` = False to avoid sending default keyboard
  799. as reply_markup (only those messages can be edited, which were
  800. sent with no reply markup or with an inline keyboard).
  801. """
  802. if 'message' in update:
  803. update = update['message']
  804. if chat_id is None and 'chat' in update:
  805. chat_id = self.get_chat_id(update)
  806. if reply_to_update and 'message_id' in update:
  807. reply_to_message_id = update['message_id']
  808. if (
  809. send_default_keyboard
  810. and reply_markup is None
  811. and type(chat_id) is int
  812. and chat_id > 0
  813. and text != self.authorization_denied_message
  814. ):
  815. reply_markup = self.default_keyboard
  816. if not text:
  817. return
  818. parse_mode = str(parse_mode)
  819. text_chunks = self.split_message_text(
  820. text=text,
  821. limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 100,
  822. parse_mode=parse_mode
  823. )
  824. for text_chunk, is_last in text_chunks:
  825. _reply_markup = (reply_markup if is_last else None)
  826. sent_message_update = await self.sendMessage(
  827. chat_id=chat_id,
  828. text=text_chunk,
  829. parse_mode=parse_mode,
  830. disable_web_page_preview=disable_web_page_preview,
  831. disable_notification=disable_notification,
  832. reply_to_message_id=reply_to_message_id,
  833. reply_markup=_reply_markup
  834. )
  835. return sent_message_update
  836. async def edit_message_text(self, text,
  837. chat_id=None, message_id=None,
  838. inline_message_id=None,
  839. parse_mode='HTML',
  840. disable_web_page_preview=None,
  841. reply_markup=None,
  842. update=None):
  843. """Edit message text, sending new messages if necessary.
  844. This method wraps lower-level `TelegramBot.editMessageText` method.
  845. Pass an `update` to extract a message identifier from it.
  846. """
  847. if update is not None:
  848. message_identifier = self.get_message_identifier(update)
  849. if 'chat_id' in message_identifier:
  850. chat_id = message_identifier['chat_id']
  851. message_id = message_identifier['message_id']
  852. if 'inline_message_id' in message_identifier:
  853. inline_message_id = message_identifier['inline_message_id']
  854. for i, text_chunk in enumerate(
  855. self.split_message_text(
  856. text=text,
  857. limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 200,
  858. parse_mode=parse_mode
  859. )
  860. ):
  861. if i == 0:
  862. edited_message = await self.editMessageText(
  863. text=text_chunk,
  864. chat_id=chat_id,
  865. message_id=message_id,
  866. inline_message_id=inline_message_id,
  867. parse_mode=parse_mode,
  868. disable_web_page_preview=disable_web_page_preview,
  869. reply_markup=reply_markup
  870. )
  871. if chat_id is None:
  872. # Cannot send messages without a chat_id
  873. # Inline keyboards attached to inline query results may be
  874. # in chats the bot cannot reach.
  875. break
  876. else:
  877. await self.send_message(
  878. text=text,
  879. chat_id=chat_id,
  880. parse_mode=parse_mode,
  881. disable_web_page_preview=disable_web_page_preview,
  882. reply_markup=reply_markup,
  883. update=update,
  884. reply_to_update=True,
  885. send_default_keyboard=False
  886. )
  887. return edited_message
  888. async def send_photo(self, chat_id=None, photo=None,
  889. caption=None,
  890. parse_mode=None,
  891. disable_notification=None,
  892. reply_to_message_id=None,
  893. reply_markup=None,
  894. update=dict(),
  895. reply_to_update=False,
  896. send_default_keyboard=True,
  897. use_stored_file_id=True):
  898. """Send photos.
  899. This method wraps lower-level `TelegramBot.sendPhoto` method.
  900. Pass an `update` to extract `chat_id` and `message_id` from it.
  901. Set `reply_to_update` = True to reply to `update['message_id']`.
  902. Set `send_default_keyboard` = False to avoid sending default keyboard
  903. as reply_markup (only those messages can be edited, which were
  904. sent with no reply markup or with an inline keyboard).
  905. If photo was already sent by this bot and `use_stored_file_id` is set
  906. to True, use file_id (it is faster and recommended).
  907. """
  908. already_sent = False
  909. if 'message' in update:
  910. update = update['message']
  911. if chat_id is None and 'chat' in update:
  912. chat_id = self.get_chat_id(update)
  913. if reply_to_update and 'message_id' in update:
  914. reply_to_message_id = update['message_id']
  915. if (
  916. send_default_keyboard
  917. and reply_markup is None
  918. and type(chat_id) is int
  919. and chat_id > 0
  920. and caption != self.authorization_denied_message
  921. ):
  922. reply_markup = self.default_keyboard
  923. if type(photo) is str:
  924. photo_path = photo
  925. with self.db as db:
  926. already_sent = db['sent_pictures'].find_one(
  927. path=photo_path,
  928. errors=False
  929. )
  930. if already_sent and use_stored_file_id:
  931. photo = already_sent['file_id']
  932. already_sent = True
  933. else:
  934. already_sent = False
  935. if not any(
  936. [
  937. photo.startswith(url_starter)
  938. for url_starter in ('http', 'www',)
  939. ]
  940. ): # If `photo` is not a url but a local file path
  941. try:
  942. with io.BytesIO() as buffered_picture:
  943. with open(
  944. os.path.join(self.path, photo_path),
  945. 'rb' # Read bytes
  946. ) as photo_file:
  947. buffered_picture.write(photo_file.read())
  948. photo = buffered_picture.getvalue()
  949. except FileNotFoundError:
  950. photo = None
  951. else:
  952. use_stored_file_id = False
  953. if photo is None:
  954. logging.error("Photo is None, `send_photo` returning...")
  955. return
  956. sent_update = None
  957. try:
  958. sent_update = await self.sendPhoto(
  959. chat_id=chat_id,
  960. photo=photo,
  961. caption=caption,
  962. parse_mode=parse_mode,
  963. disable_notification=disable_notification,
  964. reply_to_message_id=reply_to_message_id,
  965. reply_markup=reply_markup
  966. )
  967. if isinstance(sent_update, Exception):
  968. raise Exception("sendPhoto API call failed!")
  969. except Exception as e:
  970. logging.error(f"Error sending photo\n{e}")
  971. if already_sent:
  972. with self.db as db:
  973. db['sent_pictures'].update(
  974. dict(
  975. path=photo_path,
  976. errors=True
  977. ),
  978. ['path']
  979. )
  980. if (
  981. type(sent_update) is dict
  982. and 'photo' in sent_update
  983. and len(sent_update['photo']) > 0
  984. and 'file_id' in sent_update['photo'][0]
  985. and (not already_sent)
  986. and use_stored_file_id
  987. ):
  988. with self.db as db:
  989. db['sent_pictures'].insert(
  990. dict(
  991. path=photo_path,
  992. file_id=sent_update['photo'][0]['file_id'],
  993. errors=False
  994. )
  995. )
  996. return sent_update
  997. async def answer_inline_query(self,
  998. inline_query_id=None,
  999. results=[],
  1000. cache_time=None,
  1001. is_personal=None,
  1002. next_offset=None,
  1003. switch_pm_text=None,
  1004. switch_pm_parameter=None,
  1005. update=None):
  1006. """Answer inline queries.
  1007. This method wraps lower-level `answerInlineQuery` method.
  1008. If `results` is a string, cast it to proper type (list of dicts having
  1009. certain keys). See utilities.make_inline_query_answer for details.
  1010. """
  1011. if (
  1012. inline_query_id is None
  1013. and isinstance(update, dict)
  1014. and 'id' in update
  1015. ):
  1016. inline_query_id = update['id']
  1017. results = make_inline_query_answer(results)
  1018. return await self.answerInlineQuery(
  1019. inline_query_id=inline_query_id,
  1020. results=results,
  1021. cache_time=cache_time,
  1022. is_personal=is_personal,
  1023. next_offset=next_offset,
  1024. switch_pm_text=switch_pm_text,
  1025. switch_pm_parameter=switch_pm_parameter,
  1026. )
  1027. @classmethod
  1028. def set_class_maintenance_message(cls, maintenance_message):
  1029. """Set class maintenance message.
  1030. It will be returned if bot is under maintenance, unless and instance
  1031. `_maintenance_message` is set.
  1032. """
  1033. cls._maintenance_message = maintenance_message
  1034. def set_maintenance_message(self, maintenance_message):
  1035. """Set instance maintenance message.
  1036. It will be returned if bot is under maintenance.
  1037. If instance message is None, default class message is used.
  1038. """
  1039. self._maintenance_message = maintenance_message
  1040. def change_maintenance_status(self, maintenance_message=None, status=None):
  1041. """Put the bot under maintenance or end it.
  1042. While in maintenance, bot will reply to users with maintenance_message
  1043. with a few exceptions.
  1044. If status is not set, it is by default the opposite of the current one.
  1045. Optionally, `maintenance_message` may be set.
  1046. """
  1047. if status is None:
  1048. status = not self.under_maintenance
  1049. assert type(status) is bool, "status must be a boolean value!"
  1050. self._under_maintenance = status
  1051. if maintenance_message:
  1052. self.set_maintenance_message(maintenance_message)
  1053. return self._under_maintenance # Return new status
  1054. def is_allowed_during_maintenance(self, update):
  1055. """Return True if update is allowed during maintenance.
  1056. An update is allowed if any of the criteria in
  1057. `self.allowed_during_maintenance` returns True called on it.
  1058. """
  1059. for criterion in self.allowed_during_maintenance:
  1060. if criterion(update):
  1061. return True
  1062. return False
  1063. def allow_during_maintenance(self, criterion):
  1064. """Add a criterion to allow certain updates during maintenance.
  1065. `criterion` must be a function taking a Telegram `update` dictionary
  1066. and returning a boolean.
  1067. ```# Example of criterion
  1068. def allow_text_messages(update):
  1069. if 'message' in update and 'text' in update['message']:
  1070. return True
  1071. return False
  1072. ```
  1073. """
  1074. self._allowed_during_maintenance.append(criterion)
  1075. async def handle_update_during_maintenance(self, update):
  1076. """Handle an update while bot is under maintenance.
  1077. Handle all types of updates.
  1078. """
  1079. if (
  1080. 'message' in update
  1081. and 'chat' in update['message']
  1082. and update['message']['chat']['id'] > 0
  1083. ):
  1084. return await self.send_message(
  1085. text=self.maintenance_message,
  1086. update=update['message'],
  1087. reply_to_update=True
  1088. )
  1089. elif 'callback_query' in update:
  1090. await self.answerCallbackQuery(
  1091. callback_query_id=update['id'],
  1092. text=remove_html_tags(self.maintenance_message[:45])
  1093. )
  1094. elif 'inline_query' in update:
  1095. await self.answer_inline_query(
  1096. update['inline_query']['id'],
  1097. self.maintenance_message,
  1098. cache_time=30,
  1099. is_personal=False,
  1100. )
  1101. return
  1102. @classmethod
  1103. def set_class_authorization_denied_message(csl, message):
  1104. """Set class authorization denied message.
  1105. It will be returned if user is unauthorized to make a request.
  1106. """
  1107. csl._authorization_denied_message = message
  1108. def set_authorization_denied_message(self, message):
  1109. """Set instance authorization denied message.
  1110. If instance message is None, default class message is used.
  1111. """
  1112. self._authorization_denied_message = message
  1113. def set_authorization_function(self, authorization_function):
  1114. """Set a custom authorization_function.
  1115. It should evaluate True if user is authorized to perform a specific
  1116. action and False otherwise.
  1117. It should take update and role and return a Boolean.
  1118. Default authorization_function always evaluates True.
  1119. """
  1120. self.authorization_function = authorization_function
  1121. @classmethod
  1122. def set_class_unknown_command_message(cls, unknown_command_message):
  1123. """Set class unknown command message.
  1124. It will be returned if user sends an unknown command in private chat.
  1125. """
  1126. cls._unknown_command_message = unknown_command_message
  1127. def set_unknown_command_message(self, unknown_command_message):
  1128. """Set instance unknown command message.
  1129. It will be returned if user sends an unknown command in private chat.
  1130. If instance message is None, default class message is used.
  1131. """
  1132. self._unknown_command_message = unknown_command_message
  1133. def command(self, command, aliases=None, show_in_keyboard=False,
  1134. description="", authorization_level='admin'):
  1135. """Associate a bot command with a custom handler function.
  1136. Decorate command handlers like this:
  1137. ```
  1138. @bot.command('/mycommand', ['Button'], True, "My command", 'user')
  1139. async def command_handler(bot, update, user_record):
  1140. return "Result"
  1141. ```
  1142. When a message text starts with `/command[@bot_name]`, or with an
  1143. alias, it gets passed to the decorated function.
  1144. `command` is the command name (with or without /).
  1145. `aliases` is a list of aliases; each will call the command handler
  1146. function; the first alias will appear as button in
  1147. default_keyboard.
  1148. `show_in_keyboard`, if True, makes first alias appear in
  1149. default_keyboard.
  1150. `description` can be used to help users understand what `/command`
  1151. does.
  1152. `authorization_level` is the lowest authorization level needed to run
  1153. the command.
  1154. """
  1155. if not isinstance(command, str):
  1156. raise TypeError(f'Command `{command}` is not a string')
  1157. if aliases:
  1158. if not isinstance(aliases, list):
  1159. raise TypeError(f'Aliases is not a list: `{aliases}`')
  1160. if not all(
  1161. [
  1162. isinstance(alias, str)
  1163. for alias in aliases
  1164. ]
  1165. ):
  1166. raise TypeError(
  1167. f'Aliases {aliases} is not a list of strings string'
  1168. )
  1169. command = command.strip('/ ').lower()
  1170. def command_decorator(command_handler):
  1171. async def decorated_command_handler(bot, update, user_record):
  1172. logging.info(
  1173. f"Command `{command}@{bot.name}` called by "
  1174. "`{from_}`".format(
  1175. from_=(
  1176. update['from']
  1177. if 'from' in update
  1178. else update['chat']
  1179. )
  1180. )
  1181. )
  1182. if bot.authorization_function(
  1183. update=update,
  1184. user_record=user_record,
  1185. authorization_level=authorization_level
  1186. ):
  1187. # Pass supported arguments from locals() to command_handler
  1188. return await command_handler(
  1189. **{
  1190. name: argument
  1191. for name, argument in locals().items()
  1192. if name in inspect.signature(
  1193. command_handler
  1194. ).parameters
  1195. }
  1196. )
  1197. return self.unauthorized_message
  1198. self.commands[command] = dict(
  1199. handler=decorated_command_handler,
  1200. description=description,
  1201. authorization_level=authorization_level
  1202. )
  1203. if aliases:
  1204. for alias in aliases:
  1205. self.command_aliases[alias] = decorated_command_handler
  1206. if show_in_keyboard:
  1207. self.default_reply_keyboard_elements.append(aliases[0])
  1208. return command_decorator
  1209. def parser(self, condition, description='', authorization_level='admin',
  1210. argument='text'):
  1211. """Define a text message parser.
  1212. Decorate command handlers like this:
  1213. ```
  1214. def custom_criteria(update):
  1215. return 'from' in update
  1216. @bot.parser(custom_criteria, authorization_level='user')
  1217. async def text_parser(bot, update, user_record):
  1218. return "Result"
  1219. ```
  1220. If condition evaluates True when run on a message text
  1221. (not starting with '/'), such decorated function gets
  1222. called on update.
  1223. Conditions of parsers are evaluated in order; when one is True,
  1224. others will be skipped.
  1225. `description` provides information about the parser.
  1226. `authorization_level` is the lowest authorization level needed to call
  1227. the parser.
  1228. """
  1229. if not callable(condition):
  1230. raise TypeError(
  1231. f'Condition {condition.__name__} is not a callable'
  1232. )
  1233. def parser_decorator(parser):
  1234. async def decorated_parser(bot, update, user_record):
  1235. logging.info(
  1236. f"Text message update matching condition "
  1237. f"`{condition.__name__}@{bot.name}` from "
  1238. "`{user}`".format(
  1239. user=(
  1240. update['from']
  1241. if 'from' in update
  1242. else update['chat']
  1243. )
  1244. )
  1245. )
  1246. if bot.authorization_function(
  1247. update=update,
  1248. user_record=user_record,
  1249. authorization_level=authorization_level
  1250. ):
  1251. # Pass supported arguments from locals() to parser
  1252. return await parser(
  1253. **{
  1254. name: argument
  1255. for name, argument in locals().items()
  1256. if name in inspect.signature(parser).parameters
  1257. }
  1258. )
  1259. return bot.unauthorized_message
  1260. self.text_message_parsers[condition] = dict(
  1261. handler=decorated_parser,
  1262. description=description,
  1263. authorization_level=authorization_level,
  1264. argument=argument
  1265. )
  1266. return parser_decorator
  1267. def set_command(self, command, handler, aliases=None,
  1268. show_in_keyboard=False, description="",
  1269. authorization_level='admin'):
  1270. """Associate a `command` with a `handler`.
  1271. When a message text starts with `/command[@bot_name]`, or with an
  1272. alias, it gets passed to the decorated function.
  1273. `command` is the command name (with or without /)
  1274. `handler` is the function to be called on update objects.
  1275. `aliases` is a list of aliases; each will call the command handler
  1276. function; the first alias will appear as button in
  1277. default_keyboard.
  1278. `show_in_keyboard`, if True, makes first alias appear in
  1279. default_keyboard.
  1280. `description` is a description and can be used to help users understand
  1281. what `/command` does.
  1282. `authorization_level` is the lowest authorization level needed to run
  1283. the command.
  1284. """
  1285. if not callable(handler):
  1286. raise TypeError(f'Handler `{handler}` is not callable.')
  1287. return self.command(
  1288. command=command, aliases=aliases,
  1289. show_in_keyboard=show_in_keyboard, description=description,
  1290. authorization_level=authorization_level
  1291. )(handler)
  1292. def button(self, prefix, separator=None, description='',
  1293. authorization_level='admin'):
  1294. """Associate a bot button `prefix` with a handler.
  1295. When a callback data text starts with `prefix`, the associated handler
  1296. is called upon the update.
  1297. Decorate button handlers like this:
  1298. ```
  1299. @bot.button('a_prefix:///', description="A button",
  1300. authorization_level='user')
  1301. async def button_handler(bot, update, user_record, data):
  1302. return "Result"
  1303. ```
  1304. `separator` will be used to parse callback data received when a button
  1305. starting with `prefix` will be pressed.
  1306. `description` contains information about the button.
  1307. `authorization_level` is the lowest authorization level needed to
  1308. be allowed to push the button.
  1309. """
  1310. if not isinstance(prefix, str):
  1311. raise TypeError(
  1312. f'Inline button callback_data {prefix} is not a string'
  1313. )
  1314. def button_decorator(handler):
  1315. async def decorated_button_handler(bot, update, user_record):
  1316. logging.info(
  1317. f"Button `{update['data']}`@{bot.name} pressed by "
  1318. f"`{update['from']}`"
  1319. )
  1320. if bot.authorization_function(
  1321. update=update,
  1322. user_record=user_record,
  1323. authorization_level=authorization_level
  1324. ):
  1325. # Remove `prefix` from `data`
  1326. data = extract(update['data'], prefix)
  1327. # If a specific separator or default separator is set,
  1328. # use it to split `data` string in a list.
  1329. # Cast numeric `data` elements to `int`.
  1330. _separator = separator or self.callback_data_separator
  1331. if _separator:
  1332. data = [
  1333. int(element) if element.isnumeric()
  1334. else element
  1335. for element in data.split(_separator)
  1336. ]
  1337. # Pass supported arguments from locals() to handler
  1338. return await handler(
  1339. **{
  1340. name: argument
  1341. for name, argument in locals().items()
  1342. if name in inspect.signature(handler).parameters
  1343. }
  1344. )
  1345. return bot.unauthorized_message
  1346. self.callback_handlers[prefix] = dict(
  1347. handler=decorated_button_handler,
  1348. description=description,
  1349. authorization_level=authorization_level
  1350. )
  1351. return button_decorator
  1352. def query(self, condition, description='', authorization_level='admin'):
  1353. """Define an inline query.
  1354. Decorator: `@bot.query(example)`
  1355. When an inline query matches the `condition` function,
  1356. decorated function is called and passed the query update object
  1357. as argument.
  1358. `description` is a description
  1359. `authorization_level` is the lowest authorization level needed to run
  1360. the command
  1361. """
  1362. if not callable(condition):
  1363. raise TypeError(
  1364. 'Condition {c} is not a callable'.format(
  1365. c=condition.__name__
  1366. )
  1367. )
  1368. def query_decorator(handler):
  1369. async def decorated_query_handler(bot, update, user_record):
  1370. logging.info(
  1371. f"Inline query matching condition "
  1372. f"`{condition.__name__}@{bot.name}` from "
  1373. f"`{update['from']}`"
  1374. )
  1375. if self.authorization_function(
  1376. update=update,
  1377. user_record=user_record,
  1378. authorization_level=authorization_level
  1379. ):
  1380. # Pass supported arguments from locals() to handler
  1381. return await handler(
  1382. **{
  1383. name: argument
  1384. for name, argument in locals().items()
  1385. if name in inspect.signature(handler).parameters
  1386. }
  1387. )
  1388. return self.unauthorized_message
  1389. self.inline_query_handlers[condition] = dict(
  1390. handler=decorated_query_handler,
  1391. description=description,
  1392. authorization_level=authorization_level
  1393. )
  1394. return query_decorator
  1395. def set_chat_id_getter(self, getter):
  1396. """Set chat_id getter.
  1397. It must be a function that takes an update and returns the proper
  1398. chat_id.
  1399. """
  1400. assert callable(getter), "Chat id getter must be a function!"
  1401. self.get_chat_id = getter
  1402. @staticmethod
  1403. def get_user_identifier(user_id=None, update=None):
  1404. """Get telegram id of user given an update.
  1405. Result itself may be passed as either parameter (for backward
  1406. compatibility).
  1407. """
  1408. identifier = user_id or update
  1409. assert identifier is not None, (
  1410. "Provide a user_id or update object to get a user identifier."
  1411. )
  1412. if (
  1413. isinstance(identifier, dict)
  1414. and 'message' in identifier
  1415. and 'from' not in identifier
  1416. ):
  1417. identifier = identifier['message']
  1418. if isinstance(identifier, dict) and 'from' in identifier:
  1419. identifier = identifier['from']['id']
  1420. assert type(identifier) is int, (
  1421. "Unable to find a user identifier."
  1422. )
  1423. return identifier
  1424. @staticmethod
  1425. def get_message_identifier(update=dict()):
  1426. """Get a message identifier dictionary to edit `update`.
  1427. Pass the result as keyword arguments to `edit...` API methods.
  1428. """
  1429. if 'message' in update:
  1430. update = update['message']
  1431. if 'chat' in update and 'message_id' in update:
  1432. return dict(
  1433. chat_id=update['chat']['id'],
  1434. message_id=update['message_id']
  1435. )
  1436. elif 'inline_message_id' in update:
  1437. return dict(
  1438. inline_message_id=update['inline_message_id']
  1439. )
  1440. def set_individual_text_message_handler(self, handler,
  1441. update=None, user_id=None):
  1442. """Set a custom text message handler for the user.
  1443. Any text message update from the user will be handled by this custom
  1444. handler instead of default handlers for commands, aliases and text.
  1445. Custom handlers last one single use, but they can call this method and
  1446. set themselves as next custom text message handler.
  1447. """
  1448. identifier = self.get_user_identifier(
  1449. user_id=user_id,
  1450. update=update
  1451. )
  1452. assert callable(handler), (f"Handler `{handler.name}` is not "
  1453. "callable. Custom text message handler "
  1454. "could not be set.")
  1455. self.individual_text_message_handlers[identifier] = handler
  1456. return
  1457. def remove_individual_text_message_handler(self,
  1458. update=None, user_id=None):
  1459. """Remove a custom text message handler for the user.
  1460. Any text message update from the user will be handled by default
  1461. handlers for commands, aliases and text.
  1462. """
  1463. identifier = self.get_user_identifier(
  1464. user_id=user_id,
  1465. update=update
  1466. )
  1467. if identifier in self.individual_text_message_handlers:
  1468. del self.individual_text_message_handlers[identifier]
  1469. return
  1470. def set_default_keyboard(self, keyboard='set_default'):
  1471. """Set a default keyboard for the bot.
  1472. If a keyboard is not passed as argument, a default one is generated,
  1473. based on aliases of commands.
  1474. """
  1475. if keyboard == 'set_default':
  1476. buttons = [
  1477. dict(
  1478. text=x
  1479. )
  1480. for x in self.default_reply_keyboard_elements
  1481. ]
  1482. if len(buttons) == 0:
  1483. self._default_keyboard = None
  1484. else:
  1485. self._default_keyboard = dict(
  1486. keyboard=make_lines_of_buttons(
  1487. buttons,
  1488. (2 if len(buttons) < 4 else 3) # Row length
  1489. ),
  1490. resize_keyboard=True
  1491. )
  1492. else:
  1493. self._default_keyboard = keyboard
  1494. return
  1495. async def webhook_feeder(self, request):
  1496. """Handle incoming HTTP `request`s.
  1497. Get data, feed webhook and return and OK message.
  1498. """
  1499. update = await request.json()
  1500. asyncio.ensure_future(
  1501. self.route_update(update)
  1502. )
  1503. return web.Response(
  1504. body='OK'.encode('utf-8')
  1505. )
  1506. async def get_me(self):
  1507. """Get bot information.
  1508. Restart bots if bot can't be got.
  1509. """
  1510. try:
  1511. me = await self.getMe()
  1512. if isinstance(me, Exception):
  1513. raise me
  1514. elif me is None:
  1515. raise Exception('getMe returned None')
  1516. self._name = me["username"]
  1517. self._telegram_id = me['id']
  1518. except Exception as e:
  1519. logging.error(
  1520. f"API getMe method failed, information about this bot could "
  1521. f"not be retrieved. Restarting in 5 minutes...\n\n"
  1522. f"Error information:\n{e}"
  1523. )
  1524. await asyncio.sleep(5*60)
  1525. self.__class__.stop(
  1526. 65,
  1527. f"Information aformation about this bot could "
  1528. f"not be retrieved. Restarting..."
  1529. )
  1530. def setup(self):
  1531. """Make bot ask for updates and handle responses."""
  1532. self.set_default_keyboard()
  1533. if not self.webhook_url:
  1534. asyncio.ensure_future(self.get_updates())
  1535. else:
  1536. asyncio.ensure_future(self.set_webhook())
  1537. self.__class__.app.router.add_route(
  1538. 'POST', self.webhook_local_address, self.webhook_feeder
  1539. )
  1540. async def close_sessions(self):
  1541. """Close open sessions."""
  1542. for session_name, session in self.sessions.items():
  1543. if not session.closed:
  1544. await session.close()
  1545. async def set_webhook(self, url=None, certificate=None,
  1546. max_connections=None, allowed_updates=None):
  1547. """Set a webhook if token is valid."""
  1548. # Return if token is invalid
  1549. await self.get_me()
  1550. if self.name is None:
  1551. return
  1552. webhook_was_set = await self.setWebhook(
  1553. url=url, certificate=certificate, max_connections=max_connections,
  1554. allowed_updates=allowed_updates
  1555. ) # `setWebhook` API method returns `True` on success
  1556. webhook_information = await self.getWebhookInfo()
  1557. webhook_information['url'] = webhook_information['url'].replace(
  1558. self.token, "<BOT_TOKEN>"
  1559. ).replace(
  1560. self.session_token, "<SESSION_TOKEN>"
  1561. )
  1562. if webhook_was_set:
  1563. logging.info(
  1564. f"Webhook was set correctly.\n"
  1565. f"Webhook information: {webhook_information}"
  1566. )
  1567. else:
  1568. logging.error(
  1569. f"Failed to set webhook!\n"
  1570. f"Webhook information: {webhook_information}"
  1571. )
  1572. async def get_updates(self, timeout=30, limit=100, allowed_updates=None,
  1573. error_cooldown=10):
  1574. """Get updates using long polling.
  1575. timeout : int
  1576. Timeout set for Telegram servers. Make sure that connection timeout
  1577. is greater than `timeout`.
  1578. limit : int (1 - 100)
  1579. Max number of updates to be retrieved.
  1580. allowed_updates : List(str)
  1581. List of update types to be retrieved.
  1582. Empty list to allow all updates.
  1583. None to fallback to class default.
  1584. """
  1585. # Return if token is invalid
  1586. await self.get_me()
  1587. if self.name is None:
  1588. return
  1589. # Set custom list of allowed updates or fallback to class default list
  1590. if allowed_updates is None:
  1591. allowed_updates = self.allowed_updates
  1592. await self.deleteWebhook() # Remove eventually active webhook
  1593. update = None # Do not update offset if no update is received
  1594. while True:
  1595. updates = await self.getUpdates(
  1596. offset=self._offset,
  1597. timeout=timeout,
  1598. limit=limit,
  1599. allowed_updates=allowed_updates
  1600. )
  1601. if updates is None:
  1602. continue
  1603. elif isinstance(updates, TelegramError):
  1604. logging.error(
  1605. f"Waiting {error_cooldown} seconds before trying again..."
  1606. )
  1607. await asyncio.sleep(error_cooldown)
  1608. continue
  1609. for update in updates:
  1610. asyncio.ensure_future(self.route_update(update))
  1611. if update is not None:
  1612. self._offset = update['update_id'] + 1
  1613. def set_router(self, event, handler):
  1614. """Set `handler` as router for `event`."""
  1615. self.routing_table[event] = handler
  1616. async def route_update(self, update):
  1617. """Pass `update` to proper method.
  1618. Update objects have two keys:
  1619. - `update_id` (which is used as offset while retrieving new updates)
  1620. - One and only one of the following
  1621. `message`
  1622. `edited_message`
  1623. `channel_post`
  1624. `edited_channel_post`
  1625. `inline_query`
  1626. `chosen_inline_result`
  1627. `callback_query`
  1628. `shipping_query`
  1629. `pre_checkout_query`
  1630. `poll`
  1631. """
  1632. if (
  1633. self.under_maintenance
  1634. and not self.is_allowed_during_maintenance(update)
  1635. ):
  1636. return await self.handle_update_during_maintenance(update)
  1637. for key, value in update.items():
  1638. if key in self.routing_table:
  1639. with self.db as db:
  1640. user_record = db['users'].find_one(
  1641. telegram_id=self.get_user_identifier(
  1642. update=value
  1643. )
  1644. )
  1645. return await self.routing_table[key](value, user_record)
  1646. logging.error(f"Unknown type of update.\n{update}")
  1647. def additional_task(self, when='BEFORE', *args, **kwargs):
  1648. """Add a task before at app start or cleanup.
  1649. Decorate an async function to have it awaited `BEFORE` or `AFTER` main
  1650. loop.
  1651. """
  1652. when = when[0].lower()
  1653. def additional_task_decorator(task):
  1654. if when == 'b':
  1655. self.preliminary_tasks.append(task(*args, **kwargs))
  1656. elif when == 'a':
  1657. self.final_tasks.append(task(*args, **kwargs))
  1658. return additional_task_decorator
  1659. @classmethod
  1660. async def start_app(cls):
  1661. """Start running `aiohttp.web.Application`.
  1662. It will route webhook-received updates and other custom paths.
  1663. """
  1664. assert cls.local_host is not None, "Invalid local host"
  1665. assert cls.port is not None, "Invalid port"
  1666. cls.runner = web.AppRunner(cls.app)
  1667. await cls.runner.setup()
  1668. cls.server = web.TCPSite(cls.runner, cls.local_host, cls.port)
  1669. await cls.server.start()
  1670. logging.info(f"App running at http://{cls.local_host}:{cls.port}")
  1671. @classmethod
  1672. async def stop_app(cls):
  1673. """Close bot sessions and cleanup."""
  1674. for bot in cls.bots:
  1675. await asyncio.gather(
  1676. *bot.final_tasks
  1677. )
  1678. await bot.close_sessions()
  1679. await cls.runner.cleanup()
  1680. @classmethod
  1681. def stop(cls, message, final_state=0):
  1682. """Log a final `message`, stop loop and set exiting `code`.
  1683. All bots and the web app will be terminated gracefully.
  1684. The final state may be retrieved to get information about what stopped
  1685. the bots.
  1686. """
  1687. logging.info(message)
  1688. cls.final_state = final_state
  1689. cls.loop.stop()
  1690. return
  1691. @classmethod
  1692. def run(cls, local_host=None, port=None):
  1693. """Run aiohttp web app and all Bot instances.
  1694. Each bot will receive updates via long polling or webhook according to
  1695. its initialization parameters.
  1696. A single aiohttp.web.Application instance will be run (cls.app) on
  1697. local_host:port and it may serve custom-defined routes as well.
  1698. """
  1699. if local_host is not None:
  1700. cls.local_host = local_host
  1701. if port is not None:
  1702. cls.port = port
  1703. try:
  1704. cls.loop.run_until_complete(
  1705. asyncio.gather(
  1706. *[
  1707. preliminary_task
  1708. for bot in cls.bots
  1709. for preliminary_task in bot.preliminary_tasks
  1710. ]
  1711. )
  1712. )
  1713. except Exception as e:
  1714. logging.error(f"{e}", exc_info=True)
  1715. for bot in cls.bots:
  1716. bot.setup()
  1717. asyncio.ensure_future(cls.start_app())
  1718. try:
  1719. cls.loop.run_forever()
  1720. except KeyboardInterrupt:
  1721. logging.info("Stopped by KeyboardInterrupt")
  1722. except Exception as e:
  1723. logging.error(f"{e}", exc_info=True)
  1724. finally:
  1725. cls.loop.run_until_complete(cls.stop_app())
  1726. return cls.final_state