Queer European MD passionate about IT

bot.py 145 KB

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