Queer European MD passionate about IT

custombot.py 86 KB

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