Queer European MD passionate about IT

bot.py 114 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989
  1. """Provide a simple Bot object, mirroring Telegram API methods.
  2. camelCase methods mirror API directly, while snake_case ones act as middleware
  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. import datetime
  29. import io
  30. import inspect
  31. import logging
  32. import os
  33. import re
  34. import sys
  35. from collections import OrderedDict
  36. from typing import Callable
  37. # Third party modules
  38. from aiohttp import web
  39. # Project modules
  40. from .api import TelegramBot, TelegramError
  41. from .database import ObjectWithDatabase
  42. from .languages import MultiLanguageObject
  43. from .utilities import (
  44. async_get, escape_html_chars, extract, get_secure_key,
  45. make_inline_query_answer, make_lines_of_buttons, remove_html_tags
  46. )
  47. # Do not log aiohttp `INFO` and `DEBUG` levels
  48. logging.getLogger('aiohttp').setLevel(logging.WARNING)
  49. # Some methods are not implemented yet: that's the reason behind the following statement
  50. # noinspection PyUnusedLocal,PyMethodMayBeStatic,PyMethodMayBeStatic
  51. class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
  52. """Simple Bot object, providing methods corresponding to Telegram bot API.
  53. Multiple Bot() instances may be run together, along with a aiohttp web app.
  54. """
  55. bots = []
  56. _path = '.'
  57. runner = None
  58. # TODO: find a way to choose port automatically by default
  59. # Setting port to 0 does not work unfortunately
  60. local_host = 'localhost'
  61. port = 3000
  62. final_state = 0
  63. _maintenance_message = ("I am currently under maintenance!\n"
  64. "Please retry later...")
  65. _authorization_denied_message = None
  66. _unknown_command_message = None
  67. TELEGRAM_MESSAGES_MAX_LEN = 4096
  68. _default_inline_query_answer = [
  69. dict(
  70. type='article',
  71. id=0,
  72. title="I cannot answer this query, sorry",
  73. input_message_content=dict(
  74. message_text="I'm sorry "
  75. "but I could not find an answer for your query."
  76. )
  77. )
  78. ]
  79. _log_file_name = None
  80. _errors_file_name = None
  81. def __init__(
  82. self, token, hostname='', certificate=None, max_connections=40,
  83. allowed_updates=None, database_url='bot.db'
  84. ):
  85. """Init a bot instance.
  86. token : str
  87. Telegram bot API token.
  88. hostname : str
  89. Domain (or public IP address) for webhooks.
  90. certificate : str
  91. Path to domain certificate.
  92. max_connections : int (1 - 100)
  93. Maximum number of HTTPS connections allowed.
  94. allowed_updates : List(str)
  95. Allowed update types (empty list to allow all).
  96. @type allowed_updates: list(str)
  97. """
  98. # Append `self` to class list of instances
  99. self.__class__.bots.append(self)
  100. # Call superclasses constructors with proper arguments
  101. TelegramBot.__init__(self, token)
  102. ObjectWithDatabase.__init__(self, database_url=database_url)
  103. MultiLanguageObject.__init__(self)
  104. self._path = None
  105. self.preliminary_tasks = []
  106. self.final_tasks = []
  107. self._offset = 0
  108. self._hostname = hostname
  109. self._certificate = certificate
  110. self._max_connections = max_connections
  111. self._allowed_updates = allowed_updates
  112. self._session_token = get_secure_key(length=10)
  113. self._name = None
  114. self._telegram_id = None
  115. # The following routing table associates each type of Telegram `update`
  116. # with a Bot method to be invoked on it.
  117. self.routing_table = {
  118. 'message': self.message_router,
  119. 'edited_message': self.edited_message_handler,
  120. 'channel_post': self.channel_post_handler,
  121. 'edited_channel_post': self.edited_channel_post_handler,
  122. 'inline_query': self.inline_query_handler,
  123. 'chosen_inline_result': self.chosen_inline_result_handler,
  124. 'callback_query': self.callback_query_handler,
  125. 'shipping_query': self.shipping_query_handler,
  126. 'pre_checkout_query': self.pre_checkout_query_handler,
  127. 'poll': self.poll_handler,
  128. }
  129. # Different message update types need different handlers
  130. self.message_handlers = {
  131. 'text': self.text_message_handler,
  132. 'audio': self.audio_file_handler,
  133. 'document': self.document_message_handler,
  134. 'animation': self.animation_message_handler,
  135. 'game': self.game_message_handler,
  136. 'photo': self.photo_message_handler,
  137. 'sticker': self.sticker_message_handler,
  138. 'video': self.video_message_handler,
  139. 'voice': self.voice_message_handler,
  140. 'video_note': self.video_note_message_handler,
  141. 'contact': self.contact_message_handler,
  142. 'location': self.location_message_handler,
  143. 'venue': self.venue_message_handler,
  144. 'poll': self.poll_message_handler,
  145. 'new_chat_members': self.new_chat_members_message_handler,
  146. 'left_chat_member': self.left_chat_member_message_handler,
  147. 'new_chat_title': self.new_chat_title_message_handler,
  148. 'new_chat_photo': self.new_chat_photo_message_handler,
  149. 'delete_chat_photo': self.delete_chat_photo_message_handler,
  150. 'group_chat_created': self.group_chat_created_message_handler,
  151. 'supergroup_chat_created': (
  152. self.supergroup_chat_created_message_handler
  153. ),
  154. 'channel_chat_created': self.channel_chat_created_message_handler,
  155. 'migrate_to_chat_id': self.migrate_to_chat_id_message_handler,
  156. 'migrate_from_chat_id': self.migrate_from_chat_id_message_handler,
  157. 'pinned_message': self.pinned_message_message_handler,
  158. 'invoice': self.invoice_message_handler,
  159. 'successful_payment': self.successful_payment_message_handler,
  160. 'connected_website': self.connected_website_message_handler,
  161. 'passport_data': self.passport_data_message_handler,
  162. 'dice': self.dice_handler,
  163. }
  164. # Special text message handlers: individual, commands, aliases, parsers
  165. self.individual_text_message_handlers = dict()
  166. self.commands = OrderedDict()
  167. self.command_aliases = OrderedDict()
  168. self.messages['commands'] = dict()
  169. self.messages['reply_keyboard_buttons'] = dict()
  170. self._unknown_command_message = None
  171. self.text_message_parsers = OrderedDict()
  172. # Support for /help command
  173. self.messages['help_sections'] = OrderedDict()
  174. # Handle location messages
  175. self.individual_location_handlers = dict()
  176. # Handle voice messages
  177. self.individual_voice_handlers = dict()
  178. # Callback query-related properties
  179. self.callback_handlers = OrderedDict()
  180. self._callback_data_separator = None
  181. # Inline query-related properties
  182. self.inline_query_handlers = OrderedDict()
  183. self._default_inline_query_answer = None
  184. self.chosen_inline_result_handlers = dict()
  185. # Maintenance properties
  186. self._under_maintenance = False
  187. self._allowed_during_maintenance = []
  188. self._maintenance_message = None
  189. # Default chat_id getter: same chat as update
  190. self.get_chat_id = lambda update: (
  191. update['message']['chat']['id']
  192. if 'message' in update and 'chat' in update['message']
  193. else update['chat']['id']
  194. if 'chat' in update
  195. else None
  196. )
  197. # Function to get updated list of bot administrators
  198. self._get_administrators = lambda bot: []
  199. # Message to be returned if user is not allowed to call method
  200. self._authorization_denied_message = None
  201. # Default authorization function (always return True)
  202. self.authorization_function = (
  203. lambda update, user_record=None, authorization_level='user': True
  204. )
  205. self.default_reply_keyboard_elements = []
  206. self.recent_users = OrderedDict()
  207. self._log_file_name = None
  208. self._errors_file_name = None
  209. self.placeholder_requests = dict()
  210. self.shared_data = dict()
  211. self.Role = None
  212. self.packages = [sys.modules['davtelepot']]
  213. # Add `users` table with its fields if missing
  214. if 'users' not in self.db.tables:
  215. table = self.db.create_table(
  216. table_name='users'
  217. )
  218. table.create_column(
  219. 'telegram_id',
  220. self.db.types.integer
  221. )
  222. table.create_column(
  223. 'privileges',
  224. self.db.types.integer
  225. )
  226. table.create_column(
  227. 'username',
  228. self.db.types.string
  229. )
  230. table.create_column(
  231. 'first_name',
  232. self.db.types.string
  233. )
  234. table.create_column(
  235. 'last_name',
  236. self.db.types.string
  237. )
  238. table.create_column(
  239. 'language_code',
  240. self.db.types.string
  241. )
  242. table.create_column(
  243. 'selected_language_code',
  244. self.db.types.string
  245. )
  246. return
  247. @property
  248. def path(self):
  249. """Path where files should be looked for.
  250. If no instance path is set, return class path.
  251. """
  252. return self._path or self.__class__._path
  253. @classmethod
  254. def set_class_path(cls, path):
  255. """Set class path attribute."""
  256. cls._path = path
  257. def set_path(self, path):
  258. """Set instance path attribute."""
  259. self._path = path
  260. @property
  261. def log_file_name(self):
  262. """Return log file name.
  263. Fallback to class file name if set, otherwise return None.
  264. """
  265. return self._log_file_name or self.__class__._log_file_name
  266. @property
  267. def log_file_path(self):
  268. """Return log file path basing on self.path and `_log_file_name`.
  269. Fallback to class file if set, otherwise return None.
  270. """
  271. if self.log_file_name:
  272. return f"{self.path}/data/{self.log_file_name}"
  273. def set_log_file_name(self, file_name):
  274. """Set log file name."""
  275. self._log_file_name = file_name
  276. @classmethod
  277. def set_class_log_file_name(cls, file_name):
  278. """Set class log file name."""
  279. cls._log_file_name = file_name
  280. @property
  281. def errors_file_name(self):
  282. """Return errors file name.
  283. Fallback to class file name if set, otherwise return None.
  284. """
  285. return self._errors_file_name or self.__class__._errors_file_name
  286. @property
  287. def errors_file_path(self):
  288. """Return errors file path basing on self.path and `_errors_file_name`.
  289. Fallback to class file if set, otherwise return None.
  290. """
  291. if self.errors_file_name:
  292. return f"{self.path}/data/{self.errors_file_name}"
  293. def set_errors_file_name(self, file_name):
  294. """Set errors file name."""
  295. self._errors_file_name = file_name
  296. @classmethod
  297. def set_class_errors_file_name(cls, file_name):
  298. """Set class errors file name."""
  299. cls._errors_file_name = file_name
  300. @classmethod
  301. def get(cls, token, *args, **kwargs):
  302. """Given a `token`, return class instance with that token.
  303. If no instance is found, instantiate it.
  304. Positional and keyword arguments may be passed as well.
  305. """
  306. for bot in cls.bots:
  307. if bot.token == token:
  308. return bot
  309. return cls(token, *args, **kwargs)
  310. @property
  311. def hostname(self):
  312. """Hostname for the webhook URL.
  313. It must be a public domain or IP address. Port may be specified.
  314. A custom webhook url, including bot token and a random token, will be
  315. generated for Telegram to post new updates.
  316. """
  317. return self._hostname
  318. @property
  319. def webhook_url(self):
  320. """URL where Telegram servers should post new updates.
  321. It must be a public domain name or IP address. Port may be specified.
  322. """
  323. if not self.hostname:
  324. return ''
  325. return (
  326. f"{self.hostname}/webhook/{self.token}_{self.session_token}/"
  327. )
  328. @property
  329. def webhook_local_address(self):
  330. """Local address where Telegram updates are routed by revers proxy."""
  331. return (
  332. f"/webhook/{self.token}_{self.session_token}/"
  333. )
  334. @property
  335. def certificate(self):
  336. """Public certificate for `webhook_url`.
  337. May be self-signed
  338. """
  339. return self._certificate
  340. @property
  341. def max_connections(self):
  342. """Maximum number of simultaneous HTTPS connections allowed.
  343. Telegram will open as many connections as possible to boost bot’s
  344. throughput, lower values limit the load on bot‘s server.
  345. """
  346. return self._max_connections
  347. @property
  348. def allowed_updates(self):
  349. """List of update types to be retrieved.
  350. Empty list to allow all updates.
  351. """
  352. return self._allowed_updates or []
  353. @property
  354. def name(self):
  355. """Bot name."""
  356. return self._name
  357. @property
  358. def telegram_id(self):
  359. """Telegram id of this bot."""
  360. return self._telegram_id
  361. @property
  362. def session_token(self):
  363. """Return a token generated with the current instantiation."""
  364. return self._session_token
  365. @property
  366. def offset(self):
  367. """Return last update id.
  368. Useful to ignore repeated updates and restore original update order.
  369. """
  370. return self._offset
  371. @property
  372. def under_maintenance(self):
  373. """Return True if bot is under maintenance.
  374. While under maintenance, bot will reply `self.maintenance_message` to
  375. any update, except those which `self.is_allowed_during_maintenance`
  376. returns True for.
  377. """
  378. return self._under_maintenance
  379. @property
  380. def allowed_during_maintenance(self):
  381. """Return the list of criteria to allow an update during maintenance.
  382. If any of this criteria returns True on an update, that update will be
  383. handled even during maintenance.
  384. """
  385. return self._allowed_during_maintenance
  386. @property
  387. def maintenance_message(self):
  388. """Message to be returned if bot is under maintenance.
  389. If instance message is not set, class message is returned.
  390. """
  391. if self._maintenance_message:
  392. return self._maintenance_message
  393. if self.__class__.maintenance_message:
  394. return self.__class__._maintenance_message
  395. return ("I am currently under maintenance!\n"
  396. "Please retry later...")
  397. @property
  398. def authorization_denied_message(self):
  399. """Return this text if user is unauthorized to make a request.
  400. If instance message is not set, class message is returned.
  401. """
  402. if self._authorization_denied_message:
  403. return self._authorization_denied_message
  404. return self.__class__._authorization_denied_message
  405. def get_keyboard(self, user_record=None, update=None,
  406. telegram_id=None):
  407. """Return a reply keyboard translated into user language."""
  408. if user_record is None:
  409. user_record = dict()
  410. if update is None:
  411. update = dict()
  412. if (not user_record) and telegram_id:
  413. with self.db as db:
  414. user_record = db['users'].find_one(telegram_id=telegram_id)
  415. buttons = [
  416. dict(
  417. text=self.get_message(
  418. 'reply_keyboard_buttons', command,
  419. user_record=user_record, update=update,
  420. default_message=element['reply_keyboard_button']
  421. )
  422. )
  423. for command, element in self.commands.items()
  424. if 'reply_keyboard_button' in element
  425. ]
  426. if len(buttons) == 0:
  427. return
  428. return dict(
  429. keyboard=make_lines_of_buttons(
  430. buttons,
  431. (2 if len(buttons) < 4 else 3) # Row length
  432. ),
  433. resize_keyboard=True
  434. )
  435. @property
  436. def unknown_command_message(self):
  437. """Message to be returned if user sends an unknown command.
  438. If instance message is not set, class message is returned.
  439. """
  440. if self._unknown_command_message:
  441. message = self._unknown_command_message
  442. else:
  443. message = self.__class__._unknown_command_message
  444. if isinstance(message, str):
  445. message = message.format(bot=self)
  446. return message
  447. @property
  448. def callback_data_separator(self):
  449. """Separator between callback data elements.
  450. Example of callback_data: 'my_button_prefix:///1|4|test'
  451. Prefix: `my_button_prefix:///`
  452. Separator: `|` <--- this is returned
  453. Data: `['1', '4', 'test']`
  454. """
  455. return self._callback_data_separator
  456. def set_callback_data_separator(self, separator):
  457. """Set a callback_data separator.
  458. See property `callback_data_separator` for details.
  459. """
  460. assert type(separator) is str, "Separator must be a string!"
  461. self._callback_data_separator = separator
  462. @property
  463. def default_inline_query_answer(self):
  464. """Answer to be returned if inline query returned None.
  465. If instance default answer is not set, class one is returned.
  466. """
  467. if self._default_inline_query_answer:
  468. return self._default_inline_query_answer
  469. return self.__class__._default_inline_query_answer
  470. @classmethod
  471. def set_class_default_inline_query_answer(cls,
  472. default_inline_query_answer):
  473. """Set class default inline query answer.
  474. It will be returned if an inline query returned no answer.
  475. """
  476. cls._default_inline_query_answer = make_inline_query_answer(
  477. default_inline_query_answer
  478. )
  479. def set_default_inline_query_answer(self, default_inline_query_answer):
  480. """Set a custom default_inline_query_answer.
  481. It will be returned when no answer is found for an inline query.
  482. If instance answer is None, default class answer is used.
  483. """
  484. self._default_inline_query_answer = make_inline_query_answer(
  485. default_inline_query_answer
  486. )
  487. def set_get_administrator_function(self,
  488. new_function: Callable[[object],
  489. list]):
  490. """Set a new get_administrators function.
  491. This function should take bot as argument and return an updated list
  492. of its administrators.
  493. Example:
  494. ```python
  495. def get_administrators(bot):
  496. admins = bot.db['users'].find(privileges=2)
  497. return list(admins)
  498. ```
  499. """
  500. self._get_administrators = new_function
  501. @property
  502. def administrators(self):
  503. return self._get_administrators(self)
  504. async def message_router(self, update, user_record):
  505. """Route Telegram `message` update to appropriate message handler."""
  506. for key, value in update.items():
  507. if key in self.message_handlers:
  508. return await self.message_handlers[key](update, user_record)
  509. logging.error(
  510. f"The following message update was received: {update}\n"
  511. "However, this message type is unknown."
  512. )
  513. async def edited_message_handler(self, update, user_record):
  514. """Handle Telegram `edited_message` update."""
  515. logging.info(
  516. f"The following update was received: {update}\n"
  517. "However, this edited_message handler does nothing yet."
  518. )
  519. return
  520. async def channel_post_handler(self, update, user_record):
  521. """Handle Telegram `channel_post` update."""
  522. logging.info(
  523. f"The following update was received: {update}\n"
  524. "However, this channel_post handler does nothing yet."
  525. )
  526. return
  527. async def edited_channel_post_handler(self, update, user_record):
  528. """Handle Telegram `edited_channel_post` update."""
  529. logging.info(
  530. f"The following update was received: {update}\n"
  531. "However, this edited_channel_post handler does nothing yet."
  532. )
  533. return
  534. async def inline_query_handler(self, update, user_record):
  535. """Handle Telegram `inline_query` update.
  536. Answer it with results or log errors.
  537. """
  538. query = update['query']
  539. results, switch_pm_text, switch_pm_parameter = None, None, None
  540. for condition, handler in self.inline_query_handlers.items():
  541. if condition(query):
  542. _handler = handler['handler']
  543. results = await _handler(bot=self, update=update,
  544. user_record=user_record)
  545. break
  546. if not results:
  547. results = self.default_inline_query_answer
  548. if isinstance(results, dict) and 'answer' in results:
  549. if 'switch_pm_text' in results:
  550. switch_pm_text = results['switch_pm_text']
  551. if 'switch_pm_parameter' in results:
  552. switch_pm_parameter = results['switch_pm_parameter']
  553. results = results['answer']
  554. try:
  555. await self.answer_inline_query(
  556. update=update,
  557. user_record=user_record,
  558. results=results,
  559. cache_time=10,
  560. is_personal=True,
  561. switch_pm_text=switch_pm_text,
  562. switch_pm_parameter=switch_pm_parameter
  563. )
  564. except Exception as e:
  565. logging.info("Error answering inline query\n{}".format(e))
  566. return
  567. async def chosen_inline_result_handler(self, update, user_record):
  568. """Handle Telegram `chosen_inline_result` update."""
  569. if user_record is not None:
  570. user_id = user_record['telegram_id']
  571. else:
  572. user_id = update['from']['id']
  573. if user_id in self.chosen_inline_result_handlers:
  574. result_id = update['result_id']
  575. handlers = self.chosen_inline_result_handlers[user_id]
  576. if result_id in handlers:
  577. await handlers[result_id](update)
  578. return
  579. def set_chosen_inline_result_handler(self, user_id, result_id, handler):
  580. """Associate a `handler` to a `result_id` for `user_id`.
  581. When an inline result is chosen having that id, `handler` will
  582. be called and passed the update as argument.
  583. """
  584. if type(user_id) is dict:
  585. user_id = user_id['from']['id']
  586. assert type(user_id) is int, "user_id must be int!"
  587. # Query result ids are parsed as str by telegram
  588. result_id = str(result_id)
  589. assert callable(handler), "Handler must be callable"
  590. if user_id not in self.chosen_inline_result_handlers:
  591. self.chosen_inline_result_handlers[user_id] = {}
  592. self.chosen_inline_result_handlers[user_id][result_id] = handler
  593. return
  594. async def callback_query_handler(self, update, user_record):
  595. """Handle Telegram `callback_query` update.
  596. A callback query is sent when users press inline keyboard buttons.
  597. Bad clients may send malformed or deceiving callback queries:
  598. never put secrets in buttons and always check request validity!
  599. Get an `answer` from the callback handler associated to the query
  600. prefix and use it to edit the source message (or send new ones
  601. if text is longer than single message limit).
  602. Anyway, the query is answered, otherwise the client would hang and
  603. the bot would look like idle.
  604. """
  605. assert 'data' in update, "Malformed callback query lacking data field."
  606. answer = dict()
  607. data = update['data']
  608. for start_text, handler in self.callback_handlers.items():
  609. if data.startswith(start_text):
  610. _function = handler['handler']
  611. answer = await _function(
  612. bot=self,
  613. update=update,
  614. user_record=user_record
  615. )
  616. break
  617. if answer is None:
  618. answer = ''
  619. if type(answer) is str:
  620. answer = dict(text=answer)
  621. assert type(answer) is dict, "Invalid callback query answer."
  622. if 'edit' in answer:
  623. message_identifier = self.get_message_identifier(update)
  624. edit = answer['edit']
  625. method = (
  626. self.edit_message_text if 'text' in edit
  627. else self.editMessageCaption if 'caption' in edit
  628. else self.editMessageReplyMarkup if 'reply_markup' in edit
  629. else (lambda *args, **kwargs: None)
  630. )
  631. try:
  632. await method(**message_identifier, **edit)
  633. except TelegramError as e:
  634. logging.info("Message was not modified:\n{}".format(e))
  635. try:
  636. return await self.answerCallbackQuery(
  637. callback_query_id=update['id'],
  638. **{
  639. key: (val[:180] if key == 'text' else val)
  640. for key, val in answer.items()
  641. if key in ('text', 'show_alert', 'cache_time')
  642. }
  643. )
  644. except TelegramError as e:
  645. logging.error(e)
  646. return
  647. async def shipping_query_handler(self, update, user_record):
  648. """Handle Telegram `shipping_query` update."""
  649. logging.info(
  650. f"The following update was received: {update}\n"
  651. "However, this shipping_query handler does nothing yet."
  652. )
  653. return
  654. async def pre_checkout_query_handler(self, update, user_record):
  655. """Handle Telegram `pre_checkout_query` update."""
  656. logging.info(
  657. f"The following update was received: {update}\n"
  658. "However, this pre_checkout_query handler does nothing yet."
  659. )
  660. return
  661. async def poll_handler(self, update, user_record):
  662. """Handle Telegram `poll` update."""
  663. logging.info(
  664. f"The following update was received: {update}\n"
  665. "However, this poll handler does nothing yet."
  666. )
  667. return
  668. async def text_message_handler(self, update, user_record):
  669. """Handle `text` message update."""
  670. replier, reply = None, None
  671. text = update['text'].lower()
  672. user_id = update['from']['id'] if 'from' in update else None
  673. if user_id in self.individual_text_message_handlers:
  674. replier = self.individual_text_message_handlers[user_id]
  675. del self.individual_text_message_handlers[user_id]
  676. elif text.startswith('/'): # Handle commands
  677. # A command must always start with the ‘/’ symbol and may not be
  678. # longer than 32 characters.
  679. # Commands can use latin letters, numbers and underscores.
  680. command = re.search(
  681. r"([A-z_1-9]){1,32}", # Command pattern (without leading `/`)
  682. text
  683. ).group(0) # Get the first group of characters matching pattern
  684. if command in self.commands:
  685. replier = self.commands[command]['handler']
  686. elif 'chat' in update and update['chat']['id'] > 0:
  687. reply = dict(text=self.unknown_command_message)
  688. else: # Handle command aliases and text parsers
  689. # Aliases are case insensitive: text and alias are both .lower()
  690. for alias, function in self.command_aliases.items():
  691. if text.startswith(alias.lower()):
  692. replier = function
  693. break
  694. # Text message update parsers
  695. for check_function, parser in self.text_message_parsers.items():
  696. if (
  697. parser['argument'] == 'text'
  698. and check_function(text)
  699. ) or (
  700. parser['argument'] == 'update'
  701. and check_function(update)
  702. ):
  703. replier = parser['handler']
  704. break
  705. if replier:
  706. reply = await replier(
  707. bot=self,
  708. **{
  709. name: argument
  710. for name, argument in locals().items()
  711. if name in inspect.signature(
  712. replier
  713. ).parameters
  714. }
  715. )
  716. if reply:
  717. if type(reply) is str:
  718. reply = dict(text=reply)
  719. try:
  720. return await self.reply(update=update, **reply)
  721. except Exception as e:
  722. logging.error(
  723. f"Failed to handle text message:\n{e}",
  724. exc_info=True
  725. )
  726. return
  727. async def audio_file_handler(self, update, user_record):
  728. """Handle `audio` file update."""
  729. logging.info(
  730. "A audio file update was received, "
  731. "but this handler does nothing yet."
  732. )
  733. async def document_message_handler(self, update, user_record):
  734. """Handle `document` message update."""
  735. logging.info(
  736. "A document message update was received, "
  737. "but this handler does nothing yet."
  738. )
  739. async def animation_message_handler(self, update, user_record):
  740. """Handle `animation` message update."""
  741. logging.info(
  742. "A animation message update was received, "
  743. "but this handler does nothing yet."
  744. )
  745. async def game_message_handler(self, update, user_record):
  746. """Handle `game` message update."""
  747. logging.info(
  748. "A game message update was received, "
  749. "but this handler does nothing yet."
  750. )
  751. async def photo_message_handler(self, update, user_record):
  752. """Handle `photo` message update."""
  753. logging.info(
  754. "A photo message update was received, "
  755. "but this handler does nothing yet."
  756. )
  757. async def sticker_message_handler(self, update, user_record):
  758. """Handle `sticker` message update."""
  759. logging.info(
  760. "A sticker message update was received, "
  761. "but this handler does nothing yet."
  762. )
  763. async def video_message_handler(self, update, user_record):
  764. """Handle `video` message update."""
  765. logging.info(
  766. "A video message update was received, "
  767. "but this handler does nothing yet."
  768. )
  769. async def voice_message_handler(self, update, user_record):
  770. """Handle `voice` message update."""
  771. replier, reply = None, None
  772. user_id = update['from']['id'] if 'from' in update else None
  773. if user_id in self.individual_voice_handlers:
  774. replier = self.individual_voice_handlers[user_id]
  775. del self.individual_voice_handlers[user_id]
  776. if replier:
  777. reply = await replier(
  778. bot=self,
  779. **{
  780. name: argument
  781. for name, argument in locals().items()
  782. if name in inspect.signature(
  783. replier
  784. ).parameters
  785. }
  786. )
  787. if reply:
  788. if type(reply) is str:
  789. reply = dict(text=reply)
  790. try:
  791. return await self.reply(update=update, **reply)
  792. except Exception as e:
  793. logging.error(
  794. f"Failed to handle voice message:\n{e}",
  795. exc_info=True
  796. )
  797. return
  798. async def video_note_message_handler(self, update, user_record):
  799. """Handle `video_note` message update."""
  800. logging.info(
  801. "A video_note message update was received, "
  802. "but this handler does nothing yet."
  803. )
  804. async def contact_message_handler(self, update, user_record):
  805. """Handle `contact` message update."""
  806. logging.info(
  807. "A contact message update was received, "
  808. "but this handler does nothing yet."
  809. )
  810. async def location_message_handler(self, update, user_record):
  811. """Handle `location` message update."""
  812. replier, reply = None, None
  813. user_id = update['from']['id'] if 'from' in update else None
  814. if user_id in self.individual_location_handlers:
  815. replier = self.individual_location_handlers[user_id]
  816. del self.individual_location_handlers[user_id]
  817. if replier:
  818. reply = await replier(
  819. bot=self,
  820. **{
  821. name: argument
  822. for name, argument in locals().items()
  823. if name in inspect.signature(
  824. replier
  825. ).parameters
  826. }
  827. )
  828. if reply:
  829. if type(reply) is str:
  830. reply = dict(text=reply)
  831. try:
  832. return await self.reply(update=update, **reply)
  833. except Exception as e:
  834. logging.error(
  835. f"Failed to handle location message:\n{e}",
  836. exc_info=True
  837. )
  838. return
  839. async def venue_message_handler(self, update, user_record):
  840. """Handle `venue` message update."""
  841. logging.info(
  842. "A venue message update was received, "
  843. "but this handler does nothing yet."
  844. )
  845. async def poll_message_handler(self, update, user_record):
  846. """Handle `poll` message update."""
  847. logging.info(
  848. "A poll message update was received, "
  849. "but this handler does nothing yet."
  850. )
  851. async def new_chat_members_message_handler(self, update, user_record):
  852. """Handle `new_chat_members` message update."""
  853. logging.info(
  854. "A new_chat_members message update was received, "
  855. "but this handler does nothing yet."
  856. )
  857. async def left_chat_member_message_handler(self, update, user_record):
  858. """Handle `left_chat_member` message update."""
  859. logging.info(
  860. "A left_chat_member message update was received, "
  861. "but this handler does nothing yet."
  862. )
  863. async def new_chat_title_message_handler(self, update, user_record):
  864. """Handle `new_chat_title` message update."""
  865. logging.info(
  866. "A new_chat_title message update was received, "
  867. "but this handler does nothing yet."
  868. )
  869. async def new_chat_photo_message_handler(self, update, user_record):
  870. """Handle `new_chat_photo` message update."""
  871. logging.info(
  872. "A new_chat_photo message update was received, "
  873. "but this handler does nothing yet."
  874. )
  875. async def delete_chat_photo_message_handler(self, update, user_record):
  876. """Handle `delete_chat_photo` message update."""
  877. logging.info(
  878. "A delete_chat_photo message update was received, "
  879. "but this handler does nothing yet."
  880. )
  881. async def group_chat_created_message_handler(self, update, user_record):
  882. """Handle `group_chat_created` message update."""
  883. logging.info(
  884. "A group_chat_created message update was received, "
  885. "but this handler does nothing yet."
  886. )
  887. async def supergroup_chat_created_message_handler(self, update,
  888. user_record):
  889. """Handle `supergroup_chat_created` message update."""
  890. logging.info(
  891. "A supergroup_chat_created message update was received, "
  892. "but this handler does nothing yet."
  893. )
  894. async def channel_chat_created_message_handler(self, update, user_record):
  895. """Handle `channel_chat_created` message update."""
  896. logging.info(
  897. "A channel_chat_created message update was received, "
  898. "but this handler does nothing yet."
  899. )
  900. async def migrate_to_chat_id_message_handler(self, update, user_record):
  901. """Handle `migrate_to_chat_id` message update."""
  902. logging.info(
  903. "A migrate_to_chat_id message update was received, "
  904. "but this handler does nothing yet."
  905. )
  906. async def migrate_from_chat_id_message_handler(self, update, user_record):
  907. """Handle `migrate_from_chat_id` message update."""
  908. logging.info(
  909. "A migrate_from_chat_id message update was received, "
  910. "but this handler does nothing yet."
  911. )
  912. async def pinned_message_message_handler(self, update, user_record):
  913. """Handle `pinned_message` message update."""
  914. logging.info(
  915. "A pinned_message message update was received, "
  916. "but this handler does nothing yet."
  917. )
  918. async def invoice_message_handler(self, update, user_record):
  919. """Handle `invoice` message update."""
  920. logging.info(
  921. "A invoice message update was received, "
  922. "but this handler does nothing yet."
  923. )
  924. async def successful_payment_message_handler(self, update, user_record):
  925. """Handle `successful_payment` message update."""
  926. logging.info(
  927. "A successful_payment message update was received, "
  928. "but this handler does nothing yet."
  929. )
  930. async def connected_website_message_handler(self, update, user_record):
  931. """Handle `connected_website` message update."""
  932. logging.info(
  933. "A connected_website message update was received, "
  934. "but this handler does nothing yet."
  935. )
  936. async def passport_data_message_handler(self, update, user_record):
  937. """Handle `passport_data` message update."""
  938. logging.info(
  939. "A passport_data message update was received, "
  940. "but this handler does nothing yet."
  941. )
  942. async def dice_handler(self, update, user_record):
  943. """Handle `dice` message update."""
  944. logging.info(
  945. "A dice message update was received, "
  946. "but this handler does nothing yet."
  947. )
  948. # noinspection SpellCheckingInspection
  949. @staticmethod
  950. def split_message_text(text, limit=None, parse_mode='HTML'):
  951. r"""Split text if it hits telegram limits for text messages.
  952. Split at `\n` if possible.
  953. Add a `[...]` at the end and beginning of split messages,
  954. with proper code markdown.
  955. """
  956. if parse_mode == 'HTML':
  957. text = escape_html_chars(text)
  958. tags = (
  959. ('`', '`')
  960. if parse_mode == 'Markdown'
  961. else ('<code>', '</code>')
  962. if parse_mode.lower() == 'html'
  963. else ('', '')
  964. )
  965. if limit is None:
  966. limit = Bot.TELEGRAM_MESSAGES_MAX_LEN - 100
  967. # Example text: "lines\nin\nreversed\order"
  968. text = text.split("\n")[::-1] # ['order', 'reversed', 'in', 'lines']
  969. text_part_number = 0
  970. while len(text) > 0:
  971. temp = []
  972. text_part_number += 1
  973. while (
  974. len(text) > 0
  975. and len(
  976. "\n".join(temp + [text[-1]])
  977. ) < limit
  978. ):
  979. # Append lines of `text` in order (`.pop` returns the last
  980. # line in text) until the addition of the next line would hit
  981. # the `limit`.
  982. temp.append(text.pop())
  983. # If graceful split failed (last line was longer than limit)
  984. if len(temp) == 0:
  985. # Force split last line
  986. temp.append(text[-1][:limit])
  987. text[-1] = text[-1][limit:]
  988. text_chunk = "\n".join(temp) # Re-join this group of lines
  989. prefix, suffix = '', ''
  990. is_last = len(text) == 0
  991. if text_part_number > 1:
  992. prefix = f"{tags[0]}[...]{tags[1]}\n"
  993. if not is_last:
  994. suffix = f"\n{tags[0]}[...]{tags[1]}"
  995. yield (prefix + text_chunk + suffix), is_last
  996. return
  997. async def reply(self, update=None, *args, **kwargs):
  998. """Reply to `update` with proper method according to `kwargs`."""
  999. method = None
  1000. if 'text' in kwargs:
  1001. if 'message_id' in kwargs:
  1002. method = self.edit_message_text
  1003. else:
  1004. method = self.send_message
  1005. elif 'photo' in kwargs:
  1006. method = self.send_photo
  1007. elif 'audio' in kwargs:
  1008. method = self.send_audio
  1009. elif 'voice' in kwargs:
  1010. method = self.send_voice
  1011. if method is not None:
  1012. return await method(update=update, *args, **kwargs)
  1013. raise Exception("Unsupported keyword arguments for `Bot().reply`.")
  1014. async def send_message(self, chat_id=None, text=None,
  1015. parse_mode='HTML',
  1016. disable_web_page_preview=None,
  1017. disable_notification=None,
  1018. reply_to_message_id=None,
  1019. reply_markup=None,
  1020. update=None,
  1021. reply_to_update=False,
  1022. send_default_keyboard=True,
  1023. user_record=None):
  1024. """Send text via message(s).
  1025. This method wraps lower-level `TelegramBot.sendMessage` method.
  1026. Pass an `update` to extract `chat_id` and `message_id` from it.
  1027. Set `reply_to_update` = True to reply to `update['message_id']`.
  1028. Set `send_default_keyboard` = False to avoid sending default keyboard
  1029. as reply_markup (only those messages can be edited, which were
  1030. sent with no reply markup or with an inline keyboard).
  1031. """
  1032. sent_message_update = None
  1033. if update is None:
  1034. update = dict()
  1035. if 'message' in update:
  1036. update = update['message']
  1037. if chat_id is None and 'chat' in update:
  1038. chat_id = self.get_chat_id(update)
  1039. if reply_to_update and 'message_id' in update:
  1040. reply_to_message_id = update['message_id']
  1041. if (
  1042. send_default_keyboard
  1043. and reply_markup is None
  1044. and type(chat_id) is int
  1045. and chat_id > 0
  1046. and text != self.authorization_denied_message
  1047. ):
  1048. reply_markup = self.get_keyboard(
  1049. update=update,
  1050. telegram_id=chat_id
  1051. )
  1052. if not text:
  1053. return
  1054. parse_mode = str(parse_mode)
  1055. if isinstance(text, dict):
  1056. if user_record is None:
  1057. user_record = self.db['users'].find_one(telegram_id=chat_id)
  1058. text = self.get_message(
  1059. update=update,
  1060. user_record=user_record,
  1061. messages=text
  1062. )
  1063. text_chunks = self.split_message_text(
  1064. text=text,
  1065. limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 100,
  1066. parse_mode=parse_mode
  1067. )
  1068. for text_chunk, is_last in text_chunks:
  1069. _reply_markup = (reply_markup if is_last else None)
  1070. sent_message_update = await self.sendMessage(
  1071. chat_id=chat_id,
  1072. text=text_chunk,
  1073. parse_mode=parse_mode,
  1074. disable_web_page_preview=disable_web_page_preview,
  1075. disable_notification=disable_notification,
  1076. reply_to_message_id=reply_to_message_id,
  1077. reply_markup=_reply_markup
  1078. )
  1079. return sent_message_update
  1080. async def send_disposable_message(self, *args, interval=60, **kwargs):
  1081. sent_message = await self.reply(*args, **kwargs)
  1082. if sent_message is None:
  1083. return
  1084. task = self.delete_message(update=sent_message)
  1085. self.final_tasks.append(task)
  1086. await asyncio.sleep(interval)
  1087. await task
  1088. if task in self.final_tasks:
  1089. self.final_tasks.remove(task)
  1090. return
  1091. async def edit_message_text(self, text,
  1092. chat_id=None, message_id=None,
  1093. inline_message_id=None,
  1094. parse_mode='HTML',
  1095. disable_web_page_preview=None,
  1096. reply_markup=None,
  1097. update=None):
  1098. """Edit message text, sending new messages if necessary.
  1099. This method wraps lower-level `TelegramBot.editMessageText` method.
  1100. Pass an `update` to extract a message identifier from it.
  1101. """
  1102. updates = []
  1103. edited_message = None
  1104. if update is not None:
  1105. message_identifier = self.get_message_identifier(update)
  1106. if 'chat_id' in message_identifier:
  1107. chat_id = message_identifier['chat_id']
  1108. message_id = message_identifier['message_id']
  1109. if 'inline_message_id' in message_identifier:
  1110. inline_message_id = message_identifier['inline_message_id']
  1111. if isinstance(text, dict):
  1112. user_record = self.db['users'].find_one(telegram_id=chat_id)
  1113. text = self.get_message(
  1114. update=update,
  1115. user_record=user_record,
  1116. messages=text
  1117. )
  1118. for i, (text_chunk, is_last) in enumerate(
  1119. self.split_message_text(
  1120. text=text,
  1121. limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 200,
  1122. parse_mode=parse_mode
  1123. )
  1124. ):
  1125. if i == 0:
  1126. edited_message = await self.editMessageText(
  1127. text=text_chunk,
  1128. chat_id=chat_id,
  1129. message_id=message_id,
  1130. inline_message_id=inline_message_id,
  1131. parse_mode=parse_mode,
  1132. disable_web_page_preview=disable_web_page_preview,
  1133. reply_markup=(reply_markup if is_last else None)
  1134. )
  1135. if chat_id is None:
  1136. # Cannot send messages without a chat_id
  1137. # Inline keyboards attached to inline query results may be
  1138. # in chats the bot cannot reach.
  1139. break
  1140. updates = [update]
  1141. else:
  1142. updates.append(
  1143. await self.send_message(
  1144. text=text_chunk,
  1145. chat_id=chat_id,
  1146. parse_mode=parse_mode,
  1147. disable_web_page_preview=disable_web_page_preview,
  1148. reply_markup=(reply_markup if is_last else None),
  1149. update=updates[-1],
  1150. reply_to_update=True,
  1151. send_default_keyboard=False
  1152. )
  1153. )
  1154. return edited_message
  1155. async def edit_message_media(self,
  1156. chat_id=None, message_id=None,
  1157. inline_message_id=None,
  1158. media=None,
  1159. reply_markup=None,
  1160. caption=None,
  1161. parse_mode=None,
  1162. photo=None,
  1163. update=None):
  1164. if update is not None:
  1165. message_identifier = self.get_message_identifier(update)
  1166. if 'chat_id' in message_identifier:
  1167. chat_id = message_identifier['chat_id']
  1168. message_id = message_identifier['message_id']
  1169. if 'inline_message_id' in message_identifier:
  1170. inline_message_id = message_identifier['inline_message_id']
  1171. if media is None:
  1172. media = {}
  1173. if caption is not None:
  1174. media['caption'] = caption
  1175. if parse_mode is not None:
  1176. media['parse_mode'] = parse_mode
  1177. if photo is not None:
  1178. media['type'] = 'photo'
  1179. media['media'] = photo
  1180. return await self.editMessageMedia(chat_id=chat_id,
  1181. message_id=message_id,
  1182. inline_message_id=inline_message_id,
  1183. media=media,
  1184. reply_markup=reply_markup)
  1185. async def forward_message(self, chat_id, update=None, from_chat_id=None,
  1186. message_id=None, disable_notification=False):
  1187. """Forward message from `from_chat_id` to `chat_id`.
  1188. Set `disable_notification` to True to avoid disturbing recipient.
  1189. Pass the `update` to be forwarded or its identifier (`from_chat_id` and
  1190. `message_id`).
  1191. """
  1192. if from_chat_id is None or message_id is None:
  1193. message_identifier = self.get_message_identifier(update)
  1194. from_chat_id = message_identifier['chat_id']
  1195. message_id = message_identifier['message_id']
  1196. return await self.forwardMessage(
  1197. chat_id=chat_id,
  1198. from_chat_id=from_chat_id,
  1199. message_id=message_id,
  1200. disable_notification=disable_notification,
  1201. )
  1202. async def delete_message(self, update=None, chat_id=None,
  1203. message_id=None):
  1204. """Delete given update with given *args and **kwargs.
  1205. Please note, that a bot can delete only messages sent by itself
  1206. or sent in a group which it is administrator of.
  1207. """
  1208. if update is None:
  1209. update = dict()
  1210. if chat_id is None or message_id is None:
  1211. message_identifier = self.get_message_identifier(update)
  1212. else:
  1213. message_identifier = dict(
  1214. chat_id=chat_id,
  1215. message_id=message_id
  1216. )
  1217. return await self.deleteMessage(
  1218. **message_identifier
  1219. )
  1220. async def send_photo(self, chat_id=None, photo=None,
  1221. caption=None,
  1222. parse_mode=None,
  1223. disable_notification=None,
  1224. reply_to_message_id=None,
  1225. reply_markup=None,
  1226. update=None,
  1227. reply_to_update=False,
  1228. send_default_keyboard=True,
  1229. use_stored_file_id=True):
  1230. """Send photos.
  1231. This method wraps lower-level `TelegramBot.sendPhoto` method.
  1232. Pass an `update` to extract `chat_id` and `message_id` from it.
  1233. Set `reply_to_update` = True to reply to `update['message_id']`.
  1234. Set `send_default_keyboard` = False to avoid sending default keyboard
  1235. as reply_markup (only those messages can be edited, which were
  1236. sent with no reply markup or with an inline keyboard).
  1237. If photo was already sent by this bot and `use_stored_file_id` is set
  1238. to True, use file_id (it is faster and recommended).
  1239. """
  1240. already_sent = False
  1241. photo_path = None
  1242. if update is None:
  1243. update = dict()
  1244. if 'message' in update:
  1245. update = update['message']
  1246. if chat_id is None and 'chat' in update:
  1247. chat_id = self.get_chat_id(update)
  1248. if reply_to_update and 'message_id' in update:
  1249. reply_to_message_id = update['message_id']
  1250. if (
  1251. send_default_keyboard
  1252. and reply_markup is None
  1253. and type(chat_id) is int
  1254. and chat_id > 0
  1255. and caption != self.authorization_denied_message
  1256. ):
  1257. reply_markup = self.get_keyboard(
  1258. update=update,
  1259. telegram_id=chat_id
  1260. )
  1261. if type(photo) is str:
  1262. photo_path = photo
  1263. with self.db as db:
  1264. already_sent = db['sent_pictures'].find_one(
  1265. path=photo_path,
  1266. errors=False
  1267. )
  1268. if already_sent and use_stored_file_id:
  1269. photo = already_sent['file_id']
  1270. already_sent = True
  1271. else:
  1272. already_sent = False
  1273. if not any(
  1274. [
  1275. photo.startswith(url_starter)
  1276. for url_starter in ('http', 'www',)
  1277. ]
  1278. ): # If `photo` is not a url but a local file path
  1279. try:
  1280. with io.BytesIO() as buffered_picture:
  1281. with open(
  1282. os.path.join(self.path, photo_path),
  1283. 'rb' # Read bytes
  1284. ) as photo_file:
  1285. buffered_picture.write(photo_file.read())
  1286. photo = buffered_picture.getvalue()
  1287. except FileNotFoundError:
  1288. photo = None
  1289. else:
  1290. use_stored_file_id = False
  1291. if photo is None:
  1292. logging.error("Photo is None, `send_photo` returning...")
  1293. return
  1294. sent_update = None
  1295. try:
  1296. sent_update = await self.sendPhoto(
  1297. chat_id=chat_id,
  1298. photo=photo,
  1299. caption=caption,
  1300. parse_mode=parse_mode,
  1301. disable_notification=disable_notification,
  1302. reply_to_message_id=reply_to_message_id,
  1303. reply_markup=reply_markup
  1304. )
  1305. if isinstance(sent_update, Exception):
  1306. raise Exception("sendPhoto API call failed!")
  1307. except Exception as e:
  1308. logging.error(f"Error sending photo\n{e}")
  1309. if already_sent:
  1310. with self.db as db:
  1311. db['sent_pictures'].update(
  1312. dict(
  1313. path=photo_path,
  1314. errors=True
  1315. ),
  1316. ['path']
  1317. )
  1318. if (
  1319. type(sent_update) is dict
  1320. and 'photo' in sent_update
  1321. and len(sent_update['photo']) > 0
  1322. and 'file_id' in sent_update['photo'][0]
  1323. and (not already_sent)
  1324. and use_stored_file_id
  1325. ):
  1326. with self.db as db:
  1327. db['sent_pictures'].insert(
  1328. dict(
  1329. path=photo_path,
  1330. file_id=sent_update['photo'][0]['file_id'],
  1331. errors=False
  1332. )
  1333. )
  1334. return sent_update
  1335. async def send_audio(self, chat_id=None, audio=None,
  1336. caption=None,
  1337. duration=None,
  1338. performer=None,
  1339. title=None,
  1340. thumb=None,
  1341. parse_mode=None,
  1342. disable_notification=None,
  1343. reply_to_message_id=None,
  1344. reply_markup=None,
  1345. update=None,
  1346. reply_to_update=False,
  1347. send_default_keyboard=True,
  1348. use_stored_file_id=True):
  1349. """Send audio files.
  1350. This method wraps lower-level `TelegramBot.sendAudio` method.
  1351. Pass an `update` to extract `chat_id` and `message_id` from it.
  1352. Set `reply_to_update` = True to reply to `update['message_id']`.
  1353. Set `send_default_keyboard` = False to avoid sending default keyboard
  1354. as reply_markup (only those messages can be edited, which were
  1355. sent with no reply markup or with an inline keyboard).
  1356. If photo was already sent by this bot and `use_stored_file_id` is set
  1357. to True, use file_id (it is faster and recommended).
  1358. """
  1359. already_sent = False
  1360. audio_path = None
  1361. if update is None:
  1362. update = dict()
  1363. if 'message' in update:
  1364. update = update['message']
  1365. if chat_id is None and 'chat' in update:
  1366. chat_id = self.get_chat_id(update)
  1367. if reply_to_update and 'message_id' in update:
  1368. reply_to_message_id = update['message_id']
  1369. if (
  1370. send_default_keyboard
  1371. and reply_markup is None
  1372. and type(chat_id) is int
  1373. and chat_id > 0
  1374. and caption != self.authorization_denied_message
  1375. ):
  1376. reply_markup = self.get_keyboard(
  1377. update=update,
  1378. telegram_id=chat_id
  1379. )
  1380. if type(audio) is str:
  1381. audio_path = audio
  1382. with self.db as db:
  1383. already_sent = db['sent_audio_files'].find_one(
  1384. path=audio_path,
  1385. errors=False
  1386. )
  1387. if already_sent and use_stored_file_id:
  1388. audio = already_sent['file_id']
  1389. already_sent = True
  1390. else:
  1391. already_sent = False
  1392. if not any(
  1393. [
  1394. audio.startswith(url_starter)
  1395. for url_starter in ('http', 'www',)
  1396. ]
  1397. ): # If `audio` is not a url but a local file path
  1398. try:
  1399. with io.BytesIO() as buffered_picture:
  1400. with open(
  1401. os.path.join(self.path, audio_path),
  1402. 'rb' # Read bytes
  1403. ) as audio_file:
  1404. buffered_picture.write(audio_file.read())
  1405. audio = buffered_picture.getvalue()
  1406. except FileNotFoundError:
  1407. audio = None
  1408. else:
  1409. use_stored_file_id = False
  1410. if audio is None:
  1411. logging.error("Audio is None, `send_audio` returning...")
  1412. return
  1413. sent_update = None
  1414. try:
  1415. sent_update = await self.sendAudio(
  1416. chat_id=chat_id,
  1417. audio=audio,
  1418. caption=caption,
  1419. duration=duration,
  1420. performer=performer,
  1421. title=title,
  1422. thumb=thumb,
  1423. parse_mode=parse_mode,
  1424. disable_notification=disable_notification,
  1425. reply_to_message_id=reply_to_message_id,
  1426. reply_markup=reply_markup
  1427. )
  1428. if isinstance(sent_update, Exception):
  1429. raise Exception("sendAudio API call failed!")
  1430. except Exception as e:
  1431. logging.error(f"Error sending audio\n{e}")
  1432. if already_sent:
  1433. with self.db as db:
  1434. db['sent_audio_files'].update(
  1435. dict(
  1436. path=audio_path,
  1437. errors=True
  1438. ),
  1439. ['path']
  1440. )
  1441. if (
  1442. type(sent_update) is dict
  1443. and 'audio' in sent_update
  1444. and 'file_id' in sent_update['audio']
  1445. and (not already_sent)
  1446. and use_stored_file_id
  1447. ):
  1448. with self.db as db:
  1449. db['sent_audio_files'].insert(
  1450. dict(
  1451. path=audio_path,
  1452. file_id=sent_update['audio']['file_id'],
  1453. errors=False
  1454. )
  1455. )
  1456. return sent_update
  1457. async def send_voice(self, chat_id=None, voice=None,
  1458. caption=None,
  1459. duration=None,
  1460. parse_mode=None,
  1461. disable_notification=None,
  1462. reply_to_message_id=None,
  1463. reply_markup=None,
  1464. update=None,
  1465. reply_to_update=False,
  1466. send_default_keyboard=True,
  1467. use_stored_file_id=True):
  1468. """Send voice messages.
  1469. This method wraps lower-level `TelegramBot.sendVoice` method.
  1470. Pass an `update` to extract `chat_id` and `message_id` from it.
  1471. Set `reply_to_update` = True to reply to `update['message_id']`.
  1472. Set `send_default_keyboard` = False to avoid sending default keyboard
  1473. as reply_markup (only those messages can be edited, which were
  1474. sent with no reply markup or with an inline keyboard).
  1475. If photo was already sent by this bot and `use_stored_file_id` is set
  1476. to True, use file_id (it is faster and recommended).
  1477. """
  1478. already_sent = False
  1479. voice_path = None
  1480. if update is None:
  1481. update = dict()
  1482. if 'message' in update:
  1483. update = update['message']
  1484. if chat_id is None and 'chat' in update:
  1485. chat_id = self.get_chat_id(update)
  1486. if reply_to_update and 'message_id' in update:
  1487. reply_to_message_id = update['message_id']
  1488. if (
  1489. send_default_keyboard
  1490. and reply_markup is None
  1491. and type(chat_id) is int
  1492. and chat_id > 0
  1493. and caption != self.authorization_denied_message
  1494. ):
  1495. reply_markup = self.get_keyboard(
  1496. update=update,
  1497. telegram_id=chat_id
  1498. )
  1499. if type(voice) is str:
  1500. voice_path = voice
  1501. with self.db as db:
  1502. already_sent = db['sent_voice_messages'].find_one(
  1503. path=voice_path,
  1504. errors=False
  1505. )
  1506. if already_sent and use_stored_file_id:
  1507. voice = already_sent['file_id']
  1508. already_sent = True
  1509. else:
  1510. already_sent = False
  1511. if not any(
  1512. [
  1513. voice.startswith(url_starter)
  1514. for url_starter in ('http', 'www',)
  1515. ]
  1516. ): # If `voice` is not a url but a local file path
  1517. try:
  1518. with io.BytesIO() as buffered_picture:
  1519. with open(
  1520. os.path.join(self.path, voice_path),
  1521. 'rb' # Read bytes
  1522. ) as voice_file:
  1523. buffered_picture.write(voice_file.read())
  1524. voice = buffered_picture.getvalue()
  1525. except FileNotFoundError:
  1526. voice = None
  1527. else:
  1528. use_stored_file_id = False
  1529. if voice is None:
  1530. logging.error("Voice is None, `send_voice` returning...")
  1531. return
  1532. sent_update = None
  1533. try:
  1534. sent_update = await self.sendVoice(
  1535. chat_id=chat_id,
  1536. voice=voice,
  1537. caption=caption,
  1538. duration=duration,
  1539. parse_mode=parse_mode,
  1540. disable_notification=disable_notification,
  1541. reply_to_message_id=reply_to_message_id,
  1542. reply_markup=reply_markup
  1543. )
  1544. if isinstance(sent_update, Exception):
  1545. raise Exception("sendVoice API call failed!")
  1546. except Exception as e:
  1547. logging.error(f"Error sending voice\n{e}")
  1548. if already_sent:
  1549. with self.db as db:
  1550. db['sent_voice_messages'].update(
  1551. dict(
  1552. path=voice_path,
  1553. errors=True
  1554. ),
  1555. ['path']
  1556. )
  1557. if (
  1558. type(sent_update) is dict
  1559. and 'voice' in sent_update
  1560. and 'file_id' in sent_update['voice']
  1561. and (not already_sent)
  1562. and use_stored_file_id
  1563. ):
  1564. with self.db as db:
  1565. db['sent_voice_messages'].insert(
  1566. dict(
  1567. path=voice_path,
  1568. file_id=sent_update['voice']['file_id'],
  1569. errors=False
  1570. )
  1571. )
  1572. return sent_update
  1573. async def send_document(self, chat_id=None, document=None, thumb=None,
  1574. caption=None, parse_mode=None,
  1575. disable_notification=None,
  1576. reply_to_message_id=None, reply_markup=None,
  1577. document_path=None,
  1578. document_name=None,
  1579. update=None,
  1580. reply_to_update=False,
  1581. send_default_keyboard=True,
  1582. use_stored_file_id=False):
  1583. """Send a document.
  1584. This method wraps lower-level `TelegramBot.sendDocument` method.
  1585. Pass an `update` to extract `chat_id` and `message_id` from it.
  1586. Set `reply_to_update` = True to reply to `update['message_id']`.
  1587. Set `send_default_keyboard` = False to avoid sending default keyboard
  1588. as reply_markup (only those messages can be edited, which were
  1589. sent with no reply markup or with an inline keyboard).
  1590. If document was already sent by this bot and `use_stored_file_id` is
  1591. set to True, use file_id (it is faster and recommended).
  1592. `document_path` may contain `{path}`: it will be replaced by
  1593. `self.path`.
  1594. `document_name` displayed to Telegram may differ from actual document
  1595. name if this parameter is set.
  1596. """
  1597. already_sent = False
  1598. if update is None:
  1599. update = dict()
  1600. # This buffered_file trick is necessary for two reasons
  1601. # 1. File operations must be blocking, but sendDocument is a coroutine
  1602. # 2. A `with` statement is not possible here
  1603. # `buffered_file` must be closed at all costs!
  1604. buffered_file = None
  1605. if 'message' in update:
  1606. update = update['message']
  1607. if chat_id is None and 'chat' in update:
  1608. chat_id = self.get_chat_id(update)
  1609. if reply_to_update and 'message_id' in update:
  1610. reply_to_message_id = update['message_id']
  1611. if (
  1612. send_default_keyboard
  1613. and reply_markup is None
  1614. and type(chat_id) is int
  1615. and chat_id > 0
  1616. and caption != self.authorization_denied_message
  1617. ):
  1618. reply_markup = self.get_keyboard(
  1619. update=update,
  1620. telegram_id=chat_id,
  1621. )
  1622. if document_path is not None:
  1623. with self.db as db:
  1624. already_sent = db['sent_documents'].find_one(
  1625. path=document_path,
  1626. errors=False
  1627. )
  1628. if already_sent and use_stored_file_id:
  1629. document = already_sent['file_id']
  1630. already_sent = True
  1631. else:
  1632. already_sent = False
  1633. if not any(
  1634. [
  1635. document_path.startswith(url_starter)
  1636. for url_starter in ('http', 'www',)
  1637. ]
  1638. ): # If `document_path` is not a url but a local file path
  1639. try:
  1640. with open(
  1641. document_path.format(
  1642. path=self.path
  1643. ),
  1644. 'rb' # Read bytes
  1645. ) as file_:
  1646. buffered_file = io.BytesIO(file_.read())
  1647. buffered_file.name = (
  1648. document_name
  1649. or file_.name
  1650. or 'Document'
  1651. )
  1652. document = buffered_file
  1653. except FileNotFoundError as e:
  1654. if buffered_file:
  1655. buffered_file.close()
  1656. return e
  1657. else:
  1658. use_stored_file_id = False
  1659. if document is None:
  1660. logging.error(
  1661. "`document` is None, `send_document` returning..."
  1662. )
  1663. return Exception("No `document` provided")
  1664. sent_update = None
  1665. try:
  1666. sent_update = await self.sendDocument(
  1667. chat_id=chat_id,
  1668. document=document,
  1669. thumb=thumb,
  1670. caption=caption,
  1671. parse_mode=parse_mode,
  1672. disable_notification=disable_notification,
  1673. reply_to_message_id=reply_to_message_id,
  1674. reply_markup=reply_markup
  1675. )
  1676. if isinstance(sent_update, Exception):
  1677. raise Exception("sendDocument API call failed!")
  1678. except Exception as e:
  1679. logging.error(f"Error sending document\n{e}")
  1680. if already_sent:
  1681. with self.db as db:
  1682. db['sent_documents'].update(
  1683. dict(
  1684. path=document_path,
  1685. errors=True
  1686. ),
  1687. ['path']
  1688. )
  1689. finally:
  1690. if buffered_file:
  1691. buffered_file.close()
  1692. if (
  1693. type(sent_update) is dict
  1694. and 'document' in sent_update
  1695. and 'file_id' in sent_update['document']
  1696. and (not already_sent)
  1697. and use_stored_file_id
  1698. ):
  1699. with self.db as db:
  1700. db['sent_documents'].insert(
  1701. dict(
  1702. path=document_path,
  1703. file_id=sent_update['document']['file_id'],
  1704. errors=False
  1705. )
  1706. )
  1707. return sent_update
  1708. async def download_file(self, file_id,
  1709. file_name=None, path=None):
  1710. """Given a telegram `file_id`, download the related file.
  1711. Telegram may not preserve the original file name and MIME type: the
  1712. file's MIME type and name (if available) should be stored when the
  1713. File object is received.
  1714. """
  1715. file = await self.getFile(file_id=file_id)
  1716. if file is None or isinstance(file, Exception):
  1717. logging.error(f"{file}")
  1718. return
  1719. file_bytes = await async_get(
  1720. url=(
  1721. f"https://api.telegram.org/file/"
  1722. f"bot{self.token}/"
  1723. f"{file['file_path']}"
  1724. ),
  1725. mode='raw'
  1726. )
  1727. path = path or self.path
  1728. while file_name is None:
  1729. file_name = get_secure_key(length=10)
  1730. if os.path.exists(f"{path}/{file_name}"):
  1731. file_name = None
  1732. try:
  1733. with open(f"{path}/{file_name}", 'wb') as local_file:
  1734. local_file.write(file_bytes)
  1735. except Exception as e:
  1736. logging.error(f"File download failed due to {e}")
  1737. return
  1738. def translate_inline_query_answer_result(self, record,
  1739. update=None, user_record=None):
  1740. """Translate title and message text fields of inline query result.
  1741. This method does not alter original `record`. This way, default
  1742. inline query result is kept multilingual although single results
  1743. sent to users are translated.
  1744. """
  1745. result = dict()
  1746. for key, val in record.items():
  1747. if key == 'title' and isinstance(record[key], dict):
  1748. result[key] = self.get_message(
  1749. update=update,
  1750. user_record=user_record,
  1751. messages=record[key]
  1752. )
  1753. elif (
  1754. key == 'input_message_content'
  1755. and isinstance(record[key], dict)
  1756. ):
  1757. result[key] = self.translate_inline_query_answer_result(
  1758. record[key],
  1759. update=update,
  1760. user_record=user_record
  1761. )
  1762. elif key == 'message_text' and isinstance(record[key], dict):
  1763. result[key] = self.get_message(
  1764. update=update,
  1765. user_record=user_record,
  1766. messages=record[key]
  1767. )
  1768. else:
  1769. result[key] = val
  1770. return result
  1771. async def answer_inline_query(self,
  1772. inline_query_id=None,
  1773. results=None,
  1774. cache_time=None,
  1775. is_personal=None,
  1776. next_offset=None,
  1777. switch_pm_text=None,
  1778. switch_pm_parameter=None,
  1779. update=None,
  1780. user_record=None):
  1781. """Answer inline queries.
  1782. This method wraps lower-level `answerInlineQuery` method.
  1783. If `results` is a string, cast it to proper type (list of dicts having
  1784. certain keys). See utilities.make_inline_query_answer for details.
  1785. """
  1786. if results is None:
  1787. results = []
  1788. if (
  1789. inline_query_id is None
  1790. and isinstance(update, dict)
  1791. and 'id' in update
  1792. ):
  1793. inline_query_id = update['id']
  1794. results = [
  1795. self.translate_inline_query_answer_result(record=result,
  1796. update=update,
  1797. user_record=user_record)
  1798. for result in make_inline_query_answer(results)
  1799. ]
  1800. return await self.answerInlineQuery(
  1801. inline_query_id=inline_query_id,
  1802. results=results,
  1803. cache_time=cache_time,
  1804. is_personal=is_personal,
  1805. next_offset=next_offset,
  1806. switch_pm_text=switch_pm_text,
  1807. switch_pm_parameter=switch_pm_parameter,
  1808. )
  1809. @classmethod
  1810. def set_class_maintenance_message(cls, maintenance_message):
  1811. """Set class maintenance message.
  1812. It will be returned if bot is under maintenance, unless and instance
  1813. `_maintenance_message` is set.
  1814. """
  1815. cls._maintenance_message = maintenance_message
  1816. def set_maintenance_message(self, maintenance_message):
  1817. """Set instance maintenance message.
  1818. It will be returned if bot is under maintenance.
  1819. If instance message is None, default class message is used.
  1820. """
  1821. self._maintenance_message = maintenance_message
  1822. def change_maintenance_status(self, maintenance_message=None, status=None):
  1823. """Put the bot under maintenance or end it.
  1824. While in maintenance, bot will reply to users with maintenance_message
  1825. with a few exceptions.
  1826. If status is not set, it is by default the opposite of the current one.
  1827. Optionally, `maintenance_message` may be set.
  1828. """
  1829. if status is None:
  1830. status = not self.under_maintenance
  1831. assert type(status) is bool, "status must be a boolean value!"
  1832. self._under_maintenance = status
  1833. if maintenance_message:
  1834. self.set_maintenance_message(maintenance_message)
  1835. return self._under_maintenance # Return new status
  1836. def is_allowed_during_maintenance(self, update):
  1837. """Return True if update is allowed during maintenance.
  1838. An update is allowed if any of the criteria in
  1839. `self.allowed_during_maintenance` returns True called on it.
  1840. """
  1841. for criterion in self.allowed_during_maintenance:
  1842. if criterion(update):
  1843. return True
  1844. return False
  1845. def allow_during_maintenance(self, criterion):
  1846. """Add a criterion to allow certain updates during maintenance.
  1847. `criterion` must be a function taking a Telegram `update` dictionary
  1848. and returning a boolean.
  1849. ```# Example of criterion
  1850. def allow_text_messages(update):
  1851. if 'message' in update and 'text' in update['message']:
  1852. return True
  1853. return False
  1854. ```
  1855. """
  1856. self._allowed_during_maintenance.append(criterion)
  1857. async def handle_update_during_maintenance(self, update, user_record=None):
  1858. """Handle an update while bot is under maintenance.
  1859. Handle all types of updates.
  1860. """
  1861. if (
  1862. 'message' in update
  1863. and 'chat' in update['message']
  1864. and update['message']['chat']['id'] > 0
  1865. ):
  1866. return await self.send_message(
  1867. text=self.maintenance_message,
  1868. update=update['message'],
  1869. reply_to_update=True
  1870. )
  1871. elif 'callback_query' in update:
  1872. await self.answerCallbackQuery(
  1873. callback_query_id=update['id'],
  1874. text=remove_html_tags(self.maintenance_message[:45])
  1875. )
  1876. elif 'inline_query' in update:
  1877. await self.answer_inline_query(
  1878. update['inline_query']['id'],
  1879. self.maintenance_message,
  1880. cache_time=30,
  1881. is_personal=False,
  1882. update=update,
  1883. user_record=user_record
  1884. )
  1885. return
  1886. @classmethod
  1887. def set_class_authorization_denied_message(cls, message):
  1888. """Set class authorization denied message.
  1889. It will be returned if user is unauthorized to make a request.
  1890. """
  1891. cls._authorization_denied_message = message
  1892. def set_authorization_denied_message(self, message):
  1893. """Set instance authorization denied message.
  1894. If instance message is None, default class message is used.
  1895. """
  1896. self._authorization_denied_message = message
  1897. def set_authorization_function(self, authorization_function):
  1898. """Set a custom authorization_function.
  1899. It should evaluate True if user is authorized to perform a specific
  1900. action and False otherwise.
  1901. It should take update and role and return a Boolean.
  1902. Default authorization_function always evaluates True.
  1903. """
  1904. self.authorization_function = authorization_function
  1905. @classmethod
  1906. def set_class_unknown_command_message(cls, unknown_command_message):
  1907. """Set class unknown command message.
  1908. It will be returned if user sends an unknown command in private chat.
  1909. """
  1910. cls._unknown_command_message = unknown_command_message
  1911. def set_unknown_command_message(self, unknown_command_message):
  1912. """Set instance unknown command message.
  1913. It will be returned if user sends an unknown command in private chat.
  1914. If instance message is None, default class message is used.
  1915. """
  1916. self._unknown_command_message = unknown_command_message
  1917. def add_help_section(self, help_section):
  1918. """Add `help_section`."""
  1919. assert (
  1920. isinstance(help_section, dict)
  1921. and 'name' in help_section
  1922. and 'label' in help_section
  1923. and 'description' in help_section
  1924. ), "Invalid help section!"
  1925. if 'authorization_level' not in help_section:
  1926. help_section['authorization_level'] = 'admin'
  1927. self.messages['help_sections'][help_section['name']] = help_section
  1928. def command(self, command, aliases=None, reply_keyboard_button=None,
  1929. show_in_keyboard=False, description="",
  1930. help_section=None,
  1931. authorization_level='admin'):
  1932. """Associate a bot command with a custom handler function.
  1933. Decorate command handlers like this:
  1934. ```
  1935. @bot.command('/my_command', ['Button'], True, "My command", 'user')
  1936. async def command_handler(bot, update, user_record):
  1937. return "Result"
  1938. ```
  1939. When a message text starts with `/command[@bot_name]`, or with an
  1940. alias, it gets passed to the decorated function.
  1941. `command` is the command name (with or without /).
  1942. `aliases` is a list of aliases; each will call the command handler
  1943. function; the first alias will appear as button in
  1944. reply keyboard if `reply_keyboard_button` is not set.
  1945. `reply_keyboard_button` is a str or better dict of language-specific
  1946. strings to be shown in default keyboard.
  1947. `show_in_keyboard`, if True, makes a button for this command appear in
  1948. default keyboard.
  1949. `description` can be used to help users understand what `/command`
  1950. does.
  1951. `help_section` is a dict on which the corresponding help section is
  1952. built. It may provide multilanguage support and should be
  1953. structured as follows:
  1954. {
  1955. "label": { # It will be displayed as button label
  1956. 'en': "Label",
  1957. ...
  1958. },
  1959. "name": "section_name",
  1960. # If missing, `authorization_level` is used
  1961. "authorization_level": "everybody",
  1962. "description": {
  1963. 'en': "Description in English",
  1964. ...
  1965. },
  1966. }
  1967. `authorization_level` is the lowest authorization level needed to run
  1968. the command.
  1969. """
  1970. if not isinstance(command, str):
  1971. raise TypeError(f'Command `{command}` is not a string')
  1972. if isinstance(reply_keyboard_button, dict):
  1973. for button in reply_keyboard_button.values():
  1974. if button not in aliases:
  1975. aliases.append(button)
  1976. if aliases:
  1977. if not isinstance(aliases, list):
  1978. raise TypeError(f'Aliases is not a list: `{aliases}`')
  1979. if not all(
  1980. [
  1981. isinstance(alias, str)
  1982. for alias in aliases
  1983. ]
  1984. ):
  1985. raise TypeError(
  1986. f'Aliases {aliases} is not a list of strings string'
  1987. )
  1988. if isinstance(help_section, dict):
  1989. if 'authorization_level' not in help_section:
  1990. help_section['authorization_level'] = authorization_level
  1991. self.add_help_section(help_section)
  1992. command = command.strip('/ ').lower()
  1993. def command_decorator(command_handler):
  1994. async def decorated_command_handler(bot, update, user_record):
  1995. logging.info(
  1996. f"Command `{command}@{bot.name}` called by "
  1997. f"`{update['from'] if 'from' in update else update['chat']}`"
  1998. )
  1999. if bot.authorization_function(
  2000. update=update,
  2001. user_record=user_record,
  2002. authorization_level=authorization_level
  2003. ):
  2004. # Pass supported arguments from locals() to command_handler
  2005. return await command_handler(
  2006. **{
  2007. name: argument
  2008. for name, argument in locals().items()
  2009. if name in inspect.signature(
  2010. command_handler
  2011. ).parameters
  2012. }
  2013. )
  2014. return dict(text=self.authorization_denied_message)
  2015. self.commands[command] = dict(
  2016. handler=decorated_command_handler,
  2017. description=description,
  2018. authorization_level=authorization_level
  2019. )
  2020. if type(description) is dict:
  2021. self.messages['commands'][command] = dict(
  2022. description=description
  2023. )
  2024. if aliases:
  2025. for alias in aliases:
  2026. if alias.startswith('/'):
  2027. self.commands[alias.strip('/ ').lower()] = dict(
  2028. handler=decorated_command_handler,
  2029. authorization_level=authorization_level
  2030. )
  2031. else:
  2032. self.command_aliases[alias] = decorated_command_handler
  2033. if show_in_keyboard and (aliases or reply_keyboard_button):
  2034. _reply_keyboard_button = reply_keyboard_button or aliases[0]
  2035. self.messages[
  2036. 'reply_keyboard_buttons'][
  2037. command] = _reply_keyboard_button
  2038. self.commands[command][
  2039. 'reply_keyboard_button'] = _reply_keyboard_button
  2040. return command_decorator
  2041. def parser(self, condition, description='', authorization_level='admin',
  2042. argument='text'):
  2043. """Define a text message parser.
  2044. Decorate command handlers like this:
  2045. ```
  2046. def custom_criteria(update):
  2047. return 'from' in update
  2048. @bot.parser(custom_criteria, authorization_level='user')
  2049. async def text_parser(bot, update, user_record):
  2050. return "Result"
  2051. ```
  2052. If condition evaluates True when run on a message text
  2053. (not starting with '/'), such decorated function gets
  2054. called on update.
  2055. Conditions of parsers are evaluated in order; when one is True,
  2056. others will be skipped.
  2057. `description` provides information about the parser.
  2058. `authorization_level` is the lowest authorization level needed to call
  2059. the parser.
  2060. """
  2061. if not callable(condition):
  2062. raise TypeError(
  2063. f'Condition {condition.__name__} is not a callable'
  2064. )
  2065. def parser_decorator(parser):
  2066. async def decorated_parser(bot, update, user_record):
  2067. logging.info(
  2068. f"Text message update matching condition "
  2069. f"`{condition.__name__}@{bot.name}` from "
  2070. f"`{update['from'] if 'from' in update else update['chat']}`"
  2071. )
  2072. if bot.authorization_function(
  2073. update=update,
  2074. user_record=user_record,
  2075. authorization_level=authorization_level
  2076. ):
  2077. # Pass supported arguments from locals() to parser
  2078. return await parser(
  2079. **{
  2080. name: arg
  2081. for name, arg in locals().items()
  2082. if name in inspect.signature(parser).parameters
  2083. }
  2084. )
  2085. return dict(text=bot.authorization_denied_message)
  2086. self.text_message_parsers[condition] = dict(
  2087. handler=decorated_parser,
  2088. description=description,
  2089. authorization_level=authorization_level,
  2090. argument=argument
  2091. )
  2092. return parser_decorator
  2093. def set_command(self, command, handler, aliases=None,
  2094. reply_keyboard_button=None, show_in_keyboard=False,
  2095. description="",
  2096. authorization_level='admin'):
  2097. """Associate a `command` with a `handler`.
  2098. When a message text starts with `/command[@bot_name]`, or with an
  2099. alias, it gets passed to the decorated function.
  2100. `command` is the command name (with or without /)
  2101. `handler` is the function to be called on update objects.
  2102. `aliases` is a list of aliases; each will call the command handler
  2103. function; the first alias will appear as button in
  2104. reply keyboard if `reply_keyboard_button` is not set.
  2105. `reply_keyboard_button` is a str or better dict of language-specific
  2106. strings to be shown in default keyboard.
  2107. `show_in_keyboard`, if True, makes a button for this command appear in
  2108. default keyboard.
  2109. `description` is a description and can be used to help users understand
  2110. what `/command` does.
  2111. `authorization_level` is the lowest authorization level needed to run
  2112. the command.
  2113. """
  2114. if not callable(handler):
  2115. raise TypeError(f'Handler `{handler}` is not callable.')
  2116. return self.command(
  2117. command=command, aliases=aliases,
  2118. reply_keyboard_button=reply_keyboard_button,
  2119. show_in_keyboard=show_in_keyboard, description=description,
  2120. authorization_level=authorization_level
  2121. )(handler)
  2122. def button(self, prefix, separator=None, description='',
  2123. authorization_level='admin'):
  2124. """Associate a bot button `prefix` with a handler.
  2125. When a callback data text starts with `prefix`, the associated handler
  2126. is called upon the update.
  2127. Decorate button handlers like this:
  2128. ```
  2129. @bot.button('a_prefix:///', description="A button",
  2130. authorization_level='user')
  2131. async def button_handler(bot, update, user_record, data):
  2132. return "Result"
  2133. ```
  2134. `separator` will be used to parse callback data received when a button
  2135. starting with `prefix` will be pressed.
  2136. `description` contains information about the button.
  2137. `authorization_level` is the lowest authorization level needed to
  2138. be allowed to push the button.
  2139. """
  2140. if not isinstance(prefix, str):
  2141. raise TypeError(
  2142. f'Inline button callback_data {prefix} is not a string'
  2143. )
  2144. def button_decorator(handler):
  2145. async def decorated_button_handler(bot, update, user_record):
  2146. logging.info(
  2147. f"Button `{update['data']}`@{bot.name} pressed by "
  2148. f"`{update['from']}`"
  2149. )
  2150. if bot.authorization_function(
  2151. update=update,
  2152. user_record=user_record,
  2153. authorization_level=authorization_level
  2154. ):
  2155. # Remove `prefix` from `data`
  2156. data = extract(update['data'], prefix)
  2157. # If a specific separator or default separator is set,
  2158. # use it to split `data` string in a list.
  2159. # Cast numeric `data` elements to `int`.
  2160. _separator = separator or self.callback_data_separator
  2161. if _separator:
  2162. data = [
  2163. int(element) if element.isnumeric()
  2164. else element
  2165. for element in data.split(_separator)
  2166. ]
  2167. # Pass supported arguments from locals() to handler
  2168. return await handler(
  2169. **{
  2170. name: argument
  2171. for name, argument in locals().items()
  2172. if name in inspect.signature(handler).parameters
  2173. }
  2174. )
  2175. return bot.authorization_denied_message
  2176. self.callback_handlers[prefix] = dict(
  2177. handler=decorated_button_handler,
  2178. description=description,
  2179. authorization_level=authorization_level
  2180. )
  2181. return button_decorator
  2182. def query(self, condition, description='', authorization_level='admin'):
  2183. """Define an inline query.
  2184. Decorator: `@bot.query(example)`
  2185. When an inline query matches the `condition` function,
  2186. decorated function is called and passed the query update object
  2187. as argument.
  2188. `description` is a description
  2189. `authorization_level` is the lowest authorization level needed to run
  2190. the command
  2191. """
  2192. if not callable(condition):
  2193. raise TypeError(
  2194. 'Condition {c} is not a callable'.format(
  2195. c=condition.__name__
  2196. )
  2197. )
  2198. def query_decorator(handler):
  2199. async def decorated_query_handler(bot, update, user_record):
  2200. logging.info(
  2201. f"Inline query matching condition "
  2202. f"`{condition.__name__}@{bot.name}` from "
  2203. f"`{update['from']}`"
  2204. )
  2205. if self.authorization_function(
  2206. update=update,
  2207. user_record=user_record,
  2208. authorization_level=authorization_level
  2209. ):
  2210. # Pass supported arguments from locals() to handler
  2211. return await handler(
  2212. **{
  2213. name: argument
  2214. for name, argument in locals().items()
  2215. if name in inspect.signature(handler).parameters
  2216. }
  2217. )
  2218. return self.authorization_denied_message
  2219. self.inline_query_handlers[condition] = dict(
  2220. handler=decorated_query_handler,
  2221. description=description,
  2222. authorization_level=authorization_level
  2223. )
  2224. return query_decorator
  2225. def set_chat_id_getter(self, getter):
  2226. """Set chat_id getter.
  2227. It must be a function that takes an update and returns the proper
  2228. chat_id.
  2229. """
  2230. assert callable(getter), "Chat id getter must be a function!"
  2231. self.get_chat_id = getter
  2232. @staticmethod
  2233. def get_user_identifier(user_id=None, update=None):
  2234. """Get telegram id of user given an update.
  2235. Result itself may be passed as either parameter (for backward
  2236. compatibility).
  2237. """
  2238. identifier = user_id or update
  2239. assert identifier is not None, (
  2240. "Provide a user_id or update object to get a user identifier."
  2241. )
  2242. if (
  2243. isinstance(identifier, dict)
  2244. and 'message' in identifier
  2245. and 'from' not in identifier
  2246. ):
  2247. identifier = identifier['message']
  2248. if isinstance(identifier, dict) and 'from' in identifier:
  2249. identifier = identifier['from']['id']
  2250. assert type(identifier) is int, (
  2251. f"Unable to find a user identifier. Got `{identifier}`"
  2252. )
  2253. return identifier
  2254. @staticmethod
  2255. def get_message_identifier(update=None):
  2256. """Get a message identifier dictionary to edit `update`.
  2257. Pass the result as keyword arguments to `edit...` API methods.
  2258. """
  2259. if update is None:
  2260. update = dict()
  2261. if 'message' in update:
  2262. update = update['message']
  2263. if 'chat' in update and 'message_id' in update:
  2264. return dict(
  2265. chat_id=update['chat']['id'],
  2266. message_id=update['message_id']
  2267. )
  2268. elif 'inline_message_id' in update:
  2269. return dict(
  2270. inline_message_id=update['inline_message_id']
  2271. )
  2272. def set_individual_text_message_handler(self, handler,
  2273. update=None, user_id=None):
  2274. """Set a custom text message handler for the user.
  2275. Any text message update from the user will be handled by this custom
  2276. handler instead of default handlers for commands, aliases and text.
  2277. Custom handlers last one single use, but they can call this method and
  2278. set themselves as next custom text message handler.
  2279. """
  2280. identifier = self.get_user_identifier(
  2281. user_id=user_id,
  2282. update=update
  2283. )
  2284. assert callable(handler), (f"Handler `{handler.name}` is not "
  2285. "callable. Custom text message handler "
  2286. "could not be set.")
  2287. self.individual_text_message_handlers[identifier] = handler
  2288. return
  2289. def remove_individual_text_message_handler(self,
  2290. update=None, user_id=None):
  2291. """Remove a custom text message handler for the user.
  2292. Any text message update from the user will be handled by default
  2293. handlers for commands, aliases and text.
  2294. """
  2295. identifier = self.get_user_identifier(
  2296. user_id=user_id,
  2297. update=update
  2298. )
  2299. if identifier in self.individual_text_message_handlers:
  2300. del self.individual_text_message_handlers[identifier]
  2301. return
  2302. def set_individual_location_handler(self, handler,
  2303. update=None, user_id=None):
  2304. """Set a custom location handler for the user.
  2305. Any location update from the user will be handled by this custom
  2306. handler instead of default handlers for locations.
  2307. Custom handlers last one single use, but they can call this method and
  2308. set themselves as next custom handler.
  2309. """
  2310. identifier = self.get_user_identifier(
  2311. user_id=user_id,
  2312. update=update
  2313. )
  2314. assert callable(handler), (f"Handler `{handler.name}` is not "
  2315. "callable. Custom location handler "
  2316. "could not be set.")
  2317. self.individual_location_handlers[identifier] = handler
  2318. return
  2319. def remove_individual_location_handler(self,
  2320. update=None, user_id=None):
  2321. """Remove a custom location handler for the user.
  2322. Any location message update from the user will be handled by default
  2323. handlers for locations.
  2324. """
  2325. identifier = self.get_user_identifier(
  2326. user_id=user_id,
  2327. update=update
  2328. )
  2329. if identifier in self.individual_location_handlers:
  2330. del self.individual_location_handlers[identifier]
  2331. return
  2332. def set_individual_voice_handler(self, handler,
  2333. update=None, user_id=None):
  2334. """Set a custom voice message handler for the user.
  2335. Any voice message update from the user will be handled by this custom
  2336. handler instead of default handlers for voice messages.
  2337. Custom handlers last one single use, but they can call this method and
  2338. set themselves as next custom handler.
  2339. """
  2340. identifier = self.get_user_identifier(
  2341. user_id=user_id,
  2342. update=update
  2343. )
  2344. assert callable(handler), (f"Handler `{handler.name}` is not "
  2345. "callable. Custom voice handler "
  2346. "could not be set.")
  2347. self.individual_voice_handlers[identifier] = handler
  2348. return
  2349. def remove_individual_voice_handler(self,
  2350. update=None, user_id=None):
  2351. """Remove a custom voice handler for the user.
  2352. Any voice message update from the user will be handled by default
  2353. handlers for voice messages.
  2354. """
  2355. identifier = self.get_user_identifier(
  2356. user_id=user_id,
  2357. update=update
  2358. )
  2359. if identifier in self.individual_voice_handlers:
  2360. del self.individual_voice_handlers[identifier]
  2361. return
  2362. def set_placeholder(self, chat_id,
  2363. text=None, sent_message=None, timeout=1):
  2364. """Set a placeholder chat action or text message.
  2365. If it takes the bot more than `timeout` to answer, send a placeholder
  2366. message or a `is typing` chat action.
  2367. `timeout` may be expressed in seconds (int) or datetime.timedelta
  2368. This method returns a `request_id`. When the calling function has
  2369. performed its task, it must set to 1 the value of
  2370. `self.placeholder_requests[request_id]`.
  2371. If this value is still 0 at `timeout`, the placeholder is sent.
  2372. Otherwise, no action is performed.
  2373. """
  2374. request_id = len(self.placeholder_requests)
  2375. self.placeholder_requests[request_id] = 0
  2376. asyncio.ensure_future(
  2377. self.placeholder_effector(
  2378. request_id=request_id,
  2379. timeout=timeout,
  2380. chat_id=chat_id,
  2381. sent_message=sent_message,
  2382. text=text
  2383. )
  2384. )
  2385. return request_id
  2386. async def placeholder_effector(self, request_id, timeout, chat_id,
  2387. sent_message=None, text=None):
  2388. """Send a placeholder chat action or text message if needed.
  2389. If it takes the bot more than `timeout` to answer, send a placeholder
  2390. message or a `is typing` chat action.
  2391. `timeout` may be expressed in seconds (int) or datetime.timedelta
  2392. """
  2393. if type(timeout) is datetime.timedelta:
  2394. timeout = timeout.total_seconds()
  2395. await asyncio.sleep(timeout)
  2396. if not self.placeholder_requests[request_id]:
  2397. if sent_message and text:
  2398. await self.edit_message_text(
  2399. update=sent_message,
  2400. text=text,
  2401. )
  2402. else:
  2403. await self.sendChatAction(
  2404. chat_id=chat_id,
  2405. action='typing'
  2406. )
  2407. return
  2408. async def webhook_feeder(self, request):
  2409. """Handle incoming HTTP `request`s.
  2410. Get data, feed webhook and return and OK message.
  2411. """
  2412. update = await request.json()
  2413. asyncio.ensure_future(
  2414. self.route_update(update)
  2415. )
  2416. return web.Response(
  2417. body='OK'.encode('utf-8')
  2418. )
  2419. async def get_me(self):
  2420. """Get bot information.
  2421. Restart bots if bot can't be got.
  2422. """
  2423. try:
  2424. me = await self.getMe()
  2425. if isinstance(me, Exception):
  2426. raise me
  2427. elif me is None:
  2428. raise Exception('getMe returned None')
  2429. self._name = me["username"]
  2430. self._telegram_id = me['id']
  2431. except Exception as e:
  2432. logging.error(
  2433. f"API getMe method failed, information about this bot could "
  2434. f"not be retrieved. Restarting in 5 minutes...\n\n"
  2435. f"Error information:\n{e}"
  2436. )
  2437. await asyncio.sleep(5*60)
  2438. self.__class__.stop(
  2439. message="Information about this bot could not be retrieved.\n"
  2440. "Restarting...",
  2441. final_state=65
  2442. )
  2443. def setup(self):
  2444. """Make bot ask for updates and handle responses."""
  2445. if not self.webhook_url:
  2446. asyncio.ensure_future(self.get_updates())
  2447. else:
  2448. asyncio.ensure_future(self.set_webhook())
  2449. self.__class__.app.router.add_route(
  2450. 'POST', self.webhook_local_address, self.webhook_feeder
  2451. )
  2452. asyncio.ensure_future(self.update_users())
  2453. async def close_sessions(self):
  2454. """Close open sessions."""
  2455. for session_name, session in self.sessions.items():
  2456. if not session.closed:
  2457. await session.close()
  2458. async def set_webhook(self, url=None, certificate=None,
  2459. max_connections=None, allowed_updates=None):
  2460. """Set a webhook if token is valid."""
  2461. # Return if token is invalid
  2462. await self.get_me()
  2463. if self.name is None:
  2464. return
  2465. if allowed_updates is None:
  2466. allowed_updates = []
  2467. if certificate is None:
  2468. certificate = self.certificate
  2469. if max_connections is None:
  2470. max_connections = self.max_connections
  2471. if url is None:
  2472. url = self.webhook_url
  2473. webhook_was_set = await self.setWebhook(
  2474. url=url, certificate=certificate, max_connections=max_connections,
  2475. allowed_updates=allowed_updates
  2476. ) # `setWebhook` API method returns `True` on success
  2477. webhook_information = await self.getWebhookInfo()
  2478. webhook_information['url'] = webhook_information['url'].replace(
  2479. self.token, "<BOT_TOKEN>"
  2480. ).replace(
  2481. self.session_token, "<SESSION_TOKEN>"
  2482. )
  2483. if webhook_was_set:
  2484. logging.info(
  2485. f"Webhook was set correctly.\n"
  2486. f"Webhook information: {webhook_information}"
  2487. )
  2488. else:
  2489. logging.error(
  2490. f"Failed to set webhook!\n"
  2491. f"Webhook information: {webhook_information}"
  2492. )
  2493. async def get_updates(self, timeout=30, limit=100, allowed_updates=None,
  2494. error_cooldown=10):
  2495. """Get updates using long polling.
  2496. timeout : int
  2497. Timeout set for Telegram servers. Make sure that connection timeout
  2498. is greater than `timeout`.
  2499. limit : int (1 - 100)
  2500. Max number of updates to be retrieved.
  2501. allowed_updates : List(str)
  2502. List of update types to be retrieved.
  2503. Empty list to allow all updates.
  2504. None to fallback to class default.
  2505. """
  2506. # Return if token is invalid
  2507. await self.get_me()
  2508. if self.name is None:
  2509. return
  2510. # Set custom list of allowed updates or fallback to class default list
  2511. if allowed_updates is None:
  2512. allowed_updates = self.allowed_updates
  2513. await self.deleteWebhook() # Remove eventually active webhook
  2514. update = None # Do not update offset if no update is received
  2515. while True:
  2516. updates = await self.getUpdates(
  2517. offset=self._offset,
  2518. timeout=timeout,
  2519. limit=limit,
  2520. allowed_updates=allowed_updates
  2521. )
  2522. if updates is None:
  2523. continue
  2524. elif isinstance(updates, TelegramError):
  2525. logging.error(
  2526. f"Waiting {error_cooldown} seconds before trying again..."
  2527. )
  2528. await asyncio.sleep(error_cooldown)
  2529. continue
  2530. elif isinstance(updates, Exception):
  2531. logging.error(
  2532. "Unexpected exception. "
  2533. f"Waiting {error_cooldown} seconds before trying again..."
  2534. )
  2535. await asyncio.sleep(error_cooldown)
  2536. continue
  2537. for update in updates:
  2538. asyncio.ensure_future(self.route_update(update))
  2539. if update is not None:
  2540. self._offset = update['update_id'] + 1
  2541. async def update_users(self, interval=60):
  2542. """Every `interval` seconds, store news about bot users.
  2543. Compare `update['from']` data with records in `users` table and keep
  2544. track of differences in `users_history` table.
  2545. """
  2546. while 1:
  2547. await asyncio.sleep(interval)
  2548. # Iterate through a copy since asyncio.sleep(0) is awaited at each
  2549. # cycle iteration.
  2550. for telegram_id, user in self.recent_users.copy().items():
  2551. new_record = dict()
  2552. with self.db as db:
  2553. user_record = db['users'].find_one(telegram_id=telegram_id)
  2554. for key in [
  2555. 'first_name',
  2556. 'last_name',
  2557. 'username',
  2558. 'language_code'
  2559. ]:
  2560. new_record[key] = (user[key] if key in user else None)
  2561. if (
  2562. (
  2563. key not in user_record
  2564. or new_record[key] != user_record[key]
  2565. )
  2566. # Exclude fake updates
  2567. and 'notes' not in user
  2568. ):
  2569. db['users_history'].insert(
  2570. dict(
  2571. until=datetime.datetime.now(),
  2572. user_id=user_record['id'],
  2573. field=key,
  2574. value=(
  2575. user_record[key]
  2576. if key in user_record
  2577. else None
  2578. )
  2579. )
  2580. )
  2581. db['users'].update(
  2582. {
  2583. 'id': user_record['id'],
  2584. key: new_record[key]
  2585. },
  2586. ['id'],
  2587. ensure=True
  2588. )
  2589. if telegram_id in self.recent_users:
  2590. del self.recent_users[telegram_id]
  2591. await asyncio.sleep(0)
  2592. def get_user_record(self, update):
  2593. """Get user_record of update sender.
  2594. If user is unknown add them.
  2595. If update has no `from` field, return None.
  2596. If user data changed, ensure that this event gets stored.
  2597. """
  2598. if 'from' not in update or 'id' not in update['from']:
  2599. return
  2600. telegram_id = update['from']['id']
  2601. with self.db as db:
  2602. user_record = db['users'].find_one(
  2603. telegram_id=telegram_id
  2604. )
  2605. if user_record is None:
  2606. new_user = dict(
  2607. telegram_id=telegram_id,
  2608. privileges=100,
  2609. selected_language_code=None
  2610. )
  2611. for key in [
  2612. 'first_name',
  2613. 'last_name',
  2614. 'username',
  2615. 'language_code'
  2616. ]:
  2617. new_user[key] = (
  2618. update['from'][key]
  2619. if key in update['from']
  2620. else None
  2621. )
  2622. db['users'].insert(new_user)
  2623. user_record = db['users'].find_one(
  2624. telegram_id=telegram_id
  2625. )
  2626. elif (
  2627. telegram_id not in self.recent_users
  2628. and 'notes' not in update['from'] # Exclude fake updates
  2629. ):
  2630. self.recent_users[telegram_id] = update['from']
  2631. return user_record
  2632. def set_router(self, event, handler):
  2633. """Set `handler` as router for `event`."""
  2634. self.routing_table[event] = handler
  2635. async def route_update(self, update):
  2636. """Pass `update` to proper method.
  2637. Update objects have two keys:
  2638. - `update_id` (which is used as offset while retrieving new updates)
  2639. - One and only one of the following
  2640. `message`
  2641. `edited_message`
  2642. `channel_post`
  2643. `edited_channel_post`
  2644. `inline_query`
  2645. `chosen_inline_result`
  2646. `callback_query`
  2647. `shipping_query`
  2648. `pre_checkout_query`
  2649. `poll`
  2650. """
  2651. if (
  2652. self.under_maintenance
  2653. and not self.is_allowed_during_maintenance(update)
  2654. ):
  2655. return await self.handle_update_during_maintenance(update)
  2656. for key, value in update.items():
  2657. if key in self.routing_table:
  2658. user_record = self.get_user_record(update=value)
  2659. return await self.routing_table[key](
  2660. update=value,
  2661. user_record=user_record
  2662. )
  2663. logging.error(f"Unknown type of update.\n{update}")
  2664. def additional_task(self, when='BEFORE', *args, **kwargs):
  2665. """Add a task before at app start or cleanup.
  2666. Decorate an async function to have it awaited `BEFORE` or `AFTER` main
  2667. loop.
  2668. """
  2669. when = when[0].lower()
  2670. def additional_task_decorator(task):
  2671. if when == 'b':
  2672. self.preliminary_tasks.append(task(*args, **kwargs))
  2673. elif when == 'a':
  2674. self.final_tasks.append(task(*args, **kwargs))
  2675. return additional_task_decorator
  2676. @classmethod
  2677. async def start_app(cls):
  2678. """Start running `aiohttp.web.Application`.
  2679. It will route webhook-received updates and other custom paths.
  2680. """
  2681. assert cls.local_host is not None, "Invalid local host"
  2682. assert cls.port is not None, "Invalid port"
  2683. cls.runner = web.AppRunner(cls.app)
  2684. await cls.runner.setup()
  2685. cls.server = web.TCPSite(cls.runner, cls.local_host, cls.port)
  2686. try:
  2687. await cls.server.start()
  2688. except OSError as e:
  2689. logging.error(e)
  2690. raise KeyboardInterrupt("Unable to start web app.")
  2691. logging.info(f"App running at http://{cls.local_host}:{cls.port}")
  2692. @classmethod
  2693. async def stop_app(cls):
  2694. """Close bot sessions and cleanup."""
  2695. for bot in cls.bots:
  2696. await asyncio.gather(
  2697. *bot.final_tasks
  2698. )
  2699. await bot.close_sessions()
  2700. await cls.runner.cleanup()
  2701. @classmethod
  2702. def stop(cls, message, final_state=0):
  2703. """Log a final `message`, stop loop and set exiting `code`.
  2704. All bots and the web app will be terminated gracefully.
  2705. The final state may be retrieved to get information about what stopped
  2706. the bots.
  2707. """
  2708. logging.info(message)
  2709. cls.final_state = final_state
  2710. cls.loop.stop()
  2711. return
  2712. @classmethod
  2713. def run(cls, local_host=None, port=None):
  2714. """Run aiohttp web app and all Bot instances.
  2715. Each bot will receive updates via long polling or webhook according to
  2716. its initialization parameters.
  2717. A single aiohttp.web.Application instance will be run (cls.app) on
  2718. local_host:port and it may serve custom-defined routes as well.
  2719. """
  2720. if local_host is not None:
  2721. cls.local_host = local_host
  2722. if port is not None:
  2723. cls.port = port
  2724. try:
  2725. cls.loop.run_until_complete(
  2726. asyncio.gather(
  2727. *[
  2728. preliminary_task
  2729. for bot in cls.bots
  2730. for preliminary_task in bot.preliminary_tasks
  2731. ]
  2732. )
  2733. )
  2734. except Exception as e:
  2735. logging.error(f"{e}", exc_info=True)
  2736. for bot in cls.bots:
  2737. bot.setup()
  2738. asyncio.ensure_future(cls.start_app())
  2739. try:
  2740. cls.loop.run_forever()
  2741. except KeyboardInterrupt:
  2742. logging.info("Stopped by KeyboardInterrupt")
  2743. except Exception as e:
  2744. logging.error(f"{e}", exc_info=True)
  2745. finally:
  2746. cls.loop.run_until_complete(cls.stop_app())
  2747. return cls.final_state
  2748. def set_role_class(self, role):
  2749. """Set a Role class for bot.
  2750. `role` must be an instance of `authorization.Role`.
  2751. """
  2752. self.Role = role