Queer European MD passionate about IT

custombot.py 77 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080
  1. """Subclass of third party telepot.aio.Bot, providing the following features.
  2. - It prevents hitting Telegram flood limits by waiting
  3. between text and photo messages.
  4. - It provides command, parser, button and other decorators to associate
  5. common Telegram actions with custom handlers.
  6. - It supports multiple bots running in the same script
  7. and allows communications between them
  8. as well as complete independency from each other.
  9. - Each bot is associated with a sqlite database
  10. using dataset, a third party library.
  11. Please note that you need Python3.5+ to run async code
  12. Check requirements.txt for third party dependencies.
  13. """
  14. # Standard library modules
  15. import asyncio
  16. import datetime
  17. import io
  18. import logging
  19. import os
  20. # Third party modules
  21. import dataset
  22. from davteutil.utilities import (
  23. Gettable, escape_html_chars, get_cleaned_text,
  24. line_drawing_unordered_list, make_lines_of_buttons, markdown_check, MyOD,
  25. pick_most_similar_from_list, remove_html_tags, sleep_until
  26. )
  27. import telepot
  28. import telepot.aio
  29. def split_text_gracefully(text, limit, parse_mode):
  30. r"""Split text if it hits telegram limits for text messages.
  31. Split at `\n` if possible.
  32. Add a `[...]` at the end and beginning of split messages,
  33. with proper code markdown.
  34. """
  35. text = text.split("\n")[::-1]
  36. result = []
  37. while len(text) > 0:
  38. temp = []
  39. while len(text) > 0 and len("\n".join(temp + [text[-1]])) < limit:
  40. temp.append(text.pop())
  41. if len(temp) == 0:
  42. temp.append(text[-1][:limit])
  43. text[-1] = text[-1][limit:]
  44. result.append("\n".join(temp))
  45. if len(result) > 1:
  46. for i in range(1, len(result)):
  47. result[i] = "{tag[0]}[...]{tag[1]}\n{text}".format(
  48. tag=(
  49. ('`', '`') if parse_mode == 'Markdown'
  50. else ('<code>', '</code>') if parse_mode.lower() == 'html'
  51. else ('', '')
  52. ),
  53. text=result[i]
  54. )
  55. result[i-1] = "{text}\n{tag[0]}[...]{tag[1]}".format(
  56. tag=(
  57. ('`', '`') if parse_mode == 'Markdown'
  58. else ('<code>', '</code>') if parse_mode.lower() == 'html'
  59. else ('', '')
  60. ),
  61. text=result[i-1]
  62. )
  63. return result
  64. def make_inline_query_answer(answer):
  65. """Return an article-type answer to inline query.
  66. Takes either a string or a dictionary and returns a list.
  67. """
  68. if type(answer) is str:
  69. answer = dict(
  70. type='article',
  71. id=0,
  72. title=remove_html_tags(answer),
  73. input_message_content=dict(
  74. message_text=answer,
  75. parse_mode='HTML'
  76. )
  77. )
  78. if type(answer) is dict:
  79. answer = [answer]
  80. return answer
  81. class Bot(telepot.aio.Bot, Gettable):
  82. """telepot.aio.Bot (async Telegram bot framework) convenient subclass.
  83. === General functioning ===
  84. - While Bot.run() coroutine is executed, HTTP get requests are made
  85. to Telegram servers asking for new messages for each Bot instance.
  86. - Each message causes the proper Bot instance method coroutine
  87. to be awaited, according to its flavour (see routing_table)
  88. -- For example, chat messages cause `Bot().on_chat_message(message)`
  89. to be awaited.
  90. - This even-processing coroutine ensures the proper handling function
  91. a future and returns.
  92. -- That means that simpler tasks are completed before slower ones,
  93. since handling functions are not awaited but scheduled
  94. by `asyncio.ensure_future(handling_function(...))`
  95. -- For example, chat text messages are handled by
  96. `handle_text_message`, which looks for the proper function
  97. to elaborate the request (in bot's commands and parsers)
  98. - The handling function evaluates an answer, depending on the message
  99. content, and eventually provides a reply
  100. -- For example, `handle_text_message` sends its
  101. answer via `send_message`
  102. - All Bot.instances run simultaneously and faster requests
  103. are completed earlier.
  104. - All uncaught events are ignored.
  105. """
  106. instances = {}
  107. stop = False
  108. # Cooldown time between sent messages, to prevent hitting
  109. # Telegram flood limits
  110. # Current limits: 30 total messages sent per second,
  111. # 1 message per second per chat, 20 messages per minute per group
  112. COOLDOWN_TIME_ABSOLUTE = datetime.timedelta(seconds=1/30)
  113. COOLDOWN_TIME_PER_CHAT = datetime.timedelta(seconds=1)
  114. MAX_GROUP_MESSAGES_PER_MINUTE = 20
  115. # Max length of text field for a Telegram message (UTF-8 text)
  116. TELEGRAM_MESSAGES_MAX_LEN = 4096
  117. _path = '.'
  118. _unauthorized_message = None
  119. _unknown_command_message = None
  120. _maintenance_message = None
  121. _default_inline_query_answer = [
  122. dict(
  123. type='article',
  124. id=0,
  125. title="I cannot answer this query, sorry",
  126. input_message_content=dict(
  127. message_text="I'm sorry "
  128. "but I could not find an answer for your query."
  129. )
  130. )
  131. ]
  132. def __init__(self, token, db_name=None):
  133. """Instantiate Bot instance, given a token and a db name."""
  134. super().__init__(token)
  135. self.routing_table = {
  136. 'chat': self.on_chat_message,
  137. 'inline_query': self.on_inline_query,
  138. 'chosen_inline_result': self.on_chosen_inline_result,
  139. 'callback_query': self.on_callback_query
  140. }
  141. self.chat_message_handlers = {
  142. 'text': self.handle_text_message,
  143. 'pinned_message': self.handle_pinned_message,
  144. 'photo': self.handle_photo_message
  145. }
  146. if db_name:
  147. self._db_url = 'sqlite:///{name}{ext}'.format(
  148. name=db_name,
  149. ext='.db' if not db_name.endswith('.db') else ''
  150. )
  151. else:
  152. self._db_url = None
  153. self._unauthorized_message = None
  154. self.authorization_function = lambda update, authorization_level: True
  155. self.get_chat_id = lambda update: (
  156. update['message']['chat']['id']
  157. if 'message' in update
  158. else update['chat']['id']
  159. )
  160. self.commands = dict()
  161. self.callback_handlers = dict()
  162. self.inline_query_handlers = MyOD()
  163. self._default_inline_query_answer = None
  164. self.chosen_inline_result_handlers = dict()
  165. self.aliases = MyOD()
  166. self.parsers = MyOD()
  167. self.custom_parsers = dict()
  168. self.custom_photo_parsers = dict()
  169. self.bot_name = None
  170. self.default_reply_keyboard_elements = []
  171. self._default_keyboard = dict()
  172. self.run_before_loop = []
  173. self.run_after_loop = []
  174. self.to_be_obscured = []
  175. self.to_be_destroyed = []
  176. self.last_sending_time = dict(
  177. absolute=(
  178. datetime.datetime.now()
  179. - self.__class__.COOLDOWN_TIME_ABSOLUTE
  180. )
  181. )
  182. self._maintenance = False
  183. self._maintenance_message = None
  184. self.chat_actions = dict(
  185. pinned=MyOD()
  186. )
  187. @property
  188. def name(self):
  189. """Bot name."""
  190. return self.bot_name
  191. @property
  192. def path(self):
  193. """custombot.py file path."""
  194. return self.__class__._path
  195. @property
  196. def db_url(self):
  197. """Return complete path to database."""
  198. return self._db_url
  199. @property
  200. def db(self):
  201. """Connect to bot's database.
  202. It must be used inside a with statement: `with bot.db as db`
  203. """
  204. if self.db_url:
  205. return dataset.connect(self.db_url)
  206. @property
  207. def default_keyboard(self):
  208. """Get the default keyboard.
  209. It is sent when reply_markup is left blank and chat is private.
  210. """
  211. return self._default_keyboard
  212. @property
  213. def default_inline_query_answer(self):
  214. """Answer to be returned if inline query returned None."""
  215. if self._default_inline_query_answer:
  216. return self._default_inline_query_answer
  217. return self.__class__._default_inline_query_answer
  218. @property
  219. def unauthorized_message(self):
  220. """Return this if user is unauthorized to make a request.
  221. If instance message is not set, class message is returned.
  222. """
  223. if self._unauthorized_message:
  224. return self._unauthorized_message
  225. return self.__class__._unauthorized_message
  226. @property
  227. def unknown_command_message(self):
  228. """Message to be returned if user sends an unknown command in private chat.
  229. If instance message is not set, class message is returned.
  230. """
  231. if self._unknown_command_message:
  232. return self._unknown_command_message
  233. return self.__class__._unknown_command_message
  234. @property
  235. def maintenance(self):
  236. """Check whether bot is under maintenance.
  237. While under maintenance, bot will reply with
  238. `self.maintenance_message` to any request, with few exceptions.
  239. """
  240. return self._maintenance
  241. @property
  242. def maintenance_message(self):
  243. """Message to be returned if bot is under maintenance.
  244. If instance message is not set, class message is returned.
  245. """
  246. if self._maintenance_message:
  247. return self._maintenance_message
  248. if self.__class__.maintenance_message:
  249. return self.__class__._maintenance_message
  250. return "Bot is currently under maintenance! Retry later please."
  251. @classmethod
  252. def set_class_path(csl, path):
  253. """Set class path, where files will be looked for.
  254. For example, if send_photo receives `photo='mypic.png'`,
  255. it will parse it as `'{path}/mypic.png'.format(path=self.path)`
  256. """
  257. csl._path = path
  258. @classmethod
  259. def set_class_unauthorized_message(csl, unauthorized_message):
  260. """Set class unauthorized message.
  261. It will be returned if user is unauthorized to make a request.
  262. """
  263. csl._unauthorized_message = unauthorized_message
  264. @classmethod
  265. def set_class_unknown_command_message(cls, unknown_command_message):
  266. """Set class unknown command message.
  267. It will be returned if user sends an unknown command in private chat.
  268. """
  269. cls._unknown_command_message = unknown_command_message
  270. @classmethod
  271. def set_class_maintenance_message(cls, maintenance_message):
  272. """Set class maintenance message.
  273. It will be returned if bot is under maintenance.
  274. """
  275. cls._maintenance_message = maintenance_message
  276. @classmethod
  277. def set_class_default_inline_query_answer(cls,
  278. default_inline_query_answer):
  279. """Set class default inline query answer.
  280. It will be returned if an inline query returned no answer.
  281. """
  282. cls._default_inline_query_answer = default_inline_query_answer
  283. def set_unauthorized_message(self, unauthorized_message):
  284. """Set instance unauthorized message.
  285. If instance message is None, default class message is used.
  286. """
  287. self._unauthorized_message = unauthorized_message
  288. def set_unknown_command_message(self, unknown_command_message):
  289. """Set instance unknown command message.
  290. It will be returned if user sends an unknown command in private chat.
  291. If instance message is None, default class message is used.
  292. """
  293. self._unknown_command_message = unknown_command_message
  294. def set_maintenance_message(self, maintenance_message):
  295. """Set instance maintenance message.
  296. It will be returned if bot is under maintenance.
  297. If instance message is None, default class message is used.
  298. """
  299. self._maintenance_message = maintenance_message
  300. def set_default_inline_query_answer(self, default_inline_query_answer):
  301. """Set a custom default_inline_query_answer.
  302. It will be returned when no answer is found for an inline query.
  303. If instance answer is None, default class answer is used.
  304. """
  305. if type(default_inline_query_answer) in (str, dict):
  306. default_inline_query_answer = make_inline_query_answer(
  307. default_inline_query_answer
  308. )
  309. if type(default_inline_query_answer) is not list:
  310. return 1
  311. self._default_inline_query_answer = default_inline_query_answer
  312. return 0
  313. def set_maintenance(self, maintenance_message):
  314. """Put the bot under maintenance or ends it.
  315. While in maintenance, bot will reply to users with maintenance_message.
  316. Bot will accept /coma, /stop and /restart commands from admins.
  317. """
  318. self._maintenance = not self.maintenance
  319. if maintenance_message:
  320. self.set_maintenance_message(maintenance_message)
  321. if self.maintenance:
  322. return (
  323. "<i>Bot has just been put under maintenance!</i>\n\n"
  324. "Until further notice, it will reply to users "
  325. "with the following message:\n\n{}"
  326. ).format(
  327. self.maintenance_message
  328. )
  329. return "<i>Maintenance ended!</i>"
  330. def set_authorization_function(self, authorization_function):
  331. """Set a custom authorization_function.
  332. It should evaluate True if user is authorized to perform
  333. a specific action and False otherwise.
  334. It should take update and role and return a Boolean.
  335. Default authorization_function always evaluates True.
  336. """
  337. self.authorization_function = authorization_function
  338. def set_get_chat_id_function(self, get_chat_id_function):
  339. """Set a custom get_chat_id function.
  340. It should take and update and return the chat in which
  341. a reply should be sent.
  342. For instance, a bot could reply in private to group messages
  343. as a default behaviour.
  344. Default chat_id returned is current chat id.
  345. """
  346. self.get_chat_id = get_chat_id_function
  347. async def avoid_flooding(self, chat_id):
  348. """asyncio-sleep until COOLDOWN_TIME (per_chat and absolute) has passed.
  349. To prevent hitting Telegram flood limits, send_message and
  350. send_photo await this function.
  351. """
  352. if type(chat_id) is int and chat_id > 0:
  353. while (
  354. datetime.datetime.now() < (
  355. self.last_sending_time['absolute']
  356. + self.__class__.COOLDOWN_TIME_ABSOLUTE
  357. )
  358. ) or (
  359. chat_id in self.last_sending_time
  360. and (
  361. datetime.datetime.now() < (
  362. self.last_sending_time[chat_id]
  363. + self.__class__.COOLDOWN_TIME_PER_CHAT
  364. )
  365. )
  366. ):
  367. await asyncio.sleep(
  368. self.__class__.COOLDOWN_TIME_ABSOLUTE.seconds
  369. )
  370. self.last_sending_time[chat_id] = datetime.datetime.now()
  371. else:
  372. while (
  373. datetime.datetime.now() < (
  374. self.last_sending_time['absolute']
  375. + self.__class__.COOLDOWN_TIME_ABSOLUTE
  376. )
  377. ) or (
  378. chat_id in self.last_sending_time
  379. and len(
  380. [
  381. sending_datetime
  382. for sending_datetime in self.last_sending_time[chat_id]
  383. if sending_datetime >= (
  384. datetime.datetime.now()
  385. - datetime.timedelta(minutes=1)
  386. )
  387. ]
  388. ) >= self.__class__.MAX_GROUP_MESSAGES_PER_MINUTE
  389. ) or (
  390. chat_id in self.last_sending_time
  391. and len(self.last_sending_time[chat_id]) > 0
  392. and datetime.datetime.now() < (
  393. self.last_sending_time[chat_id][-1]
  394. + self.__class__.COOLDOWN_TIME_PER_CHAT
  395. )
  396. ):
  397. await asyncio.sleep(0.5)
  398. if chat_id not in self.last_sending_time:
  399. self.last_sending_time[chat_id] = []
  400. self.last_sending_time[chat_id].append(datetime.datetime.now())
  401. self.last_sending_time[chat_id] = [
  402. sending_datetime
  403. for sending_datetime in self.last_sending_time[chat_id]
  404. if sending_datetime >= (
  405. datetime.datetime.now()
  406. - datetime.timedelta(minutes=1)
  407. )
  408. ]
  409. self.last_sending_time['absolute'] = datetime.datetime.now()
  410. return
  411. async def on_inline_query(self, update):
  412. """Schedule handling of received inline queries.
  413. Notice that handling is only scheduled, not awaited.
  414. This means that all Bot instances may now handle other requests
  415. before this one is completed.
  416. """
  417. asyncio.ensure_future(self.handle_inline_query(update))
  418. return
  419. async def on_chosen_inline_result(self, update):
  420. """Schedule handling of received chosen inline result events.
  421. Notice that handling is only scheduled, not awaited.
  422. This means that all Bot instances may now handle other requests
  423. before this one is completed.
  424. """
  425. asyncio.ensure_future(self.handle_chosen_inline_result(update))
  426. return
  427. async def on_callback_query(self, update):
  428. """Schedule handling of received callback queries.
  429. A callback query is sent when users press inline keyboard buttons.
  430. Bad clients may send malformed or deceiving callback queries:
  431. never use secret keys in buttons and always check request validity!
  432. Notice that handling is only scheduled, not awaited.
  433. This means that all Bot instances may now handle other requests
  434. before this one is completed.
  435. """
  436. # Reject malformed updates lacking of data field
  437. if 'data' not in update:
  438. return
  439. asyncio.ensure_future(self.handle_callback_query(update))
  440. return
  441. async def on_chat_message(self, update):
  442. """Schedule handling of received chat message.
  443. Notice that handling is only scheduled, not awaited.
  444. According to update type, the corresponding handler is
  445. scheduled (see self.chat_message_handlers).
  446. This means that all Bot instances may now handle other
  447. requests before this one is completed.
  448. """
  449. answer = None
  450. content_type, chat_type, chat_id = telepot.glance(
  451. update,
  452. flavor='chat',
  453. long=False
  454. )
  455. if content_type in self.chat_message_handlers:
  456. answer = asyncio.ensure_future(
  457. self.chat_message_handlers[content_type](update)
  458. )
  459. else:
  460. answer = None
  461. logging.debug("Unhandled message")
  462. return answer
  463. async def handle_inline_query(self, update):
  464. """Handle inline query and answer it with results, or log errors."""
  465. query = update['query']
  466. answer = None
  467. switch_pm_text, switch_pm_parameter = None, None
  468. if self.maintenance:
  469. answer = self.maintenance_message
  470. else:
  471. for condition, handler in self.inline_query_handlers.items():
  472. answerer = handler['function']
  473. if condition(update['query']):
  474. if asyncio.iscoroutinefunction(answerer):
  475. answer = await answerer(update)
  476. else:
  477. answer = answerer(update)
  478. break
  479. if not answer:
  480. answer = self.default_inline_query_answer
  481. if type(answer) is dict:
  482. if 'switch_pm_text' in answer:
  483. switch_pm_text = answer['switch_pm_text']
  484. if 'switch_pm_parameter' in answer:
  485. switch_pm_parameter = answer['switch_pm_parameter']
  486. answer = answer['answer']
  487. if type(answer) is str:
  488. answer = make_inline_query_answer(answer)
  489. try:
  490. await self.answerInlineQuery(
  491. update['id'],
  492. answer,
  493. cache_time=10,
  494. is_personal=True,
  495. switch_pm_text=switch_pm_text,
  496. switch_pm_parameter=switch_pm_parameter
  497. )
  498. except Exception as e:
  499. logging.info("Error answering inline query\n{}".format(e))
  500. return
  501. async def handle_chosen_inline_result(self, update):
  502. """When an inline query result is chosen, perform an action.
  503. If chosen inline result id is in self.chosen_inline_result_handlers,
  504. call the related function passing the update as argument.
  505. """
  506. user_id = update['from']['id'] if 'from' in update else None
  507. if self.maintenance:
  508. return
  509. if user_id in self.chosen_inline_result_handlers:
  510. result_id = update['result_id']
  511. handlers = self.chosen_inline_result_handlers[user_id]
  512. if result_id in handlers:
  513. func = handlers[result_id]
  514. if asyncio.iscoroutinefunction(func):
  515. await func(update)
  516. else:
  517. func(update)
  518. return
  519. def set_inline_result_handler(self, user_id, result_id, func):
  520. """Associate a func to a result_id.
  521. When an inline result is chosen having that id, function will
  522. be passed the update as argument.
  523. """
  524. if type(user_id) is dict:
  525. user_id = user_id['from']['id']
  526. assert type(user_id) is int, "user_id must be int!"
  527. # Query result ids are parsed as str by telegram
  528. result_id = str(result_id)
  529. assert callable(func), "func must be a callable"
  530. if user_id not in self.chosen_inline_result_handlers:
  531. self.chosen_inline_result_handlers[user_id] = {}
  532. self.chosen_inline_result_handlers[user_id][result_id] = func
  533. return
  534. async def handle_callback_query(self, update):
  535. """Answer callback queries.
  536. Call the callback handler associated to the query prefix.
  537. The answer is used to edit the source message or send new ones
  538. if text is longer than single message limit.
  539. Anyway, the query is answered, otherwise the client would hang and
  540. the bot would look like idle.
  541. """
  542. answer = None
  543. if self.maintenance:
  544. answer = remove_html_tags(self.maintenance_message[:45])
  545. else:
  546. data = update['data']
  547. for start_text, handler in self.callback_handlers.items():
  548. answerer = handler['function']
  549. if data.startswith(start_text):
  550. if asyncio.iscoroutinefunction(answerer):
  551. answer = await answerer(update)
  552. else:
  553. answer = answerer(update)
  554. break
  555. if answer:
  556. if type(answer) is str:
  557. answer = {'text': answer}
  558. if type(answer) is not dict:
  559. return
  560. if 'edit' in answer:
  561. if 'message' in update:
  562. message_identifier = telepot.message_identifier(
  563. update['message']
  564. )
  565. else:
  566. message_identifier = telepot.message_identifier(update)
  567. edit = answer['edit']
  568. reply_markup = (
  569. edit['reply_markup']
  570. if 'reply_markup' in edit
  571. else None
  572. )
  573. text = (
  574. edit['text']
  575. if 'text' in edit
  576. else None
  577. )
  578. caption = (
  579. edit['caption']
  580. if 'caption' in edit
  581. else None
  582. )
  583. parse_mode = (
  584. edit['parse_mode']
  585. if 'parse_mode' in edit
  586. else None
  587. )
  588. disable_web_page_preview = (
  589. edit['disable_web_page_preview']
  590. if 'disable_web_page_preview' in edit
  591. else None
  592. )
  593. try:
  594. if 'text' in edit:
  595. if (
  596. len(text)
  597. > self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 200
  598. ):
  599. if 'from' in update:
  600. await self.send_message(
  601. chat_id=update['from']['id'],
  602. text=text,
  603. reply_markup=reply_markup,
  604. parse_mode=parse_mode,
  605. disable_web_page_preview=(
  606. disable_web_page_preview
  607. )
  608. )
  609. else:
  610. await self.editMessageText(
  611. msg_identifier=message_identifier,
  612. text=text,
  613. parse_mode=parse_mode,
  614. disable_web_page_preview=(
  615. disable_web_page_preview
  616. ),
  617. reply_markup=reply_markup
  618. )
  619. elif 'caption' in edit:
  620. await self.editMessageCaption(
  621. msg_identifier=message_identifier,
  622. caption=caption,
  623. reply_markup=reply_markup
  624. )
  625. elif 'reply_markup' in edit:
  626. await self.editMessageReplyMarkup(
  627. msg_identifier=message_identifier,
  628. reply_markup=reply_markup
  629. )
  630. except Exception as e:
  631. logging.info("Message was not modified:\n{}".format(e))
  632. text = answer['text'][:180] if 'text' in answer else None
  633. show_alert = (
  634. answer['show_alert']
  635. if 'show_alert' in answer
  636. else None
  637. )
  638. cache_time = (
  639. answer['cache_time']
  640. if 'cache_time' in answer
  641. else None
  642. )
  643. try:
  644. await self.answerCallbackQuery(
  645. callback_query_id=update['id'],
  646. text=text,
  647. show_alert=show_alert,
  648. cache_time=cache_time
  649. )
  650. except telepot.exception.TelegramError as e:
  651. logging.error(e)
  652. else:
  653. try:
  654. await self.answerCallbackQuery(callback_query_id=update['id'])
  655. except telepot.exception.TelegramError as e:
  656. logging.error(e)
  657. return
  658. async def handle_text_message(self, update):
  659. """Answer to chat text messages.
  660. 1) Ignore bot name (case-insensitive) and search bot custom parsers,
  661. commands, aliases and parsers for an answerer.
  662. 2) Get an answer from answerer(update).
  663. 3) Send it to the user.
  664. """
  665. answerer, answer = None, None
  666. # Lower text and replace only bot's tag,
  667. # meaning that `/command@OtherBot` will be ignored.
  668. text = update['text'].lower().replace(
  669. '@{}'.format(
  670. self.name.lower()
  671. ),
  672. ''
  673. )
  674. user_id = update['from']['id'] if 'from' in update else None
  675. if self.maintenance and not any(
  676. text.startswith(x)
  677. for x in ('/coma', '/restart')
  678. ):
  679. if update['chat']['id'] > 0:
  680. answer = self.maintenance_message
  681. elif user_id in self.custom_parsers:
  682. answerer = self.custom_parsers[user_id]
  683. del self.custom_parsers[user_id]
  684. elif text.startswith('/'):
  685. command = text.split()[0].strip(' /@')
  686. if command in self.commands:
  687. answerer = self.commands[command]['function']
  688. elif update['chat']['id'] > 0:
  689. answer = self.unknown_command_message
  690. else:
  691. # If text starts with an alias
  692. # Aliases are case insensitive: text and alias are both .lower()
  693. for alias, parser in self.aliases.items():
  694. if text.startswith(alias.lower()):
  695. answerer = parser
  696. break
  697. # If update matches any parser
  698. for check_function, parser in self.parsers.items():
  699. if (
  700. parser['argument'] == 'text'
  701. and check_function(text)
  702. ) or (
  703. parser['argument'] == 'update'
  704. and check_function(update)
  705. ):
  706. answerer = parser['function']
  707. break
  708. if answerer:
  709. if asyncio.iscoroutinefunction(answerer):
  710. answer = await answerer(update)
  711. else:
  712. answer = answerer(update)
  713. if answer:
  714. try:
  715. return await self.send_message(answer=answer, chat_id=update)
  716. except Exception as e:
  717. logging.error(
  718. "Failed to process answer:\n{}".format(e),
  719. exc_info=True
  720. )
  721. async def handle_pinned_message(self, update):
  722. """Handle pinned message chat action."""
  723. if self.maintenance:
  724. return
  725. answerer = None
  726. for criteria, handler in self.chat_actions['pinned'].items():
  727. if criteria(update):
  728. answerer = handler['function']
  729. break
  730. if answerer is None:
  731. return
  732. elif asyncio.iscoroutinefunction(answerer):
  733. answer = await answerer(update)
  734. else:
  735. answer = answerer(update)
  736. if answer:
  737. try:
  738. return await self.send_message(
  739. answer=answer,
  740. chat_id=update['chat']['id']
  741. )
  742. except Exception as e:
  743. logging.error(
  744. "Failed to process answer:\n{}".format(
  745. e
  746. ),
  747. exc_info=True
  748. )
  749. return
  750. async def handle_photo_message(self, update):
  751. """Handle photo chat message."""
  752. user_id = update['from']['id'] if 'from' in update else None
  753. answerer, answer = None, None
  754. if self.maintenance:
  755. if update['chat']['id'] > 0:
  756. answer = self.maintenance_message
  757. elif user_id in self.custom_photo_parsers:
  758. answerer = self.custom_photo_parsers[user_id]
  759. del self.custom_photo_parsers[user_id]
  760. if answerer:
  761. if asyncio.iscoroutinefunction(answerer):
  762. answer = await answerer(update)
  763. else:
  764. answer = answerer(update)
  765. if answer:
  766. try:
  767. return await self.send_message(answer=answer, chat_id=update)
  768. except Exception as e:
  769. logging.error(
  770. "Failed to process answer:\n{}".format(
  771. e
  772. ),
  773. exc_info=True
  774. )
  775. return
  776. def set_custom_parser(self, parser, update=None, user=None):
  777. """Set a custom parser for the user.
  778. Any chat message update coming from the user will be handled by
  779. this custom parser instead of default parsers (commands, aliases
  780. and text parsers).
  781. Custom parsers last one single use, but their handler can call this
  782. function to provide multiple tries.
  783. """
  784. if user and type(user) is int:
  785. pass
  786. elif type(update) is int:
  787. user = update
  788. elif type(user) is dict:
  789. user = (
  790. user['from']['id']
  791. if 'from' in user
  792. and 'id' in user['from']
  793. else None
  794. )
  795. elif not user and type(update) is dict:
  796. user = (
  797. update['from']['id']
  798. if 'from' in update
  799. and 'id' in update['from']
  800. else None
  801. )
  802. else:
  803. raise TypeError(
  804. 'Invalid user.\nuser: {}\nupdate: {}'.format(
  805. user,
  806. update
  807. )
  808. )
  809. if not type(user) is int:
  810. raise TypeError(
  811. 'User {} is not an int id'.format(
  812. user
  813. )
  814. )
  815. if not callable(parser):
  816. raise TypeError(
  817. 'Parser {} is not a callable'.format(
  818. parser.__name__
  819. )
  820. )
  821. self.custom_parsers[user] = parser
  822. return
  823. def set_custom_photo_parser(self, parser, update=None, user=None):
  824. """Set a custom photo parser for the user.
  825. Any photo chat update coming from the user will be handled by
  826. this custom parser instead of default parsers.
  827. Custom photo parsers last one single use, but their handler can
  828. call this function to provide multiple tries.
  829. """
  830. if user and type(user) is int:
  831. pass
  832. elif type(update) is int:
  833. user = update
  834. elif type(user) is dict:
  835. user = (
  836. user['from']['id']
  837. if 'from' in user
  838. and 'id' in user['from']
  839. else None
  840. )
  841. elif not user and type(update) is dict:
  842. user = (
  843. update['from']['id']
  844. if 'from' in update
  845. and 'id' in update['from']
  846. else None
  847. )
  848. else:
  849. raise TypeError(
  850. 'Invalid user.\nuser: {}\nupdate: {}'.format(
  851. user,
  852. update
  853. )
  854. )
  855. if not type(user) is int:
  856. raise TypeError(
  857. 'User {} is not an int id'.format(
  858. user
  859. )
  860. )
  861. if not callable(parser):
  862. raise TypeError(
  863. 'Parser {} is not a callable'.format(
  864. parser.__name__
  865. )
  866. )
  867. self.custom_photo_parsers[user] = parser
  868. return
  869. def command(self, command, aliases=None, show_in_keyboard=False,
  870. descr="", auth='admin'):
  871. """Define a bot command.
  872. Decorator: `@bot.command(*args)`
  873. When a message text starts with `/command[@bot_name]`, or with an
  874. alias, it gets passed to the decorated function.
  875. `command` is the command name (with or without /)
  876. `aliases` is a list of aliases
  877. `show_in_keyboard`, if True, makes first alias appear
  878. in default_keyboard
  879. `descr` is a description
  880. `auth` is the lowest authorization level needed to run the command
  881. """
  882. command = command.replace('/', '').lower()
  883. if not isinstance(command, str):
  884. raise TypeError('Command {} is not a string'.format(command))
  885. if aliases:
  886. if not isinstance(aliases, list):
  887. raise TypeError('Aliases is not a list: {}'.format(aliases))
  888. for alias in aliases:
  889. if not isinstance(alias, str):
  890. raise TypeError('Alias {} is not a string'.format(alias))
  891. def decorator(func):
  892. if asyncio.iscoroutinefunction(func):
  893. async def decorated(message):
  894. logging.info(
  895. "COMMAND({c}) @{n} FROM({f})".format(
  896. c=command,
  897. n=self.name,
  898. f=(
  899. message['from']
  900. if 'from' in message
  901. else message['chat']
  902. )
  903. )
  904. )
  905. if self.authorization_function(message, auth):
  906. return await func(message)
  907. return self.unauthorized_message
  908. else:
  909. def decorated(message):
  910. logging.info(
  911. "COMMAND({c}) @{n} FROM({f})".format(
  912. c=command,
  913. n=self.name,
  914. f=(
  915. message['from']
  916. if 'from' in message
  917. else message['chat']
  918. )
  919. )
  920. )
  921. if self.authorization_function(message, auth):
  922. return func(message)
  923. return self.unauthorized_message
  924. self.commands[command] = dict(
  925. function=decorated,
  926. descr=descr,
  927. auth=auth
  928. )
  929. if aliases:
  930. for alias in aliases:
  931. self.aliases[alias] = decorated
  932. if show_in_keyboard:
  933. self.default_reply_keyboard_elements.append(aliases[0])
  934. return decorator
  935. def parser(self, condition, descr='', auth='admin', argument='text'):
  936. """Define a message parser.
  937. Decorator: `@bot.parser(condition)`
  938. If condition evaluates True when run on a message text
  939. (not starting with '/'), such decorated function gets
  940. called on update.
  941. Conditions of parsers are evaluated in order; when one is True,
  942. others will be skipped.
  943. `descr` is a description
  944. `auth` is the lowest authorization level needed to run the command
  945. """
  946. if not callable(condition):
  947. raise TypeError(
  948. 'Condition {} is not a callable'.format(
  949. condition.__name__
  950. )
  951. )
  952. def decorator(func):
  953. if asyncio.iscoroutinefunction(func):
  954. async def decorated(message):
  955. logging.info(
  956. "TEXT MATCHING CONDITION({c}) @{n} FROM({f})".format(
  957. c=condition.__name__,
  958. n=self.name,
  959. f=(
  960. message['from']
  961. if 'from' in message
  962. else message['chat']
  963. )
  964. )
  965. )
  966. if self.authorization_function(message, auth):
  967. return await func(message)
  968. return self.unauthorized_message
  969. else:
  970. def decorated(message):
  971. logging.info(
  972. "TEXT MATCHING CONDITION({c}) @{n} FROM({f})".format(
  973. c=condition.__name__,
  974. n=self.name,
  975. f=(
  976. message['from']
  977. if 'from' in message
  978. else message['chat']
  979. )
  980. )
  981. )
  982. if self.authorization_function(message, auth):
  983. return func(message)
  984. return self.unauthorized_message
  985. self.parsers[condition] = dict(
  986. function=decorated,
  987. descr=descr,
  988. auth=auth,
  989. argument=argument
  990. )
  991. return decorator
  992. def pinned(self, condition, descr='', auth='admin'):
  993. """Handle pinned messages.
  994. Decorator: `@bot.pinned(condition)`
  995. If condition evaluates True when run on a pinned_message update,
  996. such decorated function gets called on update.
  997. Conditions are evaluated in order; when one is True,
  998. others will be skipped.
  999. `descr` is a description
  1000. `auth` is the lowest authorization level needed to run the command
  1001. """
  1002. if not callable(condition):
  1003. raise TypeError(
  1004. 'Condition {c} is not a callable'.format(
  1005. c=condition.__name__
  1006. )
  1007. )
  1008. def decorator(func):
  1009. if asyncio.iscoroutinefunction(func):
  1010. async def decorated(message):
  1011. logging.info(
  1012. "PINNED MESSAGE MATCHING({c}) @{n} FROM({f})".format(
  1013. c=condition.__name__,
  1014. n=self.name,
  1015. f=(
  1016. message['from']
  1017. if 'from' in message
  1018. else message['chat']
  1019. )
  1020. )
  1021. )
  1022. if self.authorization_function(message, auth):
  1023. return await func(message)
  1024. return
  1025. else:
  1026. def decorated(message):
  1027. logging.info(
  1028. "PINNED MESSAGE MATCHING({c}) @{n} FROM({f})".format(
  1029. c=condition.__name__,
  1030. n=self.name,
  1031. f=(
  1032. message['from']
  1033. if 'from' in message
  1034. else message['chat']
  1035. )
  1036. )
  1037. )
  1038. if self.authorization_function(message, auth):
  1039. return func(message)
  1040. return
  1041. self.chat_actions['pinned'][condition] = dict(
  1042. function=decorated,
  1043. descr=descr,
  1044. auth=auth
  1045. )
  1046. return decorator
  1047. def button(self, data, descr='', auth='admin'):
  1048. """Define a bot button.
  1049. Decorator: `@bot.button('example:///')`
  1050. When a callback data text starts with <data>, it gets passed to the
  1051. decorated function
  1052. `descr` is a description
  1053. `auth` is the lowest authorization level needed to run the command
  1054. """
  1055. if not isinstance(data, str):
  1056. raise TypeError(
  1057. 'Inline button callback_data {d} is not a string'.format(
  1058. d=data
  1059. )
  1060. )
  1061. def decorator(func):
  1062. if asyncio.iscoroutinefunction(func):
  1063. async def decorated(message):
  1064. logging.info(
  1065. "INLINE BUTTON({d}) @{n} FROM({f})".format(
  1066. d=message['data'],
  1067. n=self.name,
  1068. f=(
  1069. message['from']
  1070. )
  1071. )
  1072. )
  1073. if self.authorization_function(message, auth):
  1074. return await func(message)
  1075. return self.unauthorized_message
  1076. else:
  1077. def decorated(message):
  1078. logging.info(
  1079. "INLINE BUTTON({d}) @{n} FROM({f})".format(
  1080. d=message['data'],
  1081. n=self.name,
  1082. f=(
  1083. message['from']
  1084. )
  1085. )
  1086. )
  1087. if self.authorization_function(message, auth):
  1088. return func(message)
  1089. return self.unauthorized_message
  1090. self.callback_handlers[data] = dict(
  1091. function=decorated,
  1092. descr=descr,
  1093. auth=auth
  1094. )
  1095. return decorator
  1096. def query(self, condition, descr='', auth='admin'):
  1097. """Define an inline query.
  1098. Decorator: `@bot.query(example)`
  1099. When an inline query matches the `condition` function,
  1100. decorated function is called and passed the query update object
  1101. as argument.
  1102. `descr` is a description
  1103. `auth` is the lowest authorization level needed to run the command
  1104. """
  1105. if not callable(condition):
  1106. raise TypeError(
  1107. 'Condition {c} is not a callable'.format(
  1108. c=condition.__name__
  1109. )
  1110. )
  1111. def decorator(func):
  1112. if asyncio.iscoroutinefunction(func):
  1113. async def decorated(message):
  1114. logging.info(
  1115. "QUERY MATCHING CONDITION({c}) @{n} FROM({f})".format(
  1116. c=condition.__name__,
  1117. n=self.name,
  1118. f=message['from']
  1119. )
  1120. )
  1121. if self.authorization_function(message, auth):
  1122. return await func(message)
  1123. return self.unauthorized_message
  1124. else:
  1125. def decorated(message):
  1126. logging.info(
  1127. "QUERY MATCHING CONDITION({c}) @{n} FROM({f})".format(
  1128. c=condition.__name__,
  1129. n=self.name,
  1130. f=message['from']
  1131. )
  1132. )
  1133. if self.authorization_function(message, auth):
  1134. return func(message)
  1135. return self.unauthorized_message
  1136. self.inline_query_handlers[condition] = dict(
  1137. function=decorated,
  1138. descr=descr,
  1139. auth=auth
  1140. )
  1141. return decorator
  1142. def additional_task(self, when='BEFORE'):
  1143. """Add a task before or after message_loop.
  1144. Decorator: such decorated async functions get awaited BEFORE or
  1145. AFTER messageloop
  1146. """
  1147. when = when[0].lower()
  1148. def decorator(func):
  1149. if when == 'b':
  1150. self.run_before_loop.append(func())
  1151. elif when == 'a':
  1152. self.run_after_loop.append(func())
  1153. return decorator
  1154. def set_default_keyboard(self, keyboard='set_default'):
  1155. """Set a default keyboard for the bot.
  1156. If a keyboard is not passed as argument, a default one is generated,
  1157. based on aliases of commands.
  1158. """
  1159. if keyboard == 'set_default':
  1160. btns = [
  1161. dict(
  1162. text=x
  1163. )
  1164. for x in self.default_reply_keyboard_elements
  1165. ]
  1166. row_len = 2 if len(btns) < 4 else 3
  1167. self._default_keyboard = dict(
  1168. keyboard=make_lines_of_buttons(
  1169. btns,
  1170. row_len
  1171. ),
  1172. resize_keyboard=True
  1173. )
  1174. else:
  1175. self._default_keyboard = keyboard
  1176. return
  1177. async def edit_message(self, update, *args, **kwargs):
  1178. """Edit given update with given *args and **kwargs.
  1179. Please note, that it is currently only possible to edit messages
  1180. without reply_markup or with inline keyboards.
  1181. """
  1182. try:
  1183. return await self.editMessageText(
  1184. telepot.message_identifier(update),
  1185. *args,
  1186. **kwargs
  1187. )
  1188. except Exception as e:
  1189. logging.error("{}".format(e))
  1190. async def delete_message(self, update, *args, **kwargs):
  1191. """Delete given update with given *args and **kwargs.
  1192. Please note, that a bot can delete only messages sent by itself
  1193. or sent in a group which it is administrator of.
  1194. """
  1195. try:
  1196. return await self.deleteMessage(
  1197. telepot.message_identifier(update),
  1198. *args,
  1199. **kwargs
  1200. )
  1201. except Exception as e:
  1202. logging.error("{}".format(e))
  1203. async def send_message(self, answer=dict(), chat_id=None, text='',
  1204. parse_mode="HTML", disable_web_page_preview=None,
  1205. disable_notification=None, reply_to_message_id=None,
  1206. reply_markup=None):
  1207. """Send a message.
  1208. Convenient method to call telepot.Bot(token).sendMessage
  1209. All sendMessage **kwargs can be either **kwargs of send_message
  1210. or key:val of answer argument.
  1211. Messages longer than telegram limit will be split properly.
  1212. Telegram flood limits won't be reached thanks to
  1213. `await avoid_flooding(chat_id)`
  1214. parse_mode will be checked and edited if necessary.
  1215. Arguments will be checked and adapted.
  1216. """
  1217. if type(answer) is dict and 'chat_id' in answer:
  1218. chat_id = answer['chat_id']
  1219. # chat_id may simply be the update to which the bot should repy
  1220. if type(chat_id) is dict:
  1221. chat_id = self.get_chat_id(chat_id)
  1222. if type(answer) is str:
  1223. text = answer
  1224. if (
  1225. not reply_markup
  1226. and chat_id > 0
  1227. and text != self.unauthorized_message
  1228. ):
  1229. reply_markup = self.default_keyboard
  1230. elif type(answer) is dict:
  1231. if 'text' in answer:
  1232. text = answer['text']
  1233. if 'parse_mode' in answer:
  1234. parse_mode = answer['parse_mode']
  1235. if 'disable_web_page_preview' in answer:
  1236. disable_web_page_preview = answer['disable_web_page_preview']
  1237. if 'disable_notification' in answer:
  1238. disable_notification = answer['disable_notification']
  1239. if 'reply_to_message_id' in answer:
  1240. reply_to_message_id = answer['reply_to_message_id']
  1241. if 'reply_markup' in answer:
  1242. reply_markup = answer['reply_markup']
  1243. elif (
  1244. not reply_markup
  1245. and type(chat_id) is int
  1246. and chat_id > 0
  1247. and text != self.unauthorized_message
  1248. ):
  1249. reply_markup = self.default_keyboard
  1250. assert type(text) is str, "Text is not a string!"
  1251. assert (
  1252. type(chat_id) is int
  1253. or (type(chat_id) is str and chat_id.startswith('@'))
  1254. ), "Invalid chat_id:\n\t\t{}".format(chat_id)
  1255. if not text:
  1256. return
  1257. parse_mode = str(parse_mode)
  1258. text_chunks = split_text_gracefully(
  1259. text=text,
  1260. limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 100,
  1261. parse_mode=parse_mode
  1262. )
  1263. n = len(text_chunks)
  1264. for text_chunk in text_chunks:
  1265. n -= 1
  1266. if parse_mode.lower() == "html":
  1267. this_parse_mode = "HTML"
  1268. # Check that all tags are well-formed
  1269. if not markdown_check(
  1270. text_chunk,
  1271. [
  1272. "<", ">",
  1273. "code>", "bold>", "italic>",
  1274. "b>", "i>", "a>", "pre>"
  1275. ]
  1276. ):
  1277. this_parse_mode = "None"
  1278. text_chunk = (
  1279. "!!![invalid markdown syntax]!!!\n\n"
  1280. + text_chunk
  1281. )
  1282. elif parse_mode != "None":
  1283. this_parse_mode = "Markdown"
  1284. # Check that all markdowns are well-formed
  1285. if not markdown_check(
  1286. text_chunk,
  1287. [
  1288. "*", "_", "`"
  1289. ]
  1290. ):
  1291. this_parse_mode = "None"
  1292. text_chunk = (
  1293. "!!![invalid markdown syntax]!!!\n\n"
  1294. + text_chunk
  1295. )
  1296. else:
  1297. this_parse_mode = parse_mode
  1298. this_reply_markup = reply_markup if n == 0 else None
  1299. try:
  1300. await self.avoid_flooding(chat_id)
  1301. result = await self.sendMessage(
  1302. chat_id=chat_id,
  1303. text=text_chunk,
  1304. parse_mode=this_parse_mode,
  1305. disable_web_page_preview=disable_web_page_preview,
  1306. disable_notification=disable_notification,
  1307. reply_to_message_id=reply_to_message_id,
  1308. reply_markup=this_reply_markup
  1309. )
  1310. except Exception as e:
  1311. logging.debug(
  1312. e,
  1313. exc_info=False # Set exc_info=True for more information
  1314. )
  1315. result = e
  1316. return result
  1317. async def send_photo(self, chat_id=None, answer={},
  1318. photo=None, caption='', parse_mode='HTML',
  1319. disable_notification=None, reply_to_message_id=None,
  1320. reply_markup=None, use_stored=True,
  1321. second_chance=False):
  1322. """Send a photo.
  1323. Convenient method to call telepot.Bot(token).sendPhoto
  1324. All sendPhoto **kwargs can be either **kwargs of send_message
  1325. or key:val of answer argument.
  1326. Captions longer than telegram limit will be shortened gently.
  1327. Telegram flood limits won't be reached thanks to
  1328. `await avoid_flooding(chat_id)`
  1329. Most arguments will be checked and adapted.
  1330. If use_stored is set to True, the bot will store sent photo
  1331. telegram_id and use it for faster sending next times (unless
  1332. future errors).
  1333. Sending photos by their file_id already stored on telegram servers
  1334. is way faster: that's why bot stores and uses this info,
  1335. if required.
  1336. A second_chance is given to send photo on error.
  1337. """
  1338. if 'chat_id' in answer:
  1339. chat_id = answer['chat_id']
  1340. # chat_id may simply be the update to which the bot should repy
  1341. if type(chat_id) is dict:
  1342. chat_id = self.get_chat_id(chat_id)
  1343. assert (
  1344. type(chat_id) is int
  1345. or (type(chat_id) is str and chat_id.startswith('@'))
  1346. ), "Invalid chat_id:\n\t\t{}".format(chat_id)
  1347. if 'photo' in answer:
  1348. photo = answer['photo']
  1349. assert photo is not None, "Null photo!"
  1350. if 'caption' in answer:
  1351. caption = answer['caption']
  1352. if 'parse_mode' in answer:
  1353. parse_mode = answer['parse_mode']
  1354. if 'disable_notification' in answer:
  1355. disable_notification = answer['disable_notification']
  1356. if 'reply_to_message_id' in answer:
  1357. reply_to_message_id = answer['reply_to_message_id']
  1358. if 'reply_markup' in answer:
  1359. reply_markup = answer['reply_markup']
  1360. already_sent = False
  1361. if type(photo) is str:
  1362. photo_url = photo
  1363. with self.db as db:
  1364. already_sent = db['sent_pictures'].find_one(
  1365. url=photo_url,
  1366. errors=False
  1367. )
  1368. if already_sent and use_stored:
  1369. photo = already_sent['file_id']
  1370. already_sent = True
  1371. else:
  1372. already_sent = False
  1373. if not any(photo_url.startswith(x) for x in ['http', 'www']):
  1374. with io.BytesIO() as buffered_picture:
  1375. with open(
  1376. "{}/{}".format(
  1377. self.path,
  1378. photo_url
  1379. ),
  1380. 'rb'
  1381. ) as photo_file:
  1382. buffered_picture.write(photo_file.read())
  1383. photo = buffered_picture.getvalue()
  1384. caption = escape_html_chars(caption)
  1385. if len(caption) > 199:
  1386. new_caption = ''
  1387. tag = False
  1388. tag_body = False
  1389. count = 0
  1390. temp = ''
  1391. for char in caption:
  1392. if tag and char == '>':
  1393. tag = False
  1394. elif char == '<':
  1395. tag = True
  1396. tag_body = not tag_body
  1397. elif not tag:
  1398. count += 1
  1399. if count == 199:
  1400. break
  1401. temp += char
  1402. if not tag_body:
  1403. new_caption += temp
  1404. temp = ''
  1405. caption = new_caption
  1406. sent = None
  1407. try:
  1408. await self.avoid_flooding(chat_id)
  1409. sent = await self.sendPhoto(
  1410. chat_id=chat_id,
  1411. photo=photo,
  1412. caption=caption,
  1413. parse_mode=parse_mode,
  1414. disable_notification=disable_notification,
  1415. reply_to_message_id=reply_to_message_id,
  1416. reply_markup=reply_markup
  1417. )
  1418. if isinstance(sent, Exception):
  1419. raise Exception("SendingFailed")
  1420. except Exception as e:
  1421. logging.error(
  1422. "Error sending photo\n{}".format(
  1423. e
  1424. ),
  1425. exc_info=False # Set exc_info=True for more information
  1426. )
  1427. if already_sent:
  1428. with self.db as db:
  1429. db['sent_pictures'].update(
  1430. dict(
  1431. url=photo_url,
  1432. errors=True
  1433. ),
  1434. ['url']
  1435. )
  1436. if not second_chance:
  1437. logging.info("Trying again (only once)...")
  1438. sent = await self.send_photo(
  1439. chat_id=chat_id,
  1440. answer=answer,
  1441. photo=photo,
  1442. caption=caption,
  1443. parse_mode=parse_mode,
  1444. disable_notification=disable_notification,
  1445. reply_to_message_id=reply_to_message_id,
  1446. reply_markup=reply_markup,
  1447. second_chance=True
  1448. )
  1449. if (
  1450. sent is not None
  1451. and hasattr(sent, '__getitem__')
  1452. and 'photo' in sent
  1453. and len(sent['photo']) > 0
  1454. and 'file_id' in sent['photo'][0]
  1455. and (not already_sent)
  1456. and use_stored
  1457. ):
  1458. with self.db as db:
  1459. db['sent_pictures'].insert(
  1460. dict(
  1461. url=photo_url,
  1462. file_id=sent['photo'][0]['file_id'],
  1463. errors=False
  1464. )
  1465. )
  1466. return sent
  1467. async def send_and_destroy(self, chat_id, answer,
  1468. timer=60, mode='text', **kwargs):
  1469. """Send a message or photo and delete it after `timer` seconds."""
  1470. if mode == 'text':
  1471. sent_message = await self.send_message(
  1472. chat_id=chat_id,
  1473. answer=answer,
  1474. **kwargs
  1475. )
  1476. elif mode == 'pic':
  1477. sent_message = await self.send_photo(
  1478. chat_id=chat_id,
  1479. answer=answer,
  1480. **kwargs
  1481. )
  1482. if sent_message is None:
  1483. return
  1484. self.to_be_destroyed.append(sent_message)
  1485. await asyncio.sleep(timer)
  1486. if await self.delete_message(sent_message):
  1487. self.to_be_destroyed.remove(sent_message)
  1488. return
  1489. async def wait_and_obscure(self, update, when, inline_message_id):
  1490. """Obscure messages which can't be deleted.
  1491. Obscure an inline_message `timer` seconds after sending it,
  1492. by editing its text or caption.
  1493. At the moment Telegram won't let bots delete sent inline query results.
  1494. """
  1495. if type(when) is int:
  1496. when = datetime.datetime.now() + datetime.timedelta(seconds=when)
  1497. assert type(when) is datetime.datetime, (
  1498. "when must be a datetime instance or a number of seconds (int) "
  1499. "to be awaited"
  1500. )
  1501. if 'inline_message_id' not in update:
  1502. logging.info(
  1503. "This inline query result owns no inline_keyboard, so it "
  1504. "can't be modified"
  1505. )
  1506. return
  1507. inline_message_id = update['inline_message_id']
  1508. self.to_be_obscured.append(inline_message_id)
  1509. while datetime.datetime.now() < when:
  1510. await sleep_until(when)
  1511. try:
  1512. await self.editMessageCaption(
  1513. inline_message_id,
  1514. text="Time over"
  1515. )
  1516. except Exception:
  1517. try:
  1518. await self.editMessageText(
  1519. inline_message_id,
  1520. text="Time over"
  1521. )
  1522. except Exception as e:
  1523. logging.error(
  1524. "Couldn't obscure message\n{}\n\n{}".format(
  1525. inline_message_id,
  1526. e
  1527. )
  1528. )
  1529. self.to_be_obscured.remove(inline_message_id)
  1530. return
  1531. async def get_me(self):
  1532. """Get bot information.
  1533. Restart bots if bot can't be got.
  1534. """
  1535. try:
  1536. me = await self.getMe()
  1537. self.bot_name = me["username"]
  1538. self.telegram_id = me['id']
  1539. except Exception as e:
  1540. logging.error(
  1541. "Could not get bot\n{e}".format(
  1542. e=e
  1543. )
  1544. )
  1545. await asyncio.sleep(5*60)
  1546. self.restart_bots()
  1547. return
  1548. async def continue_running(self):
  1549. """Get updates.
  1550. If bot can be got, sets name and telegram_id,
  1551. awaits preliminary tasks and starts getting updates from telegram.
  1552. If bot can't be got, restarts all bots in 5 minutes.
  1553. """
  1554. await self.get_me()
  1555. for task in self.run_before_loop:
  1556. await task
  1557. self.set_default_keyboard()
  1558. asyncio.ensure_future(
  1559. self.message_loop(handler=self.routing_table)
  1560. )
  1561. return
  1562. def stop_bots(self):
  1563. """Exit script with code 0."""
  1564. Bot.stop = True
  1565. def restart_bots(self):
  1566. """Restart the script exiting with code 65.
  1567. Actually, you need to catch Bot.stop state when Bot.run() returns
  1568. and handle the situation yourself.
  1569. """
  1570. Bot.stop = "Restart"
  1571. @classmethod
  1572. async def check_task(cls):
  1573. """Await until cls.stop, then end session and return."""
  1574. for bot in cls.instances.values():
  1575. asyncio.ensure_future(bot.continue_running())
  1576. while not cls.stop:
  1577. await asyncio.sleep(10)
  1578. return await cls.end_session()
  1579. @classmethod
  1580. async def end_session(cls):
  1581. """Run after stop, before the script exits.
  1582. Await final tasks, obscure and delete pending messages,
  1583. log current operation (stop/restart).
  1584. """
  1585. for bot in cls.instances.values():
  1586. for task in bot.run_after_loop:
  1587. await task
  1588. for message in bot.to_be_destroyed:
  1589. try:
  1590. await bot.delete_message(message)
  1591. except Exception as e:
  1592. logging.error(
  1593. "Couldn't delete message\n{}\n\n{}".format(
  1594. message,
  1595. e
  1596. )
  1597. )
  1598. for inline_message_id in bot.to_be_obscured:
  1599. try:
  1600. await bot.editMessageCaption(
  1601. inline_message_id,
  1602. text="Time over"
  1603. )
  1604. except Exception:
  1605. try:
  1606. await bot.editMessageText(
  1607. inline_message_id,
  1608. text="Time over"
  1609. )
  1610. except Exception as e:
  1611. logging.error(
  1612. "Couldn't obscure message\n{}\n\n{}".format(
  1613. inline_message_id,
  1614. e
  1615. )
  1616. )
  1617. if cls.stop == "Restart":
  1618. logging.info("\n\t\t---Restart!---")
  1619. elif cls.stop == "KeyboardInterrupt":
  1620. logging.info("Stopped by KeyboardInterrupt.")
  1621. else:
  1622. logging.info("Stopped gracefully by user.")
  1623. return
  1624. @classmethod
  1625. def run(cls, loop=None):
  1626. """Call this method to run the async bots."""
  1627. if loop is None:
  1628. loop = asyncio.get_event_loop()
  1629. logging.info(
  1630. "{sep}{subjvb} STARTED{sep}".format(
  1631. sep='-'*10,
  1632. subjvb='BOT HAS' if len(cls.instances) == 1 else 'BOTS HAVE'
  1633. )
  1634. )
  1635. try:
  1636. loop.run_until_complete(cls.check_task())
  1637. except KeyboardInterrupt:
  1638. logging.info(
  1639. (
  1640. "\n\t\tYour script received a KeyboardInterrupt signal, "
  1641. "your bot{} being stopped."
  1642. ).format(
  1643. 's are'
  1644. if len(cls.instances) > 1
  1645. else ' is'
  1646. )
  1647. )
  1648. cls.stop = "KeyboardInterrupt"
  1649. loop.run_until_complete(cls.end_session())
  1650. except Exception as e:
  1651. logging.error(
  1652. '\nYour bot{vb} been stopped. with error \'{e}\''.format(
  1653. e=e,
  1654. vb='s have' if len(cls.instances) > 1 else ' has'
  1655. ),
  1656. exc_info=True
  1657. )
  1658. logging.info(
  1659. "{sep}{subjvb} STOPPED{sep}".format(
  1660. sep='-'*10,
  1661. subjvb='BOT HAS' if len(cls.instances) == 1 else 'BOTS HAVE'
  1662. )
  1663. )
  1664. return
  1665. @classmethod
  1666. async def _run_manual_mode(cls):
  1667. available_bots = MyOD()
  1668. for code, bot in enumerate(
  1669. cls.instances.values()
  1670. ):
  1671. await bot.get_me()
  1672. available_bots[code] = dict(
  1673. bot=bot,
  1674. code=code,
  1675. name=bot.name
  1676. )
  1677. selected_bot = None
  1678. while selected_bot is None:
  1679. user_input = input(
  1680. "\n=============================================\n"
  1681. "Which bot would you like to control manually?\n"
  1682. "Available bots:\n{}\n\n\t\t".format(
  1683. line_drawing_unordered_list(
  1684. list(
  1685. "{b[code]:>3} - {b[bot].name}".format(
  1686. b=bot,
  1687. )
  1688. for bot in available_bots.values()
  1689. )
  1690. )
  1691. )
  1692. )
  1693. if (
  1694. user_input.isnumeric()
  1695. and int(user_input) in available_bots
  1696. ):
  1697. selected_bot = available_bots[int(user_input)]
  1698. else:
  1699. selected_bot = pick_most_similar_from_list(
  1700. [
  1701. bot['name']
  1702. for bot in available_bots.values()
  1703. ],
  1704. user_input
  1705. )
  1706. selected_bot = available_bots.get_by_key_val(
  1707. key='name',
  1708. val=selected_bot,
  1709. case_sensitive=False,
  1710. return_value=True
  1711. )
  1712. if selected_bot is None:
  1713. logging.error("Invalid selection.")
  1714. continue
  1715. logging.info(
  1716. "Bot `{b[name]}` selected.".format(
  1717. b=selected_bot
  1718. )
  1719. )
  1720. exit_code = await selected_bot['bot']._run_manually()
  1721. if exit_code == 0:
  1722. break
  1723. elif exit_code == 65:
  1724. selected_bot = None
  1725. return
  1726. @classmethod
  1727. def run_manual_mode(cls, loop=None):
  1728. """Run in manual mode: send messages via bots."""
  1729. if loop is None:
  1730. loop = asyncio.get_event_loop()
  1731. logging.info(
  1732. "=== MANUAL MODE STARTED ==="
  1733. )
  1734. try:
  1735. loop.run_until_complete(
  1736. cls._run_manual_mode()
  1737. )
  1738. except KeyboardInterrupt:
  1739. logging.info(
  1740. (
  1741. "\n\t\tYour script received a KeyboardInterrupt signal, "
  1742. "your bot{} being stopped."
  1743. ).format(
  1744. 's are' if len(cls.instances) > 1 else ' is'
  1745. )
  1746. )
  1747. except Exception as e:
  1748. logging.error(
  1749. '\nYour bot{vb} been stopped. with error \'{e}\''.format(
  1750. e=e,
  1751. vb='s have' if len(cls.instances) > 1 else ' has'
  1752. ),
  1753. exc_info=True
  1754. )
  1755. logging.info(
  1756. "=== MANUAL MODE STOPPED ==="
  1757. )
  1758. async def _run_manually(self):
  1759. user_input = ' choose_addressee'
  1760. selected_user = None
  1761. users = []
  1762. while user_input:
  1763. if user_input == ' choose_addressee':
  1764. try:
  1765. user_input = input(
  1766. "Choose an addressee."
  1767. "\n\t\t"
  1768. )
  1769. if len(user_input) == 0:
  1770. return 65 # Let user select a different bot
  1771. except KeyboardInterrupt:
  1772. logging.error("Keyboard interrupt.")
  1773. return 0 # Stop running
  1774. if (
  1775. selected_user is None
  1776. and user_input.strip('-').isnumeric()
  1777. ):
  1778. user_input = int(user_input)
  1779. users = list(
  1780. filter(
  1781. lambda user: user['telegram_id'] == user_input,
  1782. users
  1783. )
  1784. )
  1785. if len(users) == 0:
  1786. users = [
  1787. dict(
  1788. telegram_id=user_input,
  1789. name='Unknown user'
  1790. )
  1791. ]
  1792. elif (
  1793. selected_user is None
  1794. and self.db_url is not None
  1795. ):
  1796. with self.db as db:
  1797. if 'users' not in db.tables:
  1798. db['users'].insert(
  1799. dict(
  1800. telegram_id=0,
  1801. privileges=100,
  1802. username='username',
  1803. first_name='first_name',
  1804. last_name='last_name'
  1805. )
  1806. )
  1807. if 'contacts' not in db.tables:
  1808. db['contacts'].insert(
  1809. dict(
  1810. telegram_id=0,
  1811. name='ZZZXXXAAA',
  1812. )
  1813. )
  1814. users = list(
  1815. db.query(
  1816. """SELECT telegram_id, MAX(name) name
  1817. FROM (
  1818. SELECT telegram_id,
  1819. COALESCE(
  1820. first_name || ' ' || last_name ||
  1821. ' (' || username || ')',
  1822. username,
  1823. first_name || ' ' || last_name,
  1824. last_name,
  1825. first_name
  1826. ) AS name
  1827. FROM users
  1828. WHERE COALESCE(
  1829. first_name || last_name || username,
  1830. first_name || username,
  1831. last_name || username,
  1832. first_name || last_name,
  1833. username,
  1834. last_name,
  1835. first_name
  1836. )
  1837. LIKE '%{u}%'
  1838. UNION ALL
  1839. SELECT telegram_id, name
  1840. FROM contacts
  1841. WHERE name LIKE '%{u}%'
  1842. )
  1843. GROUP BY telegram_id
  1844. """.format(
  1845. u=user_input
  1846. )
  1847. )
  1848. )
  1849. if len(users) == 0:
  1850. logging.info("Sorry, no user matches your query.")
  1851. user_input = ' choose_addressee'
  1852. elif len(users) > 1:
  1853. user_input = input(
  1854. "Please select one of the following users:\n"
  1855. "\n"
  1856. "{users}\n"
  1857. "\n"
  1858. "Paste their telegram_id\n"
  1859. "\t\t".format(
  1860. users=line_drawing_unordered_list(
  1861. sorted(
  1862. map(
  1863. lambda user: (
  1864. "{u[telegram_id]} - {u[name]}"
  1865. ).format(
  1866. u=user
  1867. ),
  1868. users
  1869. )
  1870. )
  1871. )
  1872. )
  1873. )
  1874. elif len(users) == 1:
  1875. selected_user = users[0]
  1876. while selected_user is not None:
  1877. sent = None
  1878. text = input(
  1879. "What would you like to send "
  1880. "{u[name]} ({u[telegram_id]})?\n"
  1881. "\t\t\t".format(
  1882. u=selected_user
  1883. )
  1884. )
  1885. if len(text) == 0:
  1886. selected_user = None
  1887. user_input = ' choose_addressee'
  1888. elif text.lower() == 'photo':
  1889. caption = input(
  1890. 'Write a caption (you can leave it blank)\n'
  1891. '\t\t\t'
  1892. )
  1893. try:
  1894. with io.BytesIO() as buffered_picture:
  1895. with open(
  1896. '{path}/sendme.png'.format(
  1897. path=self.path
  1898. ),
  1899. 'rb' # Read bytes
  1900. ) as photo_file:
  1901. buffered_picture.write(
  1902. photo_file.read()
  1903. )
  1904. photo = buffered_picture.getvalue()
  1905. sent = await self.send_photo(
  1906. chat_id=selected_user['telegram_id'],
  1907. photo=photo,
  1908. caption=caption,
  1909. parse_mode='HTML'
  1910. )
  1911. except Exception as e:
  1912. logging.error(e)
  1913. else:
  1914. try:
  1915. sent = await self.send_message(
  1916. chat_id=selected_user['telegram_id'],
  1917. text=text,
  1918. parse_mode='HTML'
  1919. )
  1920. except Exception as e:
  1921. logging.error(e)
  1922. if (
  1923. sent is not None
  1924. and not isinstance(sent, Exception)
  1925. ):
  1926. logging.info(
  1927. '\n'
  1928. 'Sent message:\n'
  1929. '{s}\n'
  1930. '\n'.format(
  1931. s=sent
  1932. )
  1933. )
  1934. while (
  1935. self.db_url
  1936. and selected_user['name'] == 'Unknown user'
  1937. ):
  1938. new_name = input(
  1939. "Please enter a nickname for this user.\n"
  1940. "Next time you may retrieve their telegram_id "
  1941. "by passing this nickname (or a part of it).\n"
  1942. "\t\t"
  1943. )
  1944. if len(new_name):
  1945. selected_user['name'] = new_name
  1946. with self.db as db:
  1947. db['contacts'].upsert(
  1948. selected_user,
  1949. ['telegram_id'],
  1950. ensure=True
  1951. )
  1952. else:
  1953. logging.info("Invalid name, please try again.")
  1954. return 65 # Keep running, making user select another bot