Queer European MD passionate about IT

utilities.py 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612
  1. """Useful functions used by Davte when programming in python."""
  2. # Standard library modules
  3. import asyncio
  4. import collections
  5. import csv
  6. import datetime
  7. from difflib import SequenceMatcher
  8. import inspect
  9. import io
  10. import json
  11. import logging
  12. import os
  13. import random
  14. import string
  15. import time
  16. # Third party modules
  17. import aiohttp
  18. from aiohttp import web
  19. from bs4 import BeautifulSoup
  20. def sumif(iterable, condition):
  21. """Sum all `iterable` items matching `condition`."""
  22. return sum(
  23. filter(
  24. condition,
  25. iterable
  26. )
  27. )
  28. def markdown_check(text, symbols):
  29. """Check that all `symbols` occur an even number of times in `text`."""
  30. for s in symbols:
  31. if (len(text.replace(s, "")) - len(text)) % 2 != 0:
  32. return False
  33. return True
  34. def shorten_text(text, limit, symbol="[...]"):
  35. """Return a given text truncated at limit if longer than limit.
  36. On truncation, add symbol.
  37. """
  38. assert type(text) is str and type(symbol) is str and type(limit) is int
  39. if len(text) <= limit:
  40. return text
  41. return text[:limit-len(symbol)] + symbol
  42. def extract(text, starter=None, ender=None):
  43. """Return string in text between starter and ender.
  44. If starter is None, truncate at ender.
  45. """
  46. if starter and starter in text:
  47. text = text.partition(starter)[2]
  48. if ender:
  49. return text.partition(ender)[0]
  50. return text
  51. def make_button(text=None, callback_data='',
  52. prefix='', delimiter='|', data=[]):
  53. """Return a Telegram bot API-compliant button.
  54. callback_data can be either a ready-to-use string or a
  55. prefix + delimiter-joined data.
  56. If callback_data exceeds Telegram limits (currently 60 characters),
  57. it gets truncated at the last delimiter before that limit.
  58. If absent, text is the same as callback_data.
  59. """
  60. if len(data):
  61. callback_data += delimiter.join(map(str, data))
  62. callback_data = "{p}{c}".format(
  63. p=prefix,
  64. c=callback_data
  65. )
  66. if len(callback_data) > 60:
  67. callback_data = callback_data[:61]
  68. callback_data = callback_data[:-1-callback_data[::-1].find(delimiter)]
  69. if text is None:
  70. text = callback_data
  71. return dict(
  72. text=text,
  73. callback_data=callback_data
  74. )
  75. def mkbtn(x, y):
  76. """Backward compatibility.
  77. Warning: this function will be deprecated sooner or later,
  78. stop using it and migrate to make_button.
  79. """
  80. return make_button(text=x, callback_data=y)
  81. def make_lines_of_buttons(buttons, row_len=1):
  82. """Split `buttons` list in a list of lists having length = `row_len`."""
  83. return [
  84. buttons[i:i + row_len]
  85. for i in range(
  86. 0,
  87. len(buttons),
  88. row_len
  89. )
  90. ]
  91. def make_inline_keyboard(buttons, row_len=1):
  92. """Make a Telegram API compliant inline keyboard."""
  93. return dict(
  94. inline_keyboard=make_lines_of_buttons(
  95. buttons,
  96. row_len
  97. )
  98. )
  99. async def async_get(url, mode='json', **kwargs):
  100. """Make an async get request.
  101. `mode`s allowed:
  102. * html
  103. * json
  104. * string
  105. Additional **kwargs may be passed.
  106. """
  107. if 'mode' in kwargs:
  108. mode = kwargs['mode']
  109. del kwargs['mode']
  110. return await async_request(
  111. url,
  112. type='get',
  113. mode=mode,
  114. **kwargs
  115. )
  116. async def async_post(url, mode='html', **kwargs):
  117. """Make an async post request.
  118. `mode`s allowed:
  119. * html
  120. * json
  121. * string
  122. Additional **kwargs may be passed (will be converted to `data`).
  123. """
  124. return await async_request(
  125. url,
  126. type='post',
  127. mode=mode,
  128. **kwargs
  129. )
  130. async def async_request(url, type='get', mode='json', encoding='utf-8',
  131. **kwargs):
  132. """Make an async html request.
  133. `types` allowed
  134. * get
  135. * post
  136. `mode`s allowed:
  137. * html
  138. * json
  139. * string
  140. * picture
  141. Additional **kwargs may be passed.
  142. """
  143. try:
  144. async with aiohttp.ClientSession() as s:
  145. async with (
  146. s.get(url, timeout=30)
  147. if type == 'get'
  148. else s.post(url, timeout=30, data=kwargs)
  149. ) as r:
  150. result = await r.read()
  151. if mode in ['html', 'json', 'string']:
  152. result = result.decode(encoding)
  153. except Exception as e:
  154. logging.error(
  155. 'Error making async request to {}:\n{}'.format(
  156. url,
  157. e
  158. ),
  159. exc_info=False
  160. ) # Set exc_info=True to debug
  161. return e
  162. if mode == 'json':
  163. try:
  164. result = json.loads(
  165. result
  166. )
  167. except json.decoder.JSONDecodeError:
  168. result = {}
  169. elif mode == 'html':
  170. result = BeautifulSoup(result, "html.parser")
  171. elif mode == 'string':
  172. result = result
  173. return result
  174. def json_read(file_, default={}, encoding='utf-8', **kwargs):
  175. """Return json parsing of `file_`, or `default` if file does not exist.
  176. `encoding` refers to how the file should be read.
  177. `kwargs` will be passed to json.load()
  178. """
  179. if not os.path.isfile(file_):
  180. return default
  181. with open(file_, "r", encoding=encoding) as f:
  182. return json.load(f, **kwargs)
  183. def json_write(what, file_, encoding='utf-8', **kwargs):
  184. """Store `what` in json `file_`.
  185. `encoding` refers to how the file should be written.
  186. `kwargs` will be passed to json.dump()
  187. """
  188. with open(file_, "w", encoding=encoding) as f:
  189. return json.dump(what, f, indent=4, **kwargs)
  190. def csv_read(file_, default=[], encoding='utf-8',
  191. delimiter=',', quotechar='"', **kwargs):
  192. """Return csv parsing of `file_`, or `default` if file does not exist.
  193. `encoding` refers to how the file should be read.
  194. `delimiter` is the separator of fields.
  195. `quotechar` is the string delimiter.
  196. `kwargs` will be passed to csv.reader()
  197. """
  198. if not os.path.isfile(file_):
  199. return default
  200. result = []
  201. keys = []
  202. with open(file_, newline='', encoding=encoding) as csv_file:
  203. csv_reader = csv.reader(
  204. csv_file,
  205. delimiter=delimiter,
  206. quotechar=quotechar,
  207. **kwargs
  208. )
  209. for row in csv_reader:
  210. if not keys:
  211. keys = row
  212. continue
  213. item = collections.OrderedDict()
  214. for key, val in zip(keys, row):
  215. item[key] = val
  216. result.append(item)
  217. return result
  218. def csv_write(info=[], file_='output.csv', encoding='utf-8',
  219. delimiter=',', quotechar='"', **kwargs):
  220. """Store `info` in CSV `file_`.
  221. `encoding` refers to how the file should be read.
  222. `delimiter` is the separator of fields.
  223. `quotechar` is the string delimiter.
  224. `encoding` refers to how the file should be written.
  225. `kwargs` will be passed to csv.writer()
  226. """
  227. assert (
  228. type(info) is list
  229. and len(info) > 0
  230. ), "info must be a non-empty list"
  231. assert all(
  232. isinstance(row, dict)
  233. for row in info
  234. ), "Rows must be dictionaries!"
  235. with open(file_, 'w', newline='', encoding=encoding) as csv_file:
  236. csv_writer = csv.writer(
  237. csv_file,
  238. delimiter=delimiter,
  239. quotechar=quotechar,
  240. **kwargs
  241. )
  242. csv_writer.writerow(info[0].keys())
  243. for row in info:
  244. csv_writer.writerow(row.values())
  245. return
  246. class MyOD(collections.OrderedDict):
  247. """Subclass of OrderedDict.
  248. It features `get_by_val` and `get_by_key_val` methods.
  249. """
  250. def __init__(self, *args, **kwargs):
  251. """Return a MyOD instance."""
  252. super().__init__(*args, **kwargs)
  253. self._anti_list_casesensitive = None
  254. self._anti_list_caseinsensitive = None
  255. @property
  256. def anti_list_casesensitive(self):
  257. """Case-sensitive reverse dictionary.
  258. Keys and values are swapped.
  259. """
  260. if not self._anti_list_casesensitive:
  261. self._anti_list_casesensitive = {
  262. val: key
  263. for key, val in self.items()
  264. }
  265. return self._anti_list_casesensitive
  266. @property
  267. def anti_list_caseinsensitive(self):
  268. """Case-sensitive reverse dictionary.
  269. Keys and values are swapped and lowered.
  270. """
  271. if not self._anti_list_caseinsensitive:
  272. self._anti_list_caseinsensitive = {
  273. (val.lower() if type(val) is str else val): key
  274. for key, val in self.items()
  275. }
  276. return self._anti_list_caseinsensitive
  277. def get_by_val(self, val, case_sensitive=True):
  278. """Get key pointing to given val.
  279. Can be case-sensitive or insensitive.
  280. MyOD[key] = val <-- MyOD.get(key) = val <--> MyOD.get_by_val(val) = key
  281. """
  282. return (
  283. self.anti_list_casesensitive
  284. if case_sensitive
  285. else self.anti_list_caseinsensitive
  286. )[val]
  287. def get_by_key_val(self, key, val,
  288. case_sensitive=True, return_value=False):
  289. """Get key (or val) of a dict-like object having key == val.
  290. Perform case-sensitive or insensitive search.
  291. """
  292. for x, y in self.items():
  293. if (
  294. (
  295. y[key] == val
  296. and case_sensitive
  297. ) or (
  298. y[key].lower() == val.lower()
  299. and not case_sensitive
  300. )
  301. ):
  302. return (
  303. y if return_value
  304. else x
  305. )
  306. return None
  307. def line_drawing_unordered_list(l):
  308. """Draw an old-fashioned unordered list.
  309. Unorderd list example
  310. ├ An element
  311. ├ Another element
  312. └Last element
  313. """
  314. result = ""
  315. if l:
  316. for x in l[:-1]:
  317. result += "├ {}\n".format(x)
  318. result += "└ {}".format(l[-1])
  319. return result
  320. def str_to_datetime(d):
  321. """Convert string to datetime.
  322. Dataset library often casts datetimes to str, this is a workaround.
  323. """
  324. if isinstance(d, datetime.datetime):
  325. return d
  326. return datetime.datetime.strptime(
  327. d,
  328. '%Y-%m-%d %H:%M:%S.%f'
  329. )
  330. def datetime_to_str(d):
  331. """Cast datetime to string."""
  332. if isinstance(d, str):
  333. d = str_to_datetime(d)
  334. if not isinstance(d, datetime.datetime):
  335. raise TypeError(
  336. 'Input of datetime_to_str function must be a datetime.datetime '
  337. 'object. Output is a str'
  338. )
  339. return '{:%Y-%m-%d %H:%M:%S.%f}'.format(d)
  340. class MyCounter():
  341. """Counter object, with a `lvl` method incrementing `n` property."""
  342. def __init__(self):
  343. """Initialize and get MyCounter instance."""
  344. self._n = 0
  345. return
  346. def lvl(self):
  347. """Increments and return self.n."""
  348. self._n += 1
  349. return self.n
  350. def reset(self):
  351. """Set self.n = 0."""
  352. self._n = 0
  353. return self.n
  354. def n(self):
  355. """Counter's value."""
  356. return self._n
  357. def wrapper(func, *args, **kwargs):
  358. """Wrap a function so that it can be later called with one argument."""
  359. def wrapped(update):
  360. return func(update, *args, **kwargs)
  361. return wrapped
  362. async def async_wrapper(coroutine, *args1, **kwargs1):
  363. """Wrap a `coroutine` so that it can be later awaited with more arguments.
  364. Set some of the arguments, let the coroutine be awaited with the rest of
  365. them later.
  366. The wrapped coroutine will always pass only supported parameters to
  367. `coroutine`.
  368. Example:
  369. ```
  370. import asyncio
  371. from davtelepot.utilities import async_wrapper
  372. async def printer(a, b, c, d):
  373. print(a, a+b, b+c, c+d)
  374. return
  375. async def main():
  376. my_coroutine = await async_wrapper(
  377. printer,
  378. c=3, d=2
  379. )
  380. await my_coroutine(a=1, b=5)
  381. asyncio.get_event_loop().run_until_complete(main())
  382. ```
  383. """
  384. async def wrapped_coroutine(*args2, **kwargs2):
  385. # Update keyword arguments
  386. kwargs1.update(kwargs2)
  387. # Pass only supported arguments
  388. kwargs = {
  389. name: argument
  390. for name, argument in kwargs1.items()
  391. if name in inspect.signature(
  392. coroutine
  393. ).parameters
  394. }
  395. return await coroutine(*args1, *args2, **kwargs)
  396. return wrapped_coroutine
  397. def forwarded(by=None):
  398. """Check that update is forwarded, optionally `by` someone in particular.
  399. Decorator: such decorated functions have effect only if update
  400. is forwarded from someone (you can specify `by` whom).
  401. """
  402. def is_forwarded_by(update, by):
  403. if 'forward_from' not in update:
  404. return False
  405. if by and update['forward_from']['id'] != by:
  406. return False
  407. return True
  408. def decorator(view_func):
  409. if asyncio.iscoroutinefunction(view_func):
  410. async def decorated(update):
  411. if is_forwarded_by(update, by):
  412. return await view_func(update)
  413. else:
  414. def decorated(update):
  415. if is_forwarded_by(update, by):
  416. return view_func(update)
  417. return decorated
  418. return decorator
  419. def chat_selective(chat_id=None):
  420. """Check that update comes from a chat, optionally having `chat_id`.
  421. Such decorated functions have effect only if update comes from
  422. a specific (if `chat_id` is given) or generic chat.
  423. """
  424. def check_function(update, chat_id):
  425. if 'chat' not in update:
  426. return False
  427. if chat_id:
  428. if update['chat']['id'] != chat_id:
  429. return False
  430. return True
  431. def decorator(view_func):
  432. if asyncio.iscoroutinefunction(view_func):
  433. async def decorated(update):
  434. if check_function(update, chat_id):
  435. return await view_func(update)
  436. else:
  437. def decorated(update):
  438. if check_function(update, chat_id):
  439. return view_func(update)
  440. return decorated
  441. return decorator
  442. async def sleep_until(when):
  443. """Sleep until now > `when`.
  444. `when` could be a datetime.datetime or a datetime.timedelta instance.
  445. """
  446. if not (
  447. isinstance(when, datetime.datetime)
  448. or isinstance(when, datetime.timedelta)
  449. ):
  450. raise TypeError(
  451. "sleep_until takes a datetime.datetime or datetime.timedelta "
  452. "object as argument!"
  453. )
  454. if isinstance(when, datetime.datetime):
  455. delta = when - datetime.datetime.now()
  456. elif isinstance(when, datetime.timedelta):
  457. delta = when
  458. if delta.days >= 0:
  459. await asyncio.sleep(
  460. delta.seconds
  461. )
  462. return
  463. async def wait_and_do(when, what, *args, **kwargs):
  464. """Sleep until `when`, then call `what` passing `args` and `kwargs`."""
  465. await sleep_until(when)
  466. return await what(*args, **kwargs)
  467. def get_csv_string(list_, delimiter=',', quotechar='"'):
  468. """Return a `delimiter`-delimited string of `list_` items.
  469. Wrap strings in `quotechar`s.
  470. """
  471. return delimiter.join(
  472. str(item) if type(item) is not str
  473. else '{q}{i}{q}'.format(
  474. i=item,
  475. q=quotechar
  476. )
  477. for item in list_
  478. )
  479. def case_accent_insensitive_sql(field):
  480. """Get a SQL string to perform a case- and accent-insensitive query.
  481. Given a `field`, return a part of SQL string necessary to perform
  482. a case- and accent-insensitive query.
  483. """
  484. replacements = [
  485. (' ', ''),
  486. ('à', 'a'),
  487. ('è', 'e'),
  488. ('é', 'e'),
  489. ('ì', 'i'),
  490. ('ò', 'o'),
  491. ('ù', 'u'),
  492. ]
  493. return "{r}LOWER({f}){w}".format(
  494. r="replace(".upper()*len(replacements),
  495. f=field,
  496. w=''.join(
  497. ", '{w[0]}', '{w[1]}')".format(w=w)
  498. for w in replacements
  499. )
  500. )
  501. # Italian definite articles.
  502. ARTICOLI = MyOD()
  503. ARTICOLI[1] = {
  504. 'ind': 'un',
  505. 'dets': 'il',
  506. 'detp': 'i',
  507. 'dess': 'l',
  508. 'desp': 'i'
  509. }
  510. ARTICOLI[2] = {
  511. 'ind': 'una',
  512. 'dets': 'la',
  513. 'detp': 'le',
  514. 'dess': 'lla',
  515. 'desp': 'lle'
  516. }
  517. ARTICOLI[3] = {
  518. 'ind': 'uno',
  519. 'dets': 'lo',
  520. 'detp': 'gli',
  521. 'dess': 'llo',
  522. 'desp': 'gli'
  523. }
  524. ARTICOLI[4] = {
  525. 'ind': 'un',
  526. 'dets': 'l\'',
  527. 'detp': 'gli',
  528. 'dess': 'll\'',
  529. 'desp': 'gli'
  530. }
  531. class Gettable():
  532. """Gettable objects can be retrieved from memory without being duplicated.
  533. Key is the primary key.
  534. Use classmethod get to instantiate (or retrieve) Gettable objects.
  535. Assign SubClass.instances = {}, otherwise Gettable.instances will
  536. contain SubClass objects.
  537. """
  538. instances = {}
  539. @classmethod
  540. def get(cls, key, *args, **kwargs):
  541. """Instantiate and/or retrieve Gettable object.
  542. SubClass.instances is searched if exists.
  543. Gettable.instances is searched otherwise.
  544. """
  545. if key not in cls.instances:
  546. cls.instances[key] = cls(key, *args, **kwargs)
  547. return cls.instances[key]
  548. class Confirmable():
  549. """Confirmable objects are provided with a confirm instance method.
  550. It evaluates True if it was called within self._confirm_timedelta,
  551. False otherwise.
  552. When it returns True, timer is reset.
  553. """
  554. CONFIRM_TIMEDELTA = datetime.timedelta(seconds=10)
  555. def __init__(self, confirm_timedelta=None):
  556. """Instantiate Confirmable instance.
  557. If `confirm_timedelta` is not passed,
  558. `self.__class__.CONFIRM_TIMEDELTA` is used as default.
  559. """
  560. if confirm_timedelta is None:
  561. confirm_timedelta = self.__class__.CONFIRM_TIMEDELTA
  562. self.set_confirm_timedelta(confirm_timedelta)
  563. self._confirm_datetimes = {}
  564. @property
  565. def confirm_timedelta(self):
  566. """Maximum timedelta between two calls of `confirm`."""
  567. return self._confirm_timedelta
  568. def confirm_datetime(self, who='unique'):
  569. """Get datetime of `who`'s last confirm.
  570. If `who` never called `confirm`, fake an expired call.
  571. """
  572. if who not in self._confirm_datetimes:
  573. self._confirm_datetimes[who] = (
  574. datetime.datetime.now()
  575. - 2*self.confirm_timedelta
  576. )
  577. return self._confirm_datetimes[who]
  578. def set_confirm_timedelta(self, confirm_timedelta):
  579. """Change self._confirm_timedelta."""
  580. if type(confirm_timedelta) is int:
  581. confirm_timedelta = datetime.timedelta(
  582. seconds=confirm_timedelta
  583. )
  584. assert isinstance(
  585. confirm_timedelta, datetime.timedelta
  586. ), "confirm_timedelta must be a datetime.timedelta instance!"
  587. self._confirm_timedelta = confirm_timedelta
  588. def confirm(self, who='unique'):
  589. """Return True if `confirm` was called by `who` recetly enough."""
  590. now = datetime.datetime.now()
  591. if now >= self.confirm_datetime(who) + self.confirm_timedelta:
  592. self._confirm_datetimes[who] = now
  593. return False
  594. self._confirm_datetimes[who] = now - 2*self.confirm_timedelta
  595. return True
  596. class HasBot():
  597. """Objects having a Bot subclass object as `.bot` attribute.
  598. HasBot objects have a .bot and .db properties for faster access.
  599. """
  600. bot = None
  601. @property
  602. def bot(self):
  603. """Class bot."""
  604. return self.__class__.bot
  605. @property
  606. def db(self):
  607. """Class bot db."""
  608. return self.bot.db
  609. @classmethod
  610. def set_bot(cls, bot):
  611. """Change class bot."""
  612. cls.bot = bot
  613. class CachedPage(Gettable):
  614. """Cache a webpage and return it during CACHE_TIME, otherwise refresh.
  615. Usage:
  616. cached_page = CachedPage.get(
  617. 'https://www.google.com',
  618. datetime.timedelta(seconds=30),
  619. **kwargs
  620. )
  621. page = await cached_page.get_page()
  622. """
  623. CACHE_TIME = datetime.timedelta(minutes=5)
  624. instances = {}
  625. def __init__(self, url, cache_time=None, **async_get_kwargs):
  626. """Instantiate CachedPage object.
  627. `url`: the URL to be cached
  628. `cache_time`: timedelta from last_update during which
  629. page will be cached
  630. `**kwargs` will be passed to async_get function
  631. """
  632. self._url = url
  633. if type(cache_time) is int:
  634. cache_time = datetime.timedelta(seconds=cache_time)
  635. if cache_time is None:
  636. cache_time = self.__class__.CACHE_TIME
  637. assert type(cache_time) is datetime.timedelta, (
  638. "Cache time must be a datetime.timedelta object!"
  639. )
  640. self._cache_time = cache_time
  641. self._page = None
  642. self._last_update = datetime.datetime.now() - self.cache_time
  643. self._async_get_kwargs = async_get_kwargs
  644. @property
  645. def url(self):
  646. """Get cached page url."""
  647. return self._url
  648. @property
  649. def cache_time(self):
  650. """Get cache time."""
  651. return self._cache_time
  652. @property
  653. def page(self):
  654. """Get webpage."""
  655. return self._page
  656. @property
  657. def last_update(self):
  658. """Get datetime of last update."""
  659. return self._last_update
  660. @property
  661. def async_get_kwargs(self):
  662. """Get async get request keyword arguments."""
  663. return self._async_get_kwargs
  664. @property
  665. def is_old(self):
  666. """Evaluate True if `chache_time` has passed since last update."""
  667. return datetime.datetime.now() > self.last_update + self.cache_time
  668. async def refresh(self):
  669. """Update cached webpage."""
  670. try:
  671. self._page = await async_get(self.url, **self.async_get_kwargs)
  672. self._last_update = datetime.datetime.now()
  673. return 0
  674. except Exception as e:
  675. self._page = None
  676. logging.error(
  677. '{e}'.format(
  678. e=e
  679. ),
  680. exc_info=False
  681. ) # Set exc_info=True to debug
  682. return 1
  683. return 1
  684. async def get_page(self):
  685. """Refresh if necessary and return webpage."""
  686. if self.is_old:
  687. await self.refresh()
  688. return self.page
  689. class Confirmator(Gettable, Confirmable):
  690. """Gettable Confirmable object."""
  691. instances = {}
  692. def __init__(self, key, *args, confirm_timedelta=None):
  693. """Call Confirmable.__init__ passing `confirm_timedelta`."""
  694. Confirmable.__init__(self, confirm_timedelta)
  695. def get_cleaned_text(update, bot=None, replace=[], strip='/ @'):
  696. """Clean `update`['text'] and return it.
  697. Strip `bot`.name and items to be `replace`d from the beginning of text.
  698. Strip `strip` characters from both ends.
  699. """
  700. if bot is not None:
  701. replace.append(
  702. '@{.name}'.format(
  703. bot
  704. )
  705. )
  706. text = update['text'].strip(strip)
  707. for s in replace:
  708. while s and text.lower().startswith(s.lower()):
  709. text = text[len(s):]
  710. return text.strip(strip)
  711. def get_user(record, link_profile=True):
  712. """Get an HTML Telegram tag for user `record`."""
  713. if not record:
  714. return
  715. from_ = {key: val for key, val in record.items()}
  716. result = '{name}'
  717. if 'telegram_id' in from_:
  718. from_['id'] = from_['telegram_id']
  719. if (
  720. 'id' in from_
  721. and from_['id'] is not None
  722. and link_profile
  723. ):
  724. result = f"""<a href="tg://user?id={from_['id']}">{{name}}</a>"""
  725. if 'username' in from_ and from_['username']:
  726. result = result.format(
  727. name=from_['username']
  728. )
  729. elif (
  730. 'first_name' in from_
  731. and from_['first_name']
  732. and 'last_name' in from_
  733. and from_['last_name']
  734. ):
  735. result = result.format(
  736. name=f"{from_['first_name']} {from_['last_name']}"
  737. )
  738. elif 'first_name' in from_ and from_['first_name']:
  739. result = result.format(
  740. name=from_['first_name']
  741. )
  742. elif 'last_name' in from_ and from_['last_name']:
  743. result = result.format(
  744. name=from_['last_name']
  745. )
  746. else:
  747. result = result.format(
  748. name="Utente anonimo"
  749. )
  750. return result
  751. def datetime_from_utc_to_local(utc_datetime):
  752. """Convert `utc_datetime` to local datetime."""
  753. now_timestamp = time.time()
  754. offset = (
  755. datetime.datetime.fromtimestamp(now_timestamp)
  756. - datetime.datetime.utcfromtimestamp(now_timestamp)
  757. )
  758. return utc_datetime + offset
  759. # TIME_SYMBOLS from more specific to less specific (avoid false positives!)
  760. TIME_SYMBOLS = MyOD()
  761. TIME_SYMBOLS["'"] = 'minutes'
  762. TIME_SYMBOLS["settimana"] = 'weeks'
  763. TIME_SYMBOLS["settimane"] = 'weeks'
  764. TIME_SYMBOLS["weeks"] = 'weeks'
  765. TIME_SYMBOLS["week"] = 'weeks'
  766. TIME_SYMBOLS["giorno"] = 'days'
  767. TIME_SYMBOLS["giorni"] = 'days'
  768. TIME_SYMBOLS["secondi"] = 'seconds'
  769. TIME_SYMBOLS["seconds"] = 'seconds'
  770. TIME_SYMBOLS["secondo"] = 'seconds'
  771. TIME_SYMBOLS["minuti"] = 'minutes'
  772. TIME_SYMBOLS["minuto"] = 'minutes'
  773. TIME_SYMBOLS["minute"] = 'minutes'
  774. TIME_SYMBOLS["minutes"] = 'minutes'
  775. TIME_SYMBOLS["day"] = 'days'
  776. TIME_SYMBOLS["days"] = 'days'
  777. TIME_SYMBOLS["ore"] = 'hours'
  778. TIME_SYMBOLS["ora"] = 'hours'
  779. TIME_SYMBOLS["sec"] = 'seconds'
  780. TIME_SYMBOLS["min"] = 'minutes'
  781. TIME_SYMBOLS["m"] = 'minutes'
  782. TIME_SYMBOLS["h"] = 'hours'
  783. TIME_SYMBOLS["d"] = 'days'
  784. TIME_SYMBOLS["s"] = 'seconds'
  785. def _interval_parser(text, result):
  786. text = text.lower()
  787. succeeded = False
  788. if result is None:
  789. result = []
  790. if len(result) == 0 or result[-1]['ok']:
  791. text_part = ''
  792. _text = text # I need to iterate through _text modifying text
  793. for char in _text:
  794. if not char.isnumeric():
  795. break
  796. else:
  797. text_part += char
  798. text = text[1:]
  799. if text_part.isnumeric():
  800. result.append(
  801. dict(
  802. unit=None,
  803. value=int(text_part),
  804. ok=False
  805. )
  806. )
  807. succeeded = True, True
  808. if text:
  809. dummy, result = _interval_parser(text, result)
  810. elif len(result) > 0 and not result[-1]['ok']:
  811. text_part = ''
  812. _text = text # I need to iterate through _text modifying text
  813. for char in _text:
  814. if char.isnumeric():
  815. break
  816. else:
  817. text_part += char
  818. text = text[1:]
  819. for time_symbol, unit in TIME_SYMBOLS.items():
  820. if time_symbol in text_part:
  821. result[-1]['unit'] = unit
  822. result[-1]['ok'] = True
  823. succeeded = True, True
  824. break
  825. else:
  826. result.pop()
  827. if text:
  828. dummy, result = _interval_parser(text, result)
  829. return succeeded, result
  830. def _date_parser(text, result):
  831. succeeded = False
  832. if 3 <= len(text) <= 10 and text.count('/') >= 1:
  833. if 3 <= len(text) <= 5 and text.count('/') == 1:
  834. text += '/{:%y}'.format(datetime.datetime.now())
  835. if 6 <= len(text) <= 10 and text.count('/') == 2:
  836. day, month, year = [
  837. int(n) for n in [
  838. ''.join(char)
  839. for char in text.split('/')
  840. if char.isnumeric()
  841. ]
  842. ]
  843. if year < 100:
  844. year += 2000
  845. if result is None:
  846. result = []
  847. result += [
  848. dict(
  849. unit='day',
  850. value=day,
  851. ok=True
  852. ),
  853. dict(
  854. unit='month',
  855. value=month,
  856. ok=True
  857. ),
  858. dict(
  859. unit='year',
  860. value=year,
  861. ok=True
  862. )
  863. ]
  864. succeeded = True, True
  865. return succeeded, result
  866. def _time_parser(text, result):
  867. succeeded = False
  868. if (1 <= len(text) <= 8) and any(char.isnumeric() for char in text):
  869. text = ''.join(
  870. ':' if char == '.' else char
  871. for char in text
  872. if char.isnumeric() or char in (':', '.')
  873. )
  874. if len(text) <= 2:
  875. text = '{:02d}:00:00'.format(int(text))
  876. elif len(text) == 4 and ':' not in text:
  877. text = '{:02d}:{:02d}:00'.format(
  878. *[int(x) for x in (text[:2], text[2:])]
  879. )
  880. elif text.count(':') == 1:
  881. text = '{:02d}:{:02d}:00'.format(
  882. *[int(x) for x in text.split(':')]
  883. )
  884. if text.count(':') == 2:
  885. hour, minute, second = (int(x) for x in text.split(':'))
  886. if (
  887. 0 <= hour <= 24
  888. and 0 <= minute <= 60
  889. and 0 <= second <= 60
  890. ):
  891. if result is None:
  892. result = []
  893. result += [
  894. dict(
  895. unit='hour',
  896. value=hour,
  897. ok=True
  898. ),
  899. dict(
  900. unit='minute',
  901. value=minute,
  902. ok=True
  903. ),
  904. dict(
  905. unit='second',
  906. value=second,
  907. ok=True
  908. )
  909. ]
  910. succeeded = True
  911. return succeeded, result
  912. WEEKDAY_NAMES_ITA = ["Lunedì", "Martedì", "Mercoledì", "Giovedì",
  913. "Venerdì", "Sabato", "Domenica"]
  914. WEEKDAY_NAMES_ENG = ["Monday", "Tuesday", "Wednesday", "Thursday",
  915. "Friday", "Saturday", "Sunday"]
  916. def _period_parser(text, result):
  917. succeeded = False
  918. if text in ('every', 'ogni',):
  919. succeeded = True
  920. if text.title() in WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG:
  921. day_code = (WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG).index(text.title())
  922. if day_code > 6:
  923. day_code -= 7
  924. today = datetime.date.today()
  925. days = 1
  926. while (today + datetime.timedelta(days=days)).weekday() != day_code:
  927. days += 1
  928. if result is None:
  929. result = []
  930. result.append(
  931. dict(
  932. unit='days',
  933. value=days,
  934. ok=True,
  935. weekly=True
  936. )
  937. )
  938. succeeded = True
  939. else:
  940. succeeded, result = _interval_parser(text, result)
  941. return succeeded, result
  942. TIME_WORDS = {
  943. 'tra': dict(
  944. parser=_interval_parser,
  945. recurring=False,
  946. type_='delta'
  947. ),
  948. 'in': dict(
  949. parser=_interval_parser,
  950. recurring=False,
  951. type_='delta'
  952. ),
  953. 'at': dict(
  954. parser=_time_parser,
  955. recurring=False,
  956. type_='set'
  957. ),
  958. 'on': dict(
  959. parser=_date_parser,
  960. recurring=False,
  961. type_='set'
  962. ),
  963. 'alle': dict(
  964. parser=_time_parser,
  965. recurring=False,
  966. type_='set'
  967. ),
  968. 'il': dict(
  969. parser=_date_parser,
  970. recurring=False,
  971. type_='set'
  972. ),
  973. 'every': dict(
  974. parser=_period_parser,
  975. recurring=True,
  976. type_='delta'
  977. ),
  978. 'ogni': dict(
  979. parser=_period_parser,
  980. recurring=True,
  981. type_='delta'
  982. ),
  983. }
  984. def parse_datetime_interval_string(text):
  985. """Parse `text` and return text, datetime and timedelta."""
  986. parsers = []
  987. result_text, result_datetime, result_timedelta = [], None, None
  988. is_quoted_text = False
  989. for word in text.split(' '):
  990. if word.count('"') % 2:
  991. is_quoted_text = not is_quoted_text
  992. if is_quoted_text or '"' in word:
  993. result_text.append(
  994. word.replace('"', '') if 'href=' not in word else word
  995. )
  996. continue
  997. result_text.append(word)
  998. word = word.lower()
  999. succeeded = False
  1000. if len(parsers) > 0:
  1001. succeeded, result = parsers[-1]['parser'](
  1002. word,
  1003. parsers[-1]['result']
  1004. )
  1005. if succeeded:
  1006. parsers[-1]['result'] = result
  1007. if not succeeded and word in TIME_WORDS:
  1008. parsers.append(
  1009. dict(
  1010. result=None,
  1011. parser=TIME_WORDS[word]['parser'],
  1012. recurring=TIME_WORDS[word]['recurring'],
  1013. type_=TIME_WORDS[word]['type_']
  1014. )
  1015. )
  1016. if succeeded:
  1017. result_text.pop()
  1018. if len(result_text) > 0 and result_text[-1].lower() in TIME_WORDS:
  1019. result_text.pop()
  1020. result_text = escape_html_chars(
  1021. ' '.join(result_text)
  1022. )
  1023. parsers = list(
  1024. filter(
  1025. lambda x: 'result' in x and x['result'],
  1026. parsers
  1027. )
  1028. )
  1029. recurring_event = False
  1030. weekly = False
  1031. _timedelta = datetime.timedelta()
  1032. _datetime = None
  1033. _now = datetime.datetime.now()
  1034. for parser in parsers:
  1035. if parser['recurring']:
  1036. recurring_event = True
  1037. type_ = parser['type_']
  1038. for result in parser['result']:
  1039. if not result['ok']:
  1040. continue
  1041. if recurring_event and 'weekly' in result and result['weekly']:
  1042. weekly = True
  1043. if type_ == 'set':
  1044. if _datetime is None:
  1045. _datetime = _now
  1046. _datetime = _datetime.replace(
  1047. **{
  1048. result['unit']: result['value']
  1049. }
  1050. )
  1051. elif type_ == 'delta':
  1052. _timedelta += datetime.timedelta(
  1053. **{
  1054. result['unit']: result['value']
  1055. }
  1056. )
  1057. if _datetime:
  1058. result_datetime = _datetime
  1059. if _timedelta:
  1060. if result_datetime is None:
  1061. result_datetime = _now
  1062. if recurring_event:
  1063. result_timedelta = _timedelta
  1064. if weekly:
  1065. result_timedelta = datetime.timedelta(days=7)
  1066. else:
  1067. result_datetime += _timedelta
  1068. while result_datetime and result_datetime < datetime.datetime.now():
  1069. result_datetime += (
  1070. result_timedelta
  1071. if result_timedelta
  1072. else datetime.timedelta(days=1)
  1073. )
  1074. return result_text, result_datetime, result_timedelta
  1075. DAY_GAPS = {
  1076. -1: 'ieri',
  1077. -2: 'avantieri',
  1078. 0: 'oggi',
  1079. 1: 'domani',
  1080. 2: 'dopodomani'
  1081. }
  1082. MONTH_NAMES_ITA = MyOD()
  1083. MONTH_NAMES_ITA[1] = "gennaio"
  1084. MONTH_NAMES_ITA[2] = "febbraio"
  1085. MONTH_NAMES_ITA[3] = "marzo"
  1086. MONTH_NAMES_ITA[4] = "aprile"
  1087. MONTH_NAMES_ITA[5] = "maggio"
  1088. MONTH_NAMES_ITA[6] = "giugno"
  1089. MONTH_NAMES_ITA[7] = "luglio"
  1090. MONTH_NAMES_ITA[8] = "agosto"
  1091. MONTH_NAMES_ITA[9] = "settembre"
  1092. MONTH_NAMES_ITA[10] = "ottobre"
  1093. MONTH_NAMES_ITA[11] = "novembre"
  1094. MONTH_NAMES_ITA[12] = "dicembre"
  1095. def beautytd(td):
  1096. """Format properly timedeltas."""
  1097. result = ''
  1098. if type(td) is int:
  1099. td = datetime.timedelta(seconds=td)
  1100. assert isinstance(
  1101. td,
  1102. datetime.timedelta
  1103. ), "td must be a datetime.timedelta object!"
  1104. mtd = datetime.timedelta
  1105. if td < mtd(minutes=1):
  1106. result = "{:.0f} secondi".format(
  1107. td.total_seconds()
  1108. )
  1109. elif td < mtd(minutes=60):
  1110. result = "{:.0f} min{}".format(
  1111. td.total_seconds()//60,
  1112. (
  1113. " {:.0f} s".format(
  1114. td.total_seconds() % 60
  1115. )
  1116. ) if td.total_seconds() % 60 else ''
  1117. )
  1118. elif td < mtd(days=1):
  1119. result = "{:.0f} h{}".format(
  1120. td.total_seconds()//3600,
  1121. (
  1122. " {:.0f} min".format(
  1123. (td.total_seconds() % 3600) // 60
  1124. )
  1125. ) if td.total_seconds() % 3600 else ''
  1126. )
  1127. elif td < mtd(days=30):
  1128. result = "{} giorni{}".format(
  1129. td.days,
  1130. (
  1131. " {:.0f} h".format(
  1132. td.total_seconds() % (3600*24) // 3600
  1133. )
  1134. ) if td.total_seconds() % (3600*24) else ''
  1135. )
  1136. return result
  1137. def beautydt(dt):
  1138. """Format a datetime in a smart way."""
  1139. if type(dt) is str:
  1140. dt = str_to_datetime(dt)
  1141. assert isinstance(
  1142. dt,
  1143. datetime.datetime
  1144. ), "dt must be a datetime.datetime object!"
  1145. now = datetime.datetime.now()
  1146. gap = dt - now
  1147. gap_days = (dt.date() - now.date()).days
  1148. result = "{dt:alle %H:%M}".format(
  1149. dt=dt
  1150. )
  1151. if abs(gap) < datetime.timedelta(minutes=30):
  1152. result += "{dt::%S}".format(dt=dt)
  1153. if -2 <= gap_days <= 2:
  1154. result += " di {dg}".format(
  1155. dg=DAY_GAPS[gap_days]
  1156. )
  1157. elif gap.days not in (-1, 0):
  1158. result += " del {d}{m}".format(
  1159. d=dt.day,
  1160. m=(
  1161. "" if now.year == dt.year and now.month == dt.month
  1162. else " {m}{y}".format(
  1163. m=MONTH_NAMES_ITA[dt.month].title(),
  1164. y="" if now.year == dt.year
  1165. else " {}".format(dt.year)
  1166. )
  1167. )
  1168. )
  1169. return result
  1170. HTML_SYMBOLS = MyOD()
  1171. HTML_SYMBOLS["&"] = "&amp;"
  1172. HTML_SYMBOLS["<"] = "&lt;"
  1173. HTML_SYMBOLS[">"] = "&gt;"
  1174. HTML_SYMBOLS["\""] = "&quot;"
  1175. HTML_SYMBOLS["&lt;b&gt;"] = "<b>"
  1176. HTML_SYMBOLS["&lt;/b&gt;"] = "</b>"
  1177. HTML_SYMBOLS["&lt;i&gt;"] = "<i>"
  1178. HTML_SYMBOLS["&lt;/i&gt;"] = "</i>"
  1179. HTML_SYMBOLS["&lt;code&gt;"] = "<code>"
  1180. HTML_SYMBOLS["&lt;/code&gt;"] = "</code>"
  1181. HTML_SYMBOLS["&lt;pre&gt;"] = "<pre>"
  1182. HTML_SYMBOLS["&lt;/pre&gt;"] = "</pre>"
  1183. HTML_SYMBOLS["&lt;a href=&quot;"] = "<a href=\""
  1184. HTML_SYMBOLS["&quot;&gt;"] = "\">"
  1185. HTML_SYMBOLS["&lt;/a&gt;"] = "</a>"
  1186. HTML_TAGS = [
  1187. None, "<b>", "</b>",
  1188. None, "<i>", "</i>",
  1189. None, "<code>", "</code>",
  1190. None, "<pre>", "</pre>",
  1191. None, "<a href=\"", "\">", "</a>",
  1192. None
  1193. ]
  1194. def remove_html_tags(text):
  1195. """Remove HTML tags from `text`."""
  1196. for tag in HTML_TAGS:
  1197. if tag is None:
  1198. continue
  1199. text = text.replace(tag, '')
  1200. return text
  1201. def escape_html_chars(text):
  1202. """Escape HTML chars if not part of a tag."""
  1203. for s, r in HTML_SYMBOLS.items():
  1204. text = text.replace(s, r)
  1205. copy = text
  1206. expected_tag = None
  1207. while copy:
  1208. min_ = min(
  1209. (
  1210. dict(
  1211. position=copy.find(tag) if tag in copy else len(copy),
  1212. tag=tag
  1213. )
  1214. for tag in HTML_TAGS
  1215. if tag
  1216. ),
  1217. key=lambda x: x['position'],
  1218. default=0
  1219. )
  1220. if min_['position'] == len(copy):
  1221. break
  1222. if expected_tag and min_['tag'] != expected_tag:
  1223. return text.replace('<', '_').replace('>', '_')
  1224. expected_tag = HTML_TAGS[HTML_TAGS.index(min_['tag'])+1]
  1225. copy = extract(copy, min_['tag'])
  1226. return text
  1227. def accents_to_jolly(text, lower=True):
  1228. """Replace letters with Italian accents with SQL jolly character."""
  1229. to_be_replaced = ('à', 'è', 'é', 'ì', 'ò', 'ù')
  1230. if lower:
  1231. text = text.lower()
  1232. else:
  1233. to_be_replaced += tuple(s.upper() for s in to_be_replaced)
  1234. for s in to_be_replaced:
  1235. text = text.replace(s, '_')
  1236. return text.replace("'", "''")
  1237. def get_secure_key(allowed_chars=None, length=6):
  1238. """Get a randomly-generate secure key.
  1239. You can specify a set of `allowed_chars` and a `length`.
  1240. """
  1241. if allowed_chars is None:
  1242. allowed_chars = string.ascii_uppercase + string.digits
  1243. return ''.join(
  1244. random.SystemRandom().choice(
  1245. allowed_chars
  1246. )
  1247. for _ in range(length)
  1248. )
  1249. def round_to_minute(datetime_):
  1250. """Round `datetime_` to closest minute."""
  1251. return (
  1252. datetime_ + datetime.timedelta(seconds=30)
  1253. ).replace(second=0, microsecond=0)
  1254. def get_line_by_content(text, key):
  1255. """Get line of `text` containing `key`."""
  1256. for line in text.split('\n'):
  1257. if key in line:
  1258. return line
  1259. return
  1260. def str_to_int(string):
  1261. """Cast str to int, ignoring non-numeric characters."""
  1262. string = ''.join(
  1263. char
  1264. for char in string
  1265. if char.isnumeric()
  1266. )
  1267. if len(string) == 0:
  1268. string = '0'
  1269. return int(string)
  1270. def starting_with_or_similar_to(a, b):
  1271. """Return similarity between two strings.
  1272. Least similar equals 0, most similar evaluates 1.
  1273. If similarity is less than 0.75, return 1 if one string starts with
  1274. the other and return 0.5 if one string is contained in the other.
  1275. """
  1276. a = a.lower()
  1277. b = b.lower()
  1278. similarity = SequenceMatcher(None, a, b).ratio()
  1279. if similarity < 0.75:
  1280. if b.startswith(a) or a.startswith(b):
  1281. return 1
  1282. if b in a or a in b:
  1283. return 0.5
  1284. return similarity
  1285. def pick_most_similar_from_list(list_, item):
  1286. """Return element from `list_` which is most similar to `item`.
  1287. Similarity is evaluated using `starting_with_or_similar_to`.
  1288. """
  1289. return max(
  1290. list_,
  1291. key=lambda element: starting_with_or_similar_to(
  1292. item,
  1293. element
  1294. )
  1295. )
  1296. def run_aiohttp_server(app, *args, **kwargs):
  1297. """Run an aiohttp web app, with its positional and keyword arguments.
  1298. Useful to run apps in dedicated threads.
  1299. """
  1300. loop = asyncio.new_event_loop()
  1301. asyncio.set_event_loop(loop)
  1302. web.run_app(app, *args, **kwargs)
  1303. def custom_join(_list, joiner, final=None):
  1304. """Join elements of `_list` using `joiner` (`final` as last joiner)."""
  1305. _list = list(map(str, _list))
  1306. if final is None:
  1307. final = joiner
  1308. if len(_list) == 0:
  1309. return ''
  1310. if len(_list) == 1:
  1311. return _list[0]
  1312. if len(_list) == 2:
  1313. return final.join(_list)
  1314. return joiner.join(_list[:-1]) + final + _list[-1]
  1315. def make_inline_query_answer(answer):
  1316. """Return an article-type answer to inline query.
  1317. Takes either a string or a dictionary and returns a list.
  1318. """
  1319. if type(answer) is str:
  1320. answer = dict(
  1321. type='article',
  1322. id=0,
  1323. title=remove_html_tags(answer),
  1324. input_message_content=dict(
  1325. message_text=answer,
  1326. parse_mode='HTML'
  1327. )
  1328. )
  1329. if type(answer) is dict:
  1330. answer = [answer]
  1331. return answer
  1332. async def dummy_coroutine(*args, **kwargs):
  1333. """Accept everthing as argument and do nothing."""
  1334. return
  1335. async def send_csv_file(bot, chat_id, query, caption=None,
  1336. file_name='File.csv', user_record=None, update=dict()):
  1337. """Run a query on `bot` database and send result as CSV file to `chat_id`.
  1338. Optional parameters `caption` and `file_name` may be passed to this
  1339. function.
  1340. """
  1341. try:
  1342. with bot.db as db:
  1343. record = db.query(
  1344. query
  1345. )
  1346. header_line = []
  1347. body_lines = []
  1348. for row in record:
  1349. if not header_line:
  1350. header_line.append(get_csv_string(row.keys()))
  1351. body_lines.append(get_csv_string(row.values()))
  1352. text = '\n'.join(header_line + body_lines)
  1353. except Exception as e:
  1354. text = "{message}\n{e}".format(
  1355. message=bot.get_message('admin', 'query_button', 'error',
  1356. user_record=user_record, update=update),
  1357. e=e
  1358. )
  1359. for x, y in {'&lt;': '<', '\n': '\r\n'}.items():
  1360. text = text.replace(x, y)
  1361. if len(text) == 0:
  1362. text = bot.get_message('admin', 'query_button', 'empty_file',
  1363. user_record=user_record, update=update)
  1364. with io.BytesIO(text.encode('utf-8')) as f:
  1365. f.name = file_name
  1366. return await bot.send_document(
  1367. chat_id=chat_id,
  1368. document=f,
  1369. caption=caption
  1370. )
  1371. async def send_part_of_text_file(bot, chat_id, file_path, caption=None,
  1372. file_name='File.txt', user_record=None,
  1373. update=dict(),
  1374. reversed_=True,
  1375. limit=None):
  1376. """Send `lines` lines of text file via `bot` in `chat_id`.
  1377. If `reversed`, read the file from last line.
  1378. TODO: do not load whole file in RAM. At the moment this is the easiest
  1379. way to allow `reversed` files, but it is inefficient and requires a lot
  1380. of memory.
  1381. """
  1382. try:
  1383. with open(file_path, 'r') as log_file:
  1384. lines = log_file.readlines()
  1385. if reversed:
  1386. lines = lines[::-1]
  1387. if limit:
  1388. lines = lines[:limit]
  1389. with io.BytesIO(
  1390. ''.join(lines).encode('utf-8')
  1391. ) as document:
  1392. document.name = file_name
  1393. return await bot.send_document(
  1394. chat_id=chat_id,
  1395. document=document,
  1396. caption=caption
  1397. )
  1398. except Exception as e:
  1399. return e