Queer European MD passionate about IT

utilities.py 50 KB

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