Queer European MD passionate about IT

custombot.py 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279
  1. """This module conveniently subclasses third party telepot.aio.Bot, providing the following features.
  2. - It prevents hitting Telegram flood limits by waiting between text and photo messages.
  3. - It provides command, parser, button and other decorators to associate common Telegram actions with custom handlers.
  4. - It supports multiple bots running in the same script and allows communications between them as well as complete independency from each other.
  5. - Each bot is associated with a sqlite database (using dataset third party library).
  6. Please note that you need Python3.5+ to run async code
  7. Check requirements.txt for third party dependencies.
  8. """
  9. # Standard library modules
  10. import asyncio
  11. import datetime
  12. import io
  13. import logging
  14. import os
  15. # Third party modules
  16. import dataset
  17. import telepot, telepot.aio
  18. # Project modules
  19. from davteutil import Gettable, MyOD
  20. from davteutil import escape_html_chars, get_cleaned_text, make_lines_of_buttons, markdown_check, remove_html_tags, sleep_until
  21. def split_text_gracefully(text, limit, parse_mode):
  22. """Split text if it hits telegram limits for text messages.
  23. Split at `\n` if possible.
  24. Add a `[...]` at the end and beginning of split messages, with proper code markdown.
  25. """
  26. text = text.split("\n")[::-1]
  27. result = []
  28. while len(text)>0:
  29. temp=[]
  30. while len(text)>0 and len("\n".join(temp + [text[-1]]))<limit:
  31. temp.append(text.pop())
  32. if len(temp) == 0:
  33. temp.append(text[-1][:limit])
  34. text[-1] = text[-1][limit:]
  35. result.append("\n".join(temp))
  36. if len(result)>1:
  37. for i in range(1,len(result)):
  38. result[i] = "{tag[0]}[...]{tag[1]}\n{text}".format(
  39. tag=('`','`') if parse_mode=='Markdown'
  40. else ('<code>', '</code>') if parse_mode.lower() == 'html'
  41. else ('', ''),
  42. text=result[i]
  43. )
  44. result[i-1] = "{text}\n{tag[0]}[...]{tag[1]}".format(
  45. tag=('`','`') if parse_mode=='Markdown'
  46. else ('<code>', '</code>') if parse_mode.lower() == 'html'
  47. else ('', ''),
  48. text=result[i-1]
  49. )
  50. return result
  51. def make_inline_query_answer(answer):
  52. """Return an article-type answer to inline query.
  53. Takes either a string or a dictionary and returns a list."""
  54. if type(answer) is str:
  55. answer = dict(
  56. type='article',
  57. id=0,
  58. title=remove_html_tags(answer),
  59. input_message_content=dict(
  60. message_text=answer,
  61. parse_mode='HTML'
  62. )
  63. )
  64. if type(answer) is dict:
  65. answer = [answer]
  66. return answer
  67. class Bot(telepot.aio.Bot, Gettable):
  68. """telepot.aio.Bot (async Telegram bot framework) convenient subclass.
  69. === General functioning ===
  70. - While Bot.run() coroutine is executed, HTTP get requests are made to Telegram servers asking for new messages for each Bot instance.
  71. - Each message causes the proper Bot instance method coroutine to be awaited, according to its flavour (see routing_table)
  72. -- For example, chat messages cause `Bot().on_chat_message(message)` to be awaited.
  73. - This even-processing coroutine ensures the proper handling function a future and returns.
  74. -- That means that simpler tasks are completed before slower ones, since handling functions are not awaited but scheduled by `asyncio.ensure_future(handling_function(...))`
  75. -- For example, chat text messages are handled by `handle_text_message`, which looks for the proper function to elaborate the request (in bot's commands and parsers)
  76. - The handling function evaluates an answer, depending on the message content, and eventually provides a reply
  77. -- For example, `handle_text_message` sends its answer via `send_message`
  78. - All Bot.instances run simultaneously and faster requests are completed earlier.
  79. - All uncaught events are ignored.
  80. """
  81. instances = {}
  82. stop = False
  83. # Cooldown time between sent messages, to prevent hitting Telegram flood limits
  84. # Current limits: 30 total messages sent per second, 1 message per second per chat, 20 messages per minute per group
  85. COOLDOWN_TIME_ABSOLUTE = datetime.timedelta(seconds=1/30)
  86. COOLDOWN_TIME_PER_CHAT = datetime.timedelta(seconds=1)
  87. MAX_GROUP_MESSAGES_PER_MINUTE = 20
  88. # Max length of text field for a Telegram message (UTF-8 text)
  89. TELEGRAM_MESSAGES_MAX_LEN = 4096
  90. _path = '.'
  91. _unauthorized_message = None
  92. _unknown_command_message = None
  93. _maintenance_message = None
  94. _default_inline_query_answer = [
  95. dict(
  96. type='article',
  97. id=0,
  98. title="I cannot answer this query, sorry",
  99. input_message_content=dict(
  100. message_text="I'm sorry but I could not find an answer for your query."
  101. )
  102. )
  103. ]
  104. def __init__(self, token, db_name=None):
  105. super().__init__(token)
  106. self.routing_table = {
  107. 'chat' : self.on_chat_message,
  108. 'inline_query' : self.on_inline_query,
  109. 'chosen_inline_result' : self.on_chosen_inline_result,
  110. 'callback_query' : self.on_callback_query
  111. }
  112. self.chat_message_handlers = {
  113. 'text': self.handle_text_message,
  114. 'pinned_message': self.handle_pinned_message,
  115. 'photo': self.handle_photo_message
  116. }
  117. if db_name:
  118. self.db_url = 'sqlite:///{name}{ext}'.format(
  119. name=db_name,
  120. ext='.db' if not db_name.endswith('.db') else ''
  121. )
  122. self._unauthorized_message = None
  123. self.authorization_function = lambda update, authorization_level: True
  124. self.get_chat_id = lambda update: update['message']['chat']['id'] if 'message' in update else update['chat']['id']
  125. self.commands = dict()
  126. self.callback_handlers = dict()
  127. self.inline_query_handlers = MyOD()
  128. self._default_inline_query_answer = None
  129. self.chosen_inline_result_handlers = dict()
  130. self.aliases = MyOD()
  131. self.parsers = MyOD()
  132. self.custom_parsers = dict()
  133. self.custom_photo_parsers = dict()
  134. self.bot_name = None
  135. self.default_reply_keyboard_elements = []
  136. self._default_keyboard = dict()
  137. self.run_before_loop = []
  138. self.run_after_loop = []
  139. self.to_be_obscured = []
  140. self.to_be_destroyed = []
  141. self.last_sending_time = dict(
  142. absolute=datetime.datetime.now() - self.__class__.COOLDOWN_TIME_ABSOLUTE
  143. )
  144. self._maintenance = False
  145. self._maintenance_message = None
  146. self.chat_actions = dict(
  147. pinned=MyOD()
  148. )
  149. @property
  150. def name(self):
  151. """Bot name"""
  152. return self.bot_name
  153. @property
  154. def path(self):
  155. """custombot.py file path"""
  156. return self.__class__._path
  157. @property
  158. def db(self):
  159. """Connection to bot's database
  160. It must be used inside a with statement: `with bot.db as db`
  161. """
  162. if self.db_url:
  163. return dataset.connect(self.db_url)
  164. @property
  165. def default_keyboard(self):
  166. """Default keyboard which is sent when reply_markup is left blank and chat is private.
  167. """
  168. return self._default_keyboard
  169. @property
  170. def default_inline_query_answer(self):
  171. """Answer to be returned if inline query returned None.
  172. """
  173. if self._default_inline_query_answer:
  174. return self._default_inline_query_answer
  175. return self.__class__._default_inline_query_answer
  176. @property
  177. def unauthorized_message(self):
  178. """Return this if user is unauthorized to make a request.
  179. If instance message is not set, class message is returned.
  180. """
  181. if self._unauthorized_message:
  182. return self._unauthorized_message
  183. return self.__class__._unauthorized_message
  184. @property
  185. def unknown_command_message(self):
  186. """Message to be returned if user sends an unknown command in private chat.
  187. If instance message is not set, class message is returned.
  188. """
  189. if self._unknown_command_message:
  190. return self._unknown_command_message
  191. return self.__class__._unknown_command_message
  192. @property
  193. def maintenance(self):
  194. """True if bot is under maintenance, False otherwise.
  195. While under maintenance, bot will reply with `self.maintenance_message` to any request, with few exceptions."""
  196. return self._maintenance
  197. @property
  198. def maintenance_message(self):
  199. """Message to be returned if bot is under maintenance.
  200. If instance message is not set, class message is returned.
  201. """
  202. if self._maintenance_message:
  203. return self._maintenance_message
  204. if self.__class__.maintenance_message:
  205. return self.__class__._maintenance_message
  206. return "Bot is currently under maintenance! Retry later please."
  207. @classmethod
  208. def set_class_path(csl, path):
  209. """Set class path, where files will be looked for.
  210. For example, if send_photo receives `photo='mypic.png'`, it will parse it as `'{path}/mypic.png'.format(path=self.path)`
  211. """
  212. csl._path = path
  213. @classmethod
  214. def set_class_unauthorized_message(csl, unauthorized_message):
  215. """Set class unauthorized message, to be returned if user is unauthorized to make a request.
  216. """
  217. csl._unauthorized_message = unauthorized_message
  218. @classmethod
  219. def set_class_unknown_command_message(cls, unknown_command_message):
  220. """Set class unknown command message, to be returned if user sends an unknown command in private chat.
  221. """
  222. cls._unknown_command_message = unknown_command_message
  223. @classmethod
  224. def set_class_maintenance_message(cls, maintenance_message):
  225. """Set class maintenance message, to be returned if bot is under maintenance.
  226. """
  227. cls._maintenance_message = maintenance_message
  228. @classmethod
  229. def set_class_default_inline_query_answer(cls, default_inline_query_answer):
  230. """Set class default inline query answer, to be returned if an inline query returned no answer.
  231. """
  232. cls._default_inline_query_answer = default_inline_query_answer
  233. def set_unauthorized_message(self, unauthorized_message):
  234. """Set instance unauthorized message
  235. If instance message is None, default class message is used.
  236. """
  237. self._unauthorized_message = unauthorized_message
  238. def set_unknown_command_message(self, unknown_command_message):
  239. """Set instance unknown command message, to be returned if user sends an unknown command in private chat.
  240. If instance message is None, default class message is used.
  241. """
  242. self._unknown_command_message = unknown_command_message
  243. def set_maintenance_message(self, maintenance_message):
  244. """Set instance maintenance message, to be returned if bot is under maintenance.
  245. If instance message is None, default class message is used.
  246. """
  247. self._maintenance_message = maintenance_message
  248. def set_default_inline_query_answer(self, default_inline_query_answer):
  249. """Set a custom default_inline_query_answer to be returned when no answer is found for an inline query.
  250. If instance answer is None, default class answer is used.
  251. """
  252. if type(default_inline_query_answer) in (str, dict):
  253. default_inline_query_answer = make_inline_query_answer(default_inline_query_answer)
  254. if type(default_inline_query_answer) is not list:
  255. return 1
  256. self._default_inline_query_answer = default_inline_query_answer
  257. return 0
  258. def set_maintenance(self, maintenance_message):
  259. """Puts the bot under maintenance or ends it.
  260. While in maintenance, bot will reply to users with maintenance_message.
  261. Bot will accept /coma, /stop and /restart commands from admins.
  262. s"""
  263. self._maintenance = not self.maintenance
  264. if maintenance_message:
  265. self.set_maintenance_message(maintenance_message)
  266. if self.maintenance:
  267. return "<i>Bot has just been put under maintenance!</i>\n\nUntil further notice, it will reply to users with the following message:\n\n{}".format(
  268. self.maintenance_message
  269. )
  270. return "<i>Maintenance ended!</i>"
  271. def set_authorization_function(self, authorization_function):
  272. """Set a custom authorization_function, which evaluates True if user is authorized to perform a specific action and False otherwise.
  273. It should take update and role and return a Boolean.
  274. Default authorization_function always evaluates True.
  275. """
  276. self.authorization_function = authorization_function
  277. def set_get_chat_id_function(self, get_chat_id_function):
  278. """Set a custom get_chat_id function which takes and update and returns the chat in which a reply should be sent.
  279. For instance, bots could reply in private to group messages as a default behaviour.
  280. Default chat_id returned is current chat id.
  281. """
  282. self.get_chat_id = get_chat_id_function
  283. async def avoid_flooding(self, chat_id):
  284. """asyncio-sleep until COOLDOWN_TIME (per_chat and absolute) has passed.
  285. To prevent hitting Telegram flood limits, send_message and send_photo await this function.
  286. """
  287. if type(chat_id) is int and chat_id > 0:
  288. while (
  289. datetime.datetime.now() < self.last_sending_time['absolute'] + self.__class__.COOLDOWN_TIME_ABSOLUTE
  290. ) or (
  291. chat_id in self.last_sending_time
  292. and datetime.datetime.now() < self.last_sending_time[chat_id] + self.__class__.COOLDOWN_TIME_PER_CHAT
  293. ):
  294. await asyncio.sleep(self.__class__.COOLDOWN_TIME_ABSOLUTE.seconds)
  295. self.last_sending_time[chat_id] = datetime.datetime.now()
  296. else:
  297. while (
  298. datetime.datetime.now() < self.last_sending_time['absolute'] + self.__class__.COOLDOWN_TIME_ABSOLUTE
  299. ) or (
  300. chat_id in self.last_sending_time
  301. and len(
  302. [
  303. sending_datetime
  304. for sending_datetime in self.last_sending_time[chat_id]
  305. if sending_datetime >= datetime.datetime.now() - datetime.timedelta(minutes=1)
  306. ]
  307. ) >= self.__class__.MAX_GROUP_MESSAGES_PER_MINUTE
  308. ) or (
  309. chat_id in self.last_sending_time
  310. and len(self.last_sending_time[chat_id]) > 0
  311. and datetime.datetime.now() < self.last_sending_time[chat_id][-1] + self.__class__.COOLDOWN_TIME_PER_CHAT
  312. ):
  313. await asyncio.sleep(0.5)
  314. if chat_id not in self.last_sending_time:
  315. self.last_sending_time[chat_id] = []
  316. self.last_sending_time[chat_id].append(datetime.datetime.now())
  317. self.last_sending_time[chat_id] = [
  318. sending_datetime
  319. for sending_datetime in self.last_sending_time[chat_id]
  320. if sending_datetime >= datetime.datetime.now() - datetime.timedelta(minutes=1)
  321. ]
  322. self.last_sending_time['absolute'] = datetime.datetime.now()
  323. return
  324. async def on_inline_query(self, update):
  325. """Schedule handling of received inline queries.
  326. Notice that handling is only scheduled, not awaited.
  327. This means that all Bot instances may now handle other requests before this one is completed.
  328. """
  329. asyncio.ensure_future(self.handle_inline_query(update))
  330. return
  331. async def on_chosen_inline_result(self, update):
  332. """Schedule handling of received chosen inline result events.
  333. Notice that handling is only scheduled, not awaited.
  334. This means that all Bot instances may now handle other requests before this one is completed.
  335. """
  336. asyncio.ensure_future(self.handle_chosen_inline_result(update))
  337. return
  338. async def on_callback_query(self, update):
  339. """Schedule handling of received callback queries.
  340. A callback query is sent when users press inline keyboard buttons.
  341. Bad clients may send malformed or deceiving callback queries: never use secret keys in buttons and always check request validity!
  342. Notice that handling is only scheduled, not awaited.
  343. This means that all Bot instances may now handle other requests before this one is completed.
  344. """
  345. # Reject malformed updates lacking of data field
  346. if 'data' not in update:
  347. return
  348. asyncio.ensure_future(self.handle_callback_query(update))
  349. return
  350. async def on_chat_message(self, update):
  351. """Schedule handling of received chat message.
  352. Notice that handling is only scheduled, not awaited.
  353. According to update type, the corresponding handler is scheduled (see self.chat_message_handlers).
  354. This means that all Bot instances may now handle other requests before this one is completed.
  355. """
  356. answer = None
  357. content_type, chat_type, chat_id = telepot.glance(
  358. update,
  359. flavor='chat',
  360. long=False
  361. )
  362. if content_type in self.chat_message_handlers:
  363. answer = asyncio.ensure_future(
  364. self.chat_message_handlers[content_type](update)
  365. )
  366. else:
  367. answer = None
  368. logging.debug("Unhandled message")
  369. return answer
  370. async def handle_inline_query(self, update):
  371. """Handle inline query and answer it with results, or log errors.
  372. """
  373. query = update['query']
  374. answer = None
  375. switch_pm_text, switch_pm_parameter = None, None
  376. if self.maintenance:
  377. answer = self.maintenance_message
  378. else:
  379. for condition, handler in self.inline_query_handlers.items():
  380. answerer = handler['function']
  381. if condition(update['query']):
  382. if asyncio.iscoroutinefunction(answerer):
  383. answer = await answerer(update)
  384. else:
  385. answer = answerer(update)
  386. break
  387. if not answer:
  388. answer = self.default_inline_query_answer
  389. if type(answer) is dict:
  390. if 'switch_pm_text' in answer:
  391. switch_pm_text = answer['switch_pm_text']
  392. if 'switch_pm_parameter' in answer:
  393. switch_pm_parameter = answer['switch_pm_parameter']
  394. answer = answer['answer']
  395. if type(answer) is str:
  396. answer = make_inline_query_answer(answer)
  397. try:
  398. await self.answerInlineQuery(
  399. update['id'],
  400. answer,
  401. cache_time=10,
  402. is_personal=True,
  403. switch_pm_text=switch_pm_text,
  404. switch_pm_parameter=switch_pm_parameter
  405. )
  406. except Exception as e:
  407. logging.info("Error answering inline query\n{}".format(e))
  408. return
  409. async def handle_chosen_inline_result(self, update):
  410. """If chosen inline result id is in self.chosen_inline_result_handlers, call the related function passing the update as argument."""
  411. user_id = update['from']['id'] if 'from' in update else None
  412. if self.maintenance:
  413. return
  414. if user_id in self.chosen_inline_result_handlers:
  415. result_id = update['result_id']
  416. handlers = self.chosen_inline_result_handlers[user_id]
  417. if result_id in handlers:
  418. func = handlers[result_id]
  419. if asyncio.iscoroutinefunction(func):
  420. await func(update)
  421. else:
  422. func(update)
  423. return
  424. def set_inline_result_handler(self, user_id, result_id, func):
  425. """Associate a func to a result_id: when an inline result is chosen having that id, function will be passed the update as argument."""
  426. if type(user_id) is dict:
  427. user_id = user_id['from']['id']
  428. assert type(user_id) is int, "user_id must be int!"
  429. result_id = str(result_id) # Query result ids are parsed as str by telegram
  430. assert callable(func), "func must be a callable"
  431. if user_id not in self.chosen_inline_result_handlers:
  432. self.chosen_inline_result_handlers[user_id] = {}
  433. self.chosen_inline_result_handlers[user_id][result_id] = func
  434. return
  435. async def handle_callback_query(self, update):
  436. """Get an answer from the callback handler associated to the query prefix.
  437. The answer is used to edit the source message or send new ones if text is longer than single message limit.
  438. Anyway, the query is answered, otherwise the client would hang and the bot would look like idle.
  439. """
  440. answer = None
  441. if self.maintenance:
  442. answer = remove_html_tags(self.maintenance_message[:45])
  443. else:
  444. data = update['data']
  445. for start_text, handler in self.callback_handlers.items():
  446. answerer = handler['function']
  447. if data.startswith(start_text):
  448. if asyncio.iscoroutinefunction(answerer):
  449. answer = await answerer(update)
  450. else:
  451. answer = answerer(update)
  452. break
  453. if answer:
  454. if type(answer) is str:
  455. answer = {'text': answer}
  456. if type(answer) is not dict:
  457. return
  458. if 'edit' in answer:
  459. if 'message' in update:
  460. message_identifier = telepot.message_identifier(update['message'])
  461. else:
  462. message_identifier = telepot.message_identifier(update)
  463. edit = answer['edit']
  464. reply_markup = edit['reply_markup'] if 'reply_markup' in edit else None
  465. text = edit['text'] if 'text' in edit else None
  466. caption = edit['caption'] if 'caption' in edit else None
  467. parse_mode = edit['parse_mode'] if 'parse_mode' in edit else None
  468. disable_web_page_preview = edit['disable_web_page_preview'] if 'disable_web_page_preview' in edit else None
  469. try:
  470. if 'text' in edit:
  471. if len(text) > self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 200:
  472. if 'from' in update:
  473. await self.send_message(
  474. chat_id=update['from']['id'],
  475. text=text,
  476. reply_markup=reply_markup,
  477. parse_mode=parse_mode,
  478. disable_web_page_preview=disable_web_page_preview
  479. )
  480. else:
  481. await self.editMessageText(
  482. msg_identifier=message_identifier,
  483. text=text,
  484. parse_mode=parse_mode,
  485. disable_web_page_preview=disable_web_page_preview,
  486. reply_markup=reply_markup
  487. )
  488. elif 'caption' in edit:
  489. await self.editMessageCaption(
  490. msg_identifier=message_identifier,
  491. caption=caption,
  492. reply_markup=reply_markup
  493. )
  494. elif 'reply_markup' in edit:
  495. await self.editMessageReplyMarkup(
  496. msg_identifier=message_identifier,
  497. reply_markup=reply_markup
  498. )
  499. except Exception as e:
  500. logging.info("Message was not modified:\n{}".format(e))
  501. text = answer['text'][:180] if 'text' in answer else None
  502. show_alert = answer['show_alert'] if 'show_alert' in answer else None
  503. cache_time = answer['cache_time'] if 'cache_time' in answer else None
  504. try:
  505. await self.answerCallbackQuery(
  506. callback_query_id=update['id'],
  507. text=text,
  508. show_alert=show_alert,
  509. cache_time=cache_time
  510. )
  511. except telepot.exception.TelegramError as e:
  512. logging.error(e)
  513. else:
  514. try:
  515. await self.answerCallbackQuery(callback_query_id=update['id'])
  516. except telepot.exception.TelegramError as e:
  517. logging.error(e)
  518. return
  519. async def handle_text_message(self, update):
  520. """Answer to chat text messages.
  521. 1) Ignore bot name (case-insensitive) and search bot custom parsers, commands, aliases and parsers for an answerer.
  522. 2) Get an answer from answerer(update).
  523. 3) Send it to the user.
  524. """
  525. answerer, answer = None, None
  526. # Lower text and replace only bot's tag, meaning that `/command@OtherBot` will be ignored.
  527. text = update['text'].lower().replace('@{}'.format(self.name.lower()),'')
  528. user_id = update['from']['id'] if 'from' in update else None
  529. if self.maintenance and not any(
  530. text.startswith(x)
  531. for x in ('/coma', '/restart')
  532. ):
  533. if update['chat']['id']>0:
  534. answer = self.maintenance_message
  535. elif user_id in self.custom_parsers:
  536. answerer = self.custom_parsers[user_id]
  537. del self.custom_parsers[user_id]
  538. elif text.startswith('/'):
  539. command = text.split()[0].strip(' /@')
  540. if command in self.commands:
  541. answerer = self.commands[command]['function']
  542. elif update['chat']['id']>0:
  543. answer = self.unknown_command_message
  544. else:
  545. # If text starts with an alias (case insensitive: text and alias are both .lower()):
  546. for alias, parser in self.aliases.items():
  547. if text.startswith(alias.lower()):
  548. answerer = parser
  549. break
  550. # If update matches any parser
  551. for check_function, parser in self.parsers.items():
  552. if (
  553. parser['argument'] == 'text'
  554. and check_function(text)
  555. ) or (
  556. parser['argument'] == 'update'
  557. and check_function(update)
  558. ):
  559. answerer = parser['function']
  560. break
  561. if answerer:
  562. if asyncio.iscoroutinefunction(answerer):
  563. answer = await answerer(update)
  564. else:
  565. answer = answerer(update)
  566. if answer:
  567. try:
  568. return await self.send_message(answer=answer, chat_id=update)
  569. except Exception as e:
  570. logging.error("Failed to process answer:\n{}".format(e), exc_info=True)
  571. async def handle_pinned_message(self, update):
  572. """Handle pinned message chat action."""
  573. if self.maintenance:
  574. return
  575. answerer = None
  576. for criteria, handler in self.chat_actions['pinned'].items():
  577. if criteria(update):
  578. answerer = handler['function']
  579. break
  580. if answerer is None:
  581. return
  582. elif asyncio.iscoroutinefunction(answerer):
  583. answer = await answerer(update)
  584. else:
  585. answer = answerer(update)
  586. if answer:
  587. try:
  588. return await self.send_message(answer=answer, chat_id=update['chat']['id'])
  589. except Exception as e:
  590. logging.error("Failed to process answer:\n{}".format(e), exc_info=True)
  591. return
  592. async def handle_photo_message(self, update):
  593. """Handle photo chat message"""
  594. user_id = update['from']['id'] if 'from' in update else None
  595. answerer, answer = None, None
  596. if self.maintenance:
  597. if update['chat']['id']>0:
  598. answer = self.maintenance_message
  599. elif user_id in self.custom_photo_parsers:
  600. answerer = self.custom_photo_parsers[user_id]
  601. del self.custom_photo_parsers[user_id]
  602. if answerer:
  603. if asyncio.iscoroutinefunction(answerer):
  604. answer = await answerer(update)
  605. else:
  606. answer = answerer(update)
  607. if answer:
  608. try:
  609. return await self.send_message(answer=answer, chat_id=update)
  610. except Exception as e:
  611. logging.error("Failed to process answer:\n{}".format(e), exc_info=True)
  612. return
  613. def set_custom_parser(self, parser, update=None, user=None):
  614. """Set a custom parser for the user.
  615. Any chat message update coming from the user will be handled by this custom parser instead of default parsers (commands, aliases and text parsers).
  616. Custom parsers last one single use, but their handler can call this function to provide multiple tries.
  617. """
  618. if user and type(user) is int:
  619. pass
  620. elif type(update) is int:
  621. user = update
  622. elif type(user) is dict:
  623. user = user['from']['id'] if 'from' in user and 'id' in user['from'] else None
  624. elif not user and type(update) is dict:
  625. user = update['from']['id'] if 'from' in update and 'id' in update['from'] else None
  626. else:
  627. raise TypeError('Invalid user.\nuser: {}\nupdate: {}'.format(user, update))
  628. if not type(user) is int:
  629. raise TypeError('User {} is not an int id'.format(user))
  630. if not callable(parser):
  631. raise TypeError('Parser {} is not a callable'.format(parser.__name__))
  632. self.custom_parsers[user] = parser
  633. return
  634. def set_custom_photo_parser(self, parser, update=None, user=None):
  635. """Set a custom photo parser for the user.
  636. Any photo chat update coming from the user will be handled by this custom parser instead of default parsers.
  637. Custom photo parsers last one single use, but their handler can call this function to provide multiple tries.
  638. """
  639. if user and type(user) is int:
  640. pass
  641. elif type(update) is int:
  642. user = update
  643. elif type(user) is dict:
  644. user = user['from']['id'] if 'from' in user and 'id' in user['from'] else None
  645. elif not user and type(update) is dict:
  646. user = update['from']['id'] if 'from' in update and 'id' in update['from'] else None
  647. else:
  648. raise TypeError('Invalid user.\nuser: {}\nupdate: {}'.format(user, update))
  649. if not type(user) is int:
  650. raise TypeError('User {} is not an int id'.format(user))
  651. if not callable(parser):
  652. raise TypeError('Parser {} is not a callable'.format(parser.__name__))
  653. self.custom_photo_parsers[user] = parser
  654. return
  655. def command(self, command, aliases=None, show_in_keyboard=False, descr="", auth='admin'):
  656. """
  657. Decorator: `@bot.command(*args)`
  658. When a message text starts with `/command[@bot_name]`, or with an alias, it gets passed to the decorated function.
  659. `command` is the command name (with or without /)
  660. `aliases` is a list of aliases
  661. `show_in_keyboard`, if True, makes first alias appear in default_keyboard
  662. `descr` is a description
  663. `auth` is the lowest authorization level needed to run the command
  664. """
  665. command = command.replace('/','').lower()
  666. if not isinstance(command, str):
  667. raise TypeError('Command {} is not a string'.format(command))
  668. if aliases:
  669. if not isinstance(aliases, list):
  670. raise TypeError('Aliases is not a list: {}'.format(aliases))
  671. for alias in aliases:
  672. if not isinstance(alias, str):
  673. raise TypeError('Alias {} is not a string'.format(alias))
  674. def decorator(func):
  675. if asyncio.iscoroutinefunction(func):
  676. async def decorated(message):
  677. logging.info("COMMAND({}) @{} FROM({})".format(command, self.name, message['from'] if 'from' in message else message['chat']))
  678. if self.authorization_function(message, auth):
  679. return await func(message)
  680. return self.unauthorized_message
  681. else:
  682. def decorated(message):
  683. logging.info("COMMAND({}) @{} FROM({})".format(command, self.name, message['from'] if 'from' in message else message['chat']))
  684. if self.authorization_function(message, auth):
  685. return func(message)
  686. return self.unauthorized_message
  687. self.commands[command] = dict(
  688. function=decorated,
  689. descr=descr,
  690. auth=auth
  691. )
  692. if aliases:
  693. for alias in aliases:
  694. self.aliases[alias] = decorated
  695. if show_in_keyboard:
  696. self.default_reply_keyboard_elements.append(aliases[0])
  697. return decorator
  698. def parser(self, condition, descr='', auth='admin', argument='text'):
  699. """
  700. Decorator: `@bot.parser(condition)`
  701. If condition evaluates True when run on a message text (not starting with '/'), such decorated function gets called on update.
  702. Conditions of parsers are evaluated in order; when one is True, others will be skipped.
  703. `descr` is a description
  704. `auth` is the lowest authorization level needed to run the command
  705. """
  706. if not callable(condition):
  707. raise TypeError('Condition {} is not a callable'.format(condition.__name__))
  708. def decorator(func):
  709. if asyncio.iscoroutinefunction(func):
  710. async def decorated(message):
  711. logging.info("TEXT MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from']))
  712. if self.authorization_function(message, auth):
  713. return await func(message)
  714. return self.unauthorized_message
  715. else:
  716. def decorated(message):
  717. logging.info("TEXT MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from']))
  718. if self.authorization_function(message, auth):
  719. return func(message)
  720. return self.unauthorized_message
  721. self.parsers[condition] = dict(
  722. function=decorated,
  723. descr=descr,
  724. auth=auth,
  725. argument=argument
  726. )
  727. return decorator
  728. def pinned(self, condition, descr='', auth='admin'):
  729. """
  730. Decorator: `@bot.pinned(condition)`
  731. If condition evaluates True when run on a pinned_message update, such decorated function gets called on update.
  732. Conditions are evaluated in order; when one is True, others will be skipped.
  733. `descr` is a description
  734. `auth` is the lowest authorization level needed to run the command
  735. """
  736. if not callable(condition):
  737. raise TypeError('Condition {} is not a callable'.format(condition.__name__))
  738. def decorator(func):
  739. if asyncio.iscoroutinefunction(func):
  740. async def decorated(message):
  741. logging.info("CHAT ACTION MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from']))
  742. if self.authorization_function(message, auth):
  743. return await func(message)
  744. return# self.unauthorized_message
  745. else:
  746. def decorated(message):
  747. logging.info("CHAT ACTION MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from']))
  748. if self.authorization_function(message, auth):
  749. return func(message)
  750. return# self.unauthorized_message
  751. self.chat_actions['pinned'][condition] = dict(
  752. function=decorated,
  753. descr=descr,
  754. auth=auth
  755. )
  756. return decorator
  757. def button(self, data, descr='', auth='admin'):
  758. """
  759. Decorator: `@bot.button('example:///')`
  760. When a callback data text starts with <data>, it gets passed to the decorated function
  761. `descr` is a description
  762. `auth` is the lowest authorization level needed to run the command
  763. """
  764. if not isinstance(data, str):
  765. raise TypeError('Inline button callback_data {} is not a string'.format(data))
  766. def decorator(func):
  767. if asyncio.iscoroutinefunction(func):
  768. async def decorated(message):
  769. logging.info("INLINE BUTTON({}) @{} FROM({})".format(message['data'], self.name, message['from']))
  770. if self.authorization_function(message, auth):
  771. return await func(message)
  772. return self.unauthorized_message
  773. else:
  774. def decorated(message):
  775. logging.info("INLINE BUTTON({}) @{} FROM({})".format(message['data'], self.name, message['from']))
  776. if self.authorization_function(message, auth):
  777. return func(message)
  778. return self.unauthorized_message
  779. self.callback_handlers[data] = dict(
  780. function=decorated,
  781. descr=descr,
  782. auth=auth
  783. )
  784. return decorator
  785. def query(self, condition, descr='', auth='admin'):
  786. """
  787. Decorator: `@bot.query(example)`
  788. When an inline query matches the `condition` function, decorated function is called and passed the query update object as argument.
  789. `descr` is a description
  790. `auth` is the lowest authorization level needed to run the command
  791. """
  792. if not callable(condition):
  793. raise TypeError('Condition {} is not a callable'.format(condition.__name__))
  794. def decorator(func):
  795. if asyncio.iscoroutinefunction(func):
  796. async def decorated(message):
  797. logging.info("QUERY MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from']))
  798. if self.authorization_function(message, auth):
  799. return await func(message)
  800. return self.unauthorized_message
  801. else:
  802. def decorated(message):
  803. logging.info("QUERY MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from']))
  804. if self.authorization_function(message, auth):
  805. return func(message)
  806. return self.unauthorized_message
  807. self.inline_query_handlers[condition] = dict(
  808. function=decorated,
  809. descr=descr,
  810. auth=auth
  811. )
  812. return decorator
  813. def additional_task(self, when='BEFORE'):
  814. """Decorator: such decorated async functions get awaited BEFORE or AFTER messageloop"""
  815. when = when[0].lower()
  816. def decorator(func):
  817. if when == 'b':
  818. self.run_before_loop.append(func())
  819. elif when == 'a':
  820. self.run_after_loop.append(func())
  821. return decorator
  822. def set_default_keyboard(self, keyboard='set_default'):
  823. """Set a default keyboard for the bot.
  824. If a keyboard is not passed as argument, a default one is generated, based on aliases of commands.
  825. """
  826. if keyboard=='set_default':
  827. btns = [
  828. dict(
  829. text=x
  830. )
  831. for x in self.default_reply_keyboard_elements
  832. ]
  833. row_len = 2 if len(btns) < 4 else 3
  834. self._default_keyboard = dict(
  835. keyboard=make_lines_of_buttons(
  836. btns,
  837. row_len
  838. ),
  839. resize_keyboard=True
  840. )
  841. else:
  842. self._default_keyboard = keyboard
  843. return
  844. async def edit_message(self, update, *args, **kwargs):
  845. """Edit given update with given *args and **kwargs.
  846. Please note, that it is currently only possible to edit messages without reply_markup or with inline keyboards.
  847. """
  848. try:
  849. return await self.editMessageText(
  850. telepot.message_identifier(update),
  851. *args,
  852. **kwargs
  853. )
  854. except Exception as e:
  855. logging.error("{}".format(e))
  856. async def delete_message(self, update, *args, **kwargs):
  857. """Delete given update with given *args and **kwargs.
  858. Please note, that a bot can delete only messages sent by itself or sent in a group which it is administrator of.
  859. """
  860. try:
  861. return await self.deleteMessage(
  862. telepot.message_identifier(update),
  863. *args,
  864. **kwargs
  865. )
  866. except Exception as e:
  867. logging.error("{}".format(e))
  868. async def send_message(self, answer=dict(), chat_id=None, text='', parse_mode="HTML", disable_web_page_preview=None, disable_notification=None, reply_to_message_id=None, reply_markup=None):
  869. """Convenient method to call telepot.Bot(token).sendMessage
  870. All sendMessage **kwargs can be either **kwargs of send_message or key:val of answer argument.
  871. Messages longer than telegram limit will be split properly.
  872. Telegram flood limits won't be reached thanks to `await avoid_flooding(chat_id)`
  873. parse_mode will be checked and edited if necessary.
  874. Arguments will be checked and adapted.
  875. """
  876. if type(answer) is dict and 'chat_id' in answer:
  877. chat_id = answer['chat_id']
  878. # chat_id may simply be the update to which the bot should repy: get_chat_id is called
  879. if type(chat_id) is dict:
  880. chat_id = self.get_chat_id(chat_id)
  881. if type(answer) is str:
  882. text = answer
  883. if not reply_markup and chat_id > 0 and text!=self.unauthorized_message:
  884. reply_markup = self.default_keyboard
  885. elif type(answer) is dict:
  886. if 'text' in answer:
  887. text = answer['text']
  888. if 'parse_mode' in answer:
  889. parse_mode = answer['parse_mode']
  890. if 'disable_web_page_preview' in answer:
  891. disable_web_page_preview = answer['disable_web_page_preview']
  892. if 'disable_notification' in answer:
  893. disable_notification = answer['disable_notification']
  894. if 'reply_to_message_id' in answer:
  895. reply_to_message_id = answer['reply_to_message_id']
  896. if 'reply_markup' in answer:
  897. reply_markup = answer['reply_markup']
  898. elif not reply_markup and type(chat_id) is int and chat_id > 0 and text!=self.unauthorized_message:
  899. reply_markup = self.default_keyboard
  900. assert type(text) is str, "Text is not a string!"
  901. assert (
  902. type(chat_id) is int
  903. or (type(chat_id) is str and chat_id.startswith('@'))
  904. ), "Invalid chat_id:\n\t\t{}".format(chat_id)
  905. if not text:
  906. return
  907. parse_mode = str(parse_mode)
  908. text_chunks = split_text_gracefully(
  909. text=text,
  910. limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 100,
  911. parse_mode=parse_mode
  912. )
  913. n = len(text_chunks)
  914. for text_chunk in text_chunks:
  915. n-=1
  916. if parse_mode.lower() == "html":
  917. this_parse_mode = "HTML"
  918. # Check that all tags are well-formed
  919. if not markdown_check(
  920. text_chunk,
  921. [
  922. "<", ">",
  923. "code>", "bold>", "italic>",
  924. "b>", "i>", "a>", "pre>"
  925. ]
  926. ):
  927. this_parse_mode = "None"
  928. text_chunk = "!!![invalid markdown syntax]!!!\n\n" + text_chunk
  929. elif parse_mode != "None":
  930. this_parse_mode = "Markdown"
  931. # Check that all markdowns are well-formed
  932. if not markdown_check(
  933. text_chunk,
  934. [
  935. "*", "_", "`"
  936. ]
  937. ):
  938. this_parse_mode = "None"
  939. text_chunk = "!!![invalid markdown syntax]!!!\n\n" + text_chunk
  940. else:
  941. this_parse_mode = parse_mode
  942. this_reply_markup = reply_markup if n==0 else None
  943. try:
  944. await self.avoid_flooding(chat_id)
  945. result = await self.sendMessage(
  946. chat_id=chat_id,
  947. text=text_chunk,
  948. parse_mode=this_parse_mode,
  949. disable_web_page_preview=disable_web_page_preview,
  950. disable_notification=disable_notification,
  951. reply_to_message_id=reply_to_message_id,
  952. reply_markup=this_reply_markup
  953. )
  954. except Exception as e:
  955. logging.debug(e, exc_info=False) # Set exc_info=True for more information
  956. result = e
  957. return result
  958. async def send_photo(self, chat_id=None, answer={}, photo=None, caption='', parse_mode='HTML', disable_notification=None, reply_to_message_id=None,reply_markup=None, use_stored=True, second_chance=False):
  959. """Convenient method to call telepot.Bot(token).sendPhoto
  960. All sendPhoto **kwargs can be either **kwargs of send_message or key:val of answer argument.
  961. Captions longer than telegram limit will be shortened gently.
  962. Telegram flood limits won't be reached thanks to `await avoid_flooding(chat_id)`
  963. Most arguments will be checked and adapted.
  964. If use_stored is set to True, the bot will store sent photo telegram_id and use it for faster sending next times (unless future errors).
  965. Sending photos by their file_id already stored on telegram servers is way faster: that's why bot stores and uses this info, if required.
  966. A second_chance is given to send photo on error.
  967. """
  968. if 'chat_id' in answer:
  969. chat_id = answer['chat_id']
  970. # chat_id may simply be the update to which the bot should repy: get_chat_id is called
  971. if type(chat_id) is dict:
  972. chat_id = self.get_chat_id(chat_id)
  973. assert (
  974. type(chat_id) is int
  975. or (type(chat_id) is str and chat_id.startswith('@'))
  976. ), "Invalid chat_id:\n\t\t{}".format(chat_id)
  977. if 'photo' in answer:
  978. photo = answer['photo']
  979. assert photo is not None, "Null photo!"
  980. if 'caption' in answer:
  981. caption = answer['caption']
  982. if 'parse_mode' in answer:
  983. parse_mode = answer['parse_mode']
  984. if 'disable_notification' in answer:
  985. disable_notification = answer['disable_notification']
  986. if 'reply_to_message_id' in answer:
  987. reply_to_message_id = answer['reply_to_message_id']
  988. if 'reply_markup' in answer:
  989. reply_markup = answer['reply_markup']
  990. already_sent = False
  991. if type(photo) is str:
  992. photo_url = photo
  993. with self.db as db:
  994. already_sent = db['sent_pictures'].find_one(url=photo_url, errors=False)
  995. if already_sent and use_stored:
  996. photo = already_sent['file_id']
  997. already_sent = True
  998. else:
  999. already_sent = False
  1000. if not any(photo_url.startswith(x) for x in ['http', 'www']):
  1001. with io.BytesIO() as buffered_picture:
  1002. with open("{}/{}".format(self.path, photo_url), 'rb') as photo_file:
  1003. buffered_picture.write(photo_file.read())
  1004. photo = buffered_picture.getvalue()
  1005. caption = escape_html_chars(caption)
  1006. if len(caption) > 199:
  1007. new_caption = ''
  1008. tag = False
  1009. tag_body = False
  1010. count = 0
  1011. temp = ''
  1012. for char in caption:
  1013. if tag and char == '>':
  1014. tag = False
  1015. elif char == '<':
  1016. tag = True
  1017. tag_body = not tag_body
  1018. elif not tag:
  1019. count += 1
  1020. if count == 199:
  1021. break
  1022. temp += char
  1023. if not tag_body:
  1024. new_caption += temp
  1025. temp = ''
  1026. caption = new_caption
  1027. sent = None
  1028. try:
  1029. await self.avoid_flooding(chat_id)
  1030. sent = await self.sendPhoto(
  1031. chat_id=chat_id,
  1032. photo=photo,
  1033. caption=caption,
  1034. parse_mode=parse_mode,
  1035. disable_notification=disable_notification,
  1036. reply_to_message_id=reply_to_message_id,
  1037. reply_markup=reply_markup
  1038. )
  1039. if isinstance(sent, Exception):
  1040. raise Exception("SendingFailed")
  1041. except Exception as e:
  1042. logging.error("Error sending photo\n{}".format(e), exc_info=False) # Set exc_info=True for more information
  1043. if already_sent:
  1044. with self.db as db:
  1045. db['sent_pictures'].update(
  1046. dict(
  1047. url=photo_url,
  1048. errors=True
  1049. ),
  1050. ['url']
  1051. )
  1052. if not second_chance:
  1053. logging.info("Trying again (only once)...")
  1054. sent = await self.send_photo(
  1055. chat_id=chat_id,
  1056. answer=answer,
  1057. photo=photo,
  1058. caption=caption,
  1059. parse_mode=parse_mode,
  1060. disable_notification=disable_notification,
  1061. reply_to_message_id=reply_to_message_id,
  1062. reply_markup=reply_markup,
  1063. second_chance=True
  1064. )
  1065. if (
  1066. sent is not None
  1067. and hasattr(sent, '__getitem__')
  1068. and 'photo' in sent
  1069. and len(sent['photo'])>0
  1070. and 'file_id' in sent['photo'][0]
  1071. and (not already_sent)
  1072. and use_stored
  1073. ):
  1074. with self.db as db:
  1075. db['sent_pictures'].insert(
  1076. dict(
  1077. url=photo_url,
  1078. file_id=sent['photo'][0]['file_id'],
  1079. errors=False
  1080. )
  1081. )
  1082. return sent
  1083. async def send_and_destroy(self, chat_id, answer, timer=60, mode='text', **kwargs):
  1084. """Send a message or photo and delete it after `timer` seconds"""
  1085. if mode == 'text':
  1086. sent_message = await self.send_message(
  1087. chat_id=chat_id,
  1088. answer=answer,
  1089. **kwargs
  1090. )
  1091. elif mode == 'pic':
  1092. sent_message = await self.send_photo(
  1093. chat_id=chat_id,
  1094. answer=answer,
  1095. **kwargs
  1096. )
  1097. if sent_message is None:
  1098. return
  1099. self.to_be_destroyed.append(sent_message)
  1100. await asyncio.sleep(timer)
  1101. if await self.delete_message(sent_message):
  1102. self.to_be_destroyed.remove(sent_message)
  1103. return
  1104. async def wait_and_obscure(self, update, when, inline_message_id):
  1105. """Obscure an inline_message `timer` seconds after sending it, by editing its text or caption.
  1106. At the moment Telegram won't let bots delete sent inline query results."""
  1107. if type(when) is int:
  1108. when = datetime.datetime.now() + datetime.timedelta(seconds=when)
  1109. assert type(when) is datetime.datetime, "when must be a datetime instance or a number of seconds (int) to be awaited"
  1110. if 'inline_message_id' not in update:
  1111. logging.info("This inline query result owns no inline_keyboard, so it can't be modified")
  1112. return
  1113. inline_message_id = update['inline_message_id']
  1114. self.to_be_obscured.append(inline_message_id)
  1115. while datetime.datetime.now() < when:
  1116. await sleep_until(when)
  1117. try:
  1118. await self.editMessageCaption(inline_message_id, text="Time over")
  1119. except:
  1120. try:
  1121. await self.editMessageText(inline_message_id, text="Time over")
  1122. except Exception as e:
  1123. logging.error("Couldn't obscure message\n{}\n\n{}".format(inline_message_id,e))
  1124. self.to_be_obscured.remove(inline_message_id)
  1125. return
  1126. async def continue_running(self):
  1127. """If bot can be got, sets name and telegram_id, awaits preliminary tasks and starts getting updates from telegram.
  1128. If bot can't be got, restarts all bots in 5 minutes."""
  1129. try:
  1130. me = await self.getMe()
  1131. self.bot_name = me["username"]
  1132. self.telegram_id = me['id']
  1133. except:
  1134. logging.error("Could not get bot")
  1135. await asyncio.sleep(5*60)
  1136. self.restart_bots()
  1137. return
  1138. for task in self.run_before_loop:
  1139. await task
  1140. self.set_default_keyboard()
  1141. asyncio.ensure_future(
  1142. self.message_loop(handler=self.routing_table)
  1143. )
  1144. return
  1145. def stop_bots(self):
  1146. """Causes the script to exit"""
  1147. Bot.stop = True
  1148. def restart_bots(self):
  1149. """Causes the script to restart.
  1150. Actually, you need to catch Bot.stop state when Bot.run() returns and handle the situation yourself."""
  1151. Bot.stop = "Restart"
  1152. @classmethod
  1153. async def check_task(cls):
  1154. """Await until cls.stop, then end session and return"""
  1155. for bot in cls.instances.values():
  1156. asyncio.ensure_future(bot.continue_running())
  1157. while not cls.stop:
  1158. await asyncio.sleep(10)
  1159. return await cls.end_session()
  1160. @classmethod
  1161. async def end_session(cls):
  1162. """Run after stop, before the script exits.
  1163. Await final tasks, obscure and delete pending messages, log current operation (stop/restart)."""
  1164. for bot in cls.instances.values():
  1165. for task in bot.run_after_loop:
  1166. await task
  1167. for message in bot.to_be_destroyed:
  1168. try:
  1169. await bot.delete_message(message)
  1170. except Exception as e:
  1171. logging.error("Couldn't delete message\n{}\n\n{}".format(message,e))
  1172. for inline_message_id in bot.to_be_obscured:
  1173. try:
  1174. await bot.editMessageCaption(inline_message_id, text="Time over")
  1175. except:
  1176. try:
  1177. await bot.editMessageText(inline_message_id, text="Time over")
  1178. except Exception as e:
  1179. logging.error("Couldn't obscure message\n{}\n\n{}".format(inline_message_id,e))
  1180. if cls.stop=="Restart":
  1181. logging.info("\n\t\t---Restart!---")
  1182. elif cls.stop=="KeyboardInterrupt":
  1183. logging.info("Stopped by KeyboardInterrupt.")
  1184. else:
  1185. logging.info("Stopped gracefully by user.")
  1186. return
  1187. @classmethod
  1188. def run(cls, loop=None):
  1189. """
  1190. Call this method to run the async bots.
  1191. """
  1192. if not loop:
  1193. loop = asyncio.get_event_loop()
  1194. logging.info(
  1195. "{sep}{subjvb} STARTED{sep}".format(
  1196. sep='-'*10,
  1197. subjvb='BOT HAS' if len(cls.instances)==1 else 'BOTS HAVE'
  1198. )
  1199. )
  1200. try:
  1201. loop.run_until_complete(cls.check_task())
  1202. except KeyboardInterrupt:
  1203. logging.info('\n\t\tYour script received a KeyboardInterrupt signal, your bot{} being stopped.'.format(
  1204. 's are' if len(cls.instances)>1 else ' is'
  1205. ))
  1206. cls.stop = "KeyboardInterrupt"
  1207. loop.run_until_complete(cls.end_session())
  1208. except Exception as e:
  1209. logging.error('\nYour bot has been stopped. with error \'{}\''.format(e), exc_info=True)
  1210. logging.info(
  1211. "{sep}{subjvb} STOPPED{sep}".format(
  1212. sep='-'*10,
  1213. subjvb='BOT HAS' if len(cls.instances)==1 else 'BOTS HAVE'
  1214. )
  1215. )
  1216. return