validators.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
  2. # █▀█ █ █ █ █▀█ █▀▄ █
  3. # © Copyright 2022
  4. # https://t.me/hikariatama
  5. #
  6. # 🔒 Licensed under the GNU AGPLv3
  7. # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
  8. import re
  9. import grapheme
  10. import functools
  11. import typing
  12. from emoji import get_emoji_unicode_dict
  13. from . import utils
  14. ConfigAllowedTypes = typing.Union[tuple, list, str, int, bool, None]
  15. ALLOWED_EMOJIS = set(get_emoji_unicode_dict("en").values())
  16. class ValidationError(Exception):
  17. """
  18. Is being raised when config value passed can't be converted properly
  19. Must be raised with string, describing why value is incorrect
  20. It will be shown in .config, if user tries to set incorrect value
  21. """
  22. class Validator:
  23. """
  24. Class used as validator of config value
  25. :param validator: Sync function, which raises `ValidationError` if passed
  26. value is incorrect (with explanation) and returns converted
  27. value if it is semantically correct.
  28. ⚠️ If validator returns `None`, value will always be set to `None`
  29. :param doc: Docstrings for this validator as string, or dict in format:
  30. {
  31. "en": "docstring",
  32. "ru": "докстрингом",
  33. "ua": "докстрiнгом",
  34. "jp": "ヒント",
  35. }
  36. Use instrumental case with lowercase
  37. :param _internal_id: Do not pass anything here, or things will break
  38. """
  39. def __init__(
  40. self,
  41. validator: callable,
  42. doc: typing.Optional[typing.Union[str, dict]] = None,
  43. _internal_id: typing.Optional[int] = None,
  44. ):
  45. self.validate = validator
  46. if isinstance(doc, str):
  47. doc = {"en": doc, "ru": doc}
  48. self.doc = doc
  49. self.internal_id = _internal_id
  50. class Boolean(Validator):
  51. """
  52. Any logical value to be passed
  53. `1`, `"1"` etc. will be automatically converted to bool
  54. """
  55. def __init__(self):
  56. super().__init__(
  57. self._validate,
  58. {"en": "boolean", "ru": "логическим значением"},
  59. _internal_id="Boolean",
  60. )
  61. @staticmethod
  62. def _validate(value: ConfigAllowedTypes, /) -> bool:
  63. true_cases = ["True", "true", "1", 1, True]
  64. false_cases = ["False", "false", "0", 0, False]
  65. if value not in true_cases + false_cases:
  66. raise ValidationError("Passed value must be a boolean")
  67. return value in true_cases
  68. class Integer(Validator):
  69. """
  70. Checks whether passed argument is an integer value
  71. :param digits: Digits quantity, which must be passed
  72. :param minimum: Minimal number to be passed
  73. :param maximum: Maximum number to be passed
  74. """
  75. def __init__(
  76. self,
  77. *,
  78. digits: typing.Optional[int] = None,
  79. minimum: typing.Optional[int] = None,
  80. maximum: typing.Optional[int] = None,
  81. ):
  82. _sign_en = "positive " if minimum is not None and minimum == 0 else ""
  83. _sign_ru = "положительным " if minimum is not None and minimum == 0 else ""
  84. _sign_en = "negative " if maximum is not None and maximum == 0 else _sign_en
  85. _sign_ru = (
  86. "отрицательным " if maximum is not None and maximum == 0 else _sign_ru
  87. )
  88. _digits_en = f" with exactly {digits} digits" if digits is not None else ""
  89. _digits_ru = f", в котором ровно {digits} цифр " if digits is not None else ""
  90. if minimum is not None and minimum != 0:
  91. doc = (
  92. {
  93. "en": f"{_sign_en}integer greater than {minimum}{_digits_en}",
  94. "ru": f"{_sign_ru}целым числом больше {minimum}{_digits_ru}",
  95. }
  96. if maximum is None and maximum != 0
  97. else {
  98. "en": f"{_sign_en}integer from {minimum} to {maximum}{_digits_en}",
  99. "ru": (
  100. f"{_sign_ru}целым числом в промежутке от {minimum} до"
  101. f" {maximum}{_digits_ru}"
  102. ),
  103. }
  104. )
  105. elif maximum is None and maximum != 0:
  106. doc = {
  107. "en": f"{_sign_en}integer{_digits_en}",
  108. "ru": f"{_sign_ru}целым числом{_digits_ru}",
  109. }
  110. else:
  111. doc = {
  112. "en": f"{_sign_en}integer less than {maximum}{_digits_en}",
  113. "ru": f"{_sign_ru}целым числом меньше {maximum}{_digits_ru}",
  114. }
  115. super().__init__(
  116. functools.partial(
  117. self._validate,
  118. digits=digits,
  119. minimum=minimum,
  120. maximum=maximum,
  121. ),
  122. doc,
  123. _internal_id="Integer",
  124. )
  125. @staticmethod
  126. def _validate(
  127. value: ConfigAllowedTypes,
  128. /,
  129. *,
  130. digits: int,
  131. minimum: int,
  132. maximum: int,
  133. ) -> typing.Union[int, None]:
  134. try:
  135. value = int(str(value).strip())
  136. except ValueError:
  137. raise ValidationError(f"Passed value ({value}) must be a number")
  138. if minimum is not None and value < minimum:
  139. raise ValidationError(f"Passed value ({value}) is lower than minimum one")
  140. if maximum is not None and value > maximum:
  141. raise ValidationError(f"Passed value ({value}) is greater than maximum one")
  142. if digits is not None and len(str(value)) != digits:
  143. raise ValidationError(
  144. f"The length of passed value ({value}) is incorrect "
  145. f"(Must be exactly {digits} digits)"
  146. )
  147. return value
  148. class Choice(Validator):
  149. """
  150. Check whether entered value is in the allowed list
  151. :param possible_values: Allowed values to be passed to config param
  152. """
  153. def __init__(
  154. self,
  155. possible_values: typing.List[ConfigAllowedTypes],
  156. /,
  157. ):
  158. super().__init__(
  159. functools.partial(self._validate, possible_values=possible_values),
  160. {
  161. "en": (
  162. "one of the following:"
  163. f" {' / '.join(list(map(str, possible_values)))}"
  164. ),
  165. "ru": f"одним из: {' / '.join(list(map(str, possible_values)))}",
  166. },
  167. _internal_id="Choice",
  168. )
  169. @staticmethod
  170. def _validate(
  171. value: ConfigAllowedTypes,
  172. /,
  173. *,
  174. possible_values: typing.List[ConfigAllowedTypes],
  175. ) -> ConfigAllowedTypes:
  176. if value not in possible_values:
  177. raise ValidationError(
  178. f"Passed value ({value}) is not one of the following:"
  179. f" {' / '.join(list(map(str, possible_values)))}"
  180. )
  181. return value
  182. class MultiChoice(Validator):
  183. """
  184. Check whether every entered value is in the allowed list
  185. :param possible_values: Allowed values to be passed to config param
  186. """
  187. def __init__(
  188. self,
  189. possible_values: typing.List[ConfigAllowedTypes],
  190. /,
  191. ):
  192. super().__init__(
  193. functools.partial(self._validate, possible_values=possible_values),
  194. {
  195. "en": (
  196. "list of values, where each one must be one of:"
  197. f" {' / '.join(list(map(str, possible_values)))}"
  198. ),
  199. "ru": (
  200. "список значений, каждое из которых должно быть одним из"
  201. f" следующего: {' / '.join(list(map(str, possible_values)))}"
  202. ),
  203. },
  204. _internal_id="MultiChoice",
  205. )
  206. @staticmethod
  207. def _validate(
  208. value: typing.List[ConfigAllowedTypes],
  209. /,
  210. *,
  211. possible_values: typing.List[ConfigAllowedTypes],
  212. ) -> typing.List[ConfigAllowedTypes]:
  213. if not isinstance(value, (list, tuple)):
  214. value = [value]
  215. for item in value:
  216. if item not in possible_values:
  217. raise ValidationError(
  218. f"One of passed values ({item}) is not one of the following:"
  219. f" {' / '.join(list(map(str, possible_values)))}"
  220. )
  221. return list(set(value))
  222. class Series(Validator):
  223. """
  224. Represents the series of value (simply `list`)
  225. :param separator: With which separator values must be separated
  226. :param validator: Internal validator for each sequence value
  227. :param min_len: Minimal number of series items to be passed
  228. :param max_len: Maximum number of series items to be passed
  229. :param fixed_len: Fixed number of series items to be passed
  230. """
  231. def __init__(
  232. self,
  233. validator: typing.Optional[Validator] = None,
  234. min_len: typing.Optional[int] = None,
  235. max_len: typing.Optional[int] = None,
  236. fixed_len: typing.Optional[int] = None,
  237. ):
  238. _each_en = (
  239. f" (each must be {validator.doc['en']})" if validator is not None else ""
  240. )
  241. _each_ru = (
  242. f" (каждое должно быть {validator.doc['ru']})"
  243. if validator is not None
  244. else ""
  245. )
  246. if fixed_len is not None:
  247. _len_en = f" (exactly {fixed_len} pcs.)"
  248. _len_ru = f" (ровно {fixed_len} шт.)"
  249. elif min_len is None:
  250. if max_len is None:
  251. _len_en = ""
  252. _len_ru = ""
  253. else:
  254. _len_en = f" (up to {max_len} pcs.)"
  255. _len_ru = f" (до {max_len} шт.)"
  256. elif max_len is not None:
  257. _len_en = f" (from {min_len} to {max_len} pcs.)"
  258. _len_ru = f" (от {min_len} до {max_len} шт.)"
  259. else:
  260. _len_en = f" (at least {min_len} pcs.)"
  261. _len_ru = f" (как минимум {min_len} шт.)"
  262. doc = {
  263. "en": f"series of values{_len_en}{_each_en}, separated with «,»",
  264. "ru": f"списком значений{_len_ru}{_each_ru}, разделенных «,»",
  265. }
  266. super().__init__(
  267. functools.partial(
  268. self._validate,
  269. validator=validator,
  270. min_len=min_len,
  271. max_len=max_len,
  272. fixed_len=fixed_len,
  273. ),
  274. doc,
  275. _internal_id="Series",
  276. )
  277. @staticmethod
  278. def _validate(
  279. value: ConfigAllowedTypes,
  280. /,
  281. *,
  282. validator: typing.Optional[Validator] = None,
  283. min_len: typing.Optional[int] = None,
  284. max_len: typing.Optional[int] = None,
  285. fixed_len: typing.Optional[int] = None,
  286. ) -> typing.List[ConfigAllowedTypes]:
  287. if not isinstance(value, (list, tuple, set)):
  288. value = str(value).split(",")
  289. if isinstance(value, (tuple, set)):
  290. value = list(value)
  291. if min_len is not None and len(value) < min_len:
  292. raise ValidationError(
  293. f"Passed value ({value}) contains less than {min_len} items"
  294. )
  295. if max_len is not None and len(value) > max_len:
  296. raise ValidationError(
  297. f"Passed value ({value}) contains more than {max_len} items"
  298. )
  299. if fixed_len is not None and len(value) != fixed_len:
  300. raise ValidationError(
  301. f"Passed value ({value}) must contain exactly {fixed_len} items"
  302. )
  303. value = [item.strip() if isinstance(item, str) else item for item in value]
  304. if isinstance(validator, Validator):
  305. for i, item in enumerate(value):
  306. try:
  307. value[i] = validator.validate(item)
  308. except ValidationError:
  309. raise ValidationError(
  310. f"Passed value ({value}) contains invalid item"
  311. f" ({str(item).strip()}), which must be {validator.doc['en']}"
  312. )
  313. value = list(filter(lambda x: x, value))
  314. return value
  315. class Link(Validator):
  316. """Valid url must be specified"""
  317. def __init__(self):
  318. super().__init__(
  319. lambda value: self._validate(value),
  320. {
  321. "en": "link",
  322. "ru": "ссылкой",
  323. },
  324. _internal_id="Link",
  325. )
  326. @staticmethod
  327. def _validate(value: ConfigAllowedTypes, /) -> str:
  328. try:
  329. if not utils.check_url(value):
  330. raise Exception("Invalid URL")
  331. except Exception:
  332. raise ValidationError(f"Passed value ({value}) is not a valid URL")
  333. return value
  334. class String(Validator):
  335. """
  336. Checks for length of passed value and automatically converts it to string
  337. :param length: Exact length of string
  338. :param min_len: Minimal length of string
  339. :param max_len: Maximum length of string
  340. """
  341. def __init__(
  342. self,
  343. length: typing.Optional[int] = None,
  344. min_len: typing.Optional[int] = None,
  345. max_len: typing.Optional[int] = None,
  346. ):
  347. if length is not None:
  348. doc = {
  349. "en": f"string of length {length}",
  350. "ru": f"строкой из {length} символа(-ов)",
  351. }
  352. else:
  353. if min_len is None:
  354. if max_len is None:
  355. doc = {
  356. "en": "string",
  357. "ru": "строкой",
  358. }
  359. else:
  360. doc = {
  361. "en": f"string of length up to {max_len}",
  362. "ru": f"строкой не более чем из {max_len} символа(-ов)",
  363. }
  364. elif max_len is not None:
  365. doc = {
  366. "en": f"string of length from {min_len} to {max_len}",
  367. "ru": f"строкой из {min_len}-{max_len} символа(-ов)",
  368. }
  369. else:
  370. doc = {
  371. "en": f"string of length at least {min_len}",
  372. "ru": f"строкой не менее чем из {min_len} символа(-ов)",
  373. }
  374. super().__init__(
  375. functools.partial(
  376. self._validate,
  377. length=length,
  378. min_len=min_len,
  379. max_len=max_len,
  380. ),
  381. doc,
  382. _internal_id="String",
  383. )
  384. @staticmethod
  385. def _validate(
  386. value: ConfigAllowedTypes,
  387. /,
  388. *,
  389. length: typing.Optional[int],
  390. min_len: typing.Optional[int],
  391. max_len: typing.Optional[int],
  392. ) -> str:
  393. if (
  394. isinstance(length, int)
  395. and len(list(grapheme.graphemes(str(value)))) != length
  396. ):
  397. raise ValidationError(
  398. f"Passed value ({value}) must be a length of {length}"
  399. )
  400. return str(value)
  401. class RegExp(Validator):
  402. """
  403. Checks if value matches the regex
  404. :param regex: Regex to match
  405. :param flags: Flags to pass to re.compile
  406. :param description: Description of regex
  407. """
  408. def __init__(
  409. self,
  410. regex: str,
  411. flags: typing.Optional[re.RegexFlag] = None,
  412. description: typing.Optional[typing.Union[dict, str]] = None,
  413. ):
  414. try:
  415. re.compile(regex, flags=flags)
  416. except re.error as e:
  417. raise Exception(f"{regex} is not a valid regex") from e
  418. if description is None:
  419. doc = {
  420. "en": f"string matching pattern «{regex}»",
  421. "ru": f"строкой, соответствующей шаблону «{regex}»",
  422. }
  423. else:
  424. if isinstance(description, str):
  425. doc = {
  426. "en": description,
  427. "ru": description,
  428. }
  429. else:
  430. doc = description
  431. super().__init__(
  432. functools.partial(self._validate, regex=regex, flags=flags),
  433. doc,
  434. _internal_id="RegExp",
  435. )
  436. @staticmethod
  437. def _validate(
  438. value: ConfigAllowedTypes,
  439. /,
  440. *,
  441. regex: str,
  442. flags: typing.Optional[re.RegexFlag],
  443. ) -> str:
  444. if not re.match(regex, value, flags=flags):
  445. raise ValidationError(f"Passed value ({value}) must follow pattern {regex}")
  446. return value
  447. class Float(Validator):
  448. """
  449. Checks whether passed argument is a float value
  450. :param minimum: Minimal number to be passed
  451. :param maximum: Maximum number to be passed
  452. """
  453. def __init__(
  454. self,
  455. minimum: typing.Optional[float] = None,
  456. maximum: typing.Optional[float] = None,
  457. ):
  458. _sign_en = "positive " if minimum is not None and minimum == 0 else ""
  459. _sign_ru = "положительным " if minimum is not None and minimum == 0 else ""
  460. _sign_en = "negative " if maximum is not None and maximum == 0 else _sign_en
  461. _sign_ru = (
  462. "отрицательным " if maximum is not None and maximum == 0 else _sign_ru
  463. )
  464. if minimum is not None and minimum != 0:
  465. doc = (
  466. {
  467. "en": f"{_sign_en}float greater than {minimum}",
  468. "ru": f"{_sign_ru}дробным числом больше {minimum}",
  469. }
  470. if maximum is None and maximum != 0
  471. else {
  472. "en": f"{_sign_en}float from {minimum} to {maximum}",
  473. "ru": (
  474. f"{_sign_ru}дробным числом в промежутке от {minimum} до"
  475. f" {maximum}"
  476. ),
  477. }
  478. )
  479. elif maximum is None and maximum != 0:
  480. doc = {
  481. "en": f"{_sign_en}float",
  482. "ru": f"{_sign_ru}дробным числом",
  483. }
  484. else:
  485. doc = {
  486. "en": f"{_sign_en}float less than {maximum}",
  487. "ru": f"{_sign_ru}дробным числом меньше {maximum}",
  488. }
  489. super().__init__(
  490. functools.partial(
  491. self._validate,
  492. minimum=minimum,
  493. maximum=maximum,
  494. ),
  495. doc,
  496. _internal_id="Float",
  497. )
  498. @staticmethod
  499. def _validate(
  500. value: ConfigAllowedTypes,
  501. /,
  502. *,
  503. minimum: typing.Optional[float] = None,
  504. maximum: typing.Optional[float] = None,
  505. ) -> float:
  506. try:
  507. value = float(str(value).strip().replace(",", "."))
  508. except ValueError:
  509. raise ValidationError(f"Passed value ({value}) must be a float")
  510. if minimum is not None and value < minimum:
  511. raise ValidationError(f"Passed value ({value}) is lower than minimum one")
  512. if maximum is not None and value > maximum:
  513. raise ValidationError(f"Passed value ({value}) is greater than maximum one")
  514. return value
  515. class TelegramID(Validator):
  516. def __init__(self):
  517. super().__init__(
  518. self._validate,
  519. "Telegram ID",
  520. _internal_id="TelegramID",
  521. )
  522. @staticmethod
  523. def _validate(value: ConfigAllowedTypes, /) -> int:
  524. e = ValidationError(f"Passed value ({value}) is not a valid telegram id")
  525. try:
  526. value = int(str(value).strip())
  527. except Exception:
  528. raise e
  529. if str(value).startswith("-100"):
  530. value = int(str(value)[4:])
  531. if value > 2**64 - 1 or value < 0:
  532. raise e
  533. return value
  534. class Union(Validator):
  535. def __init__(self, *validators):
  536. doc = {
  537. "en": "one of the following:\n",
  538. "ru": "одним из следующего:\n",
  539. }
  540. def case(x: str) -> str:
  541. return x[0].upper() + x[1:]
  542. for validator in validators:
  543. doc["en"] += f"- {case(validator.doc['en'])}\n"
  544. doc["ru"] += f"- {case(validator.doc['ru'])}\n"
  545. doc["en"] = doc["en"].strip()
  546. doc["ru"] = doc["ru"].strip()
  547. super().__init__(
  548. functools.partial(self._validate, validators=validators),
  549. doc,
  550. _internal_id="Union",
  551. )
  552. @staticmethod
  553. def _validate(
  554. value: ConfigAllowedTypes,
  555. /,
  556. *,
  557. validators: list,
  558. ) -> ConfigAllowedTypes:
  559. for validator in validators:
  560. try:
  561. return validator.validate(value)
  562. except ValidationError:
  563. pass
  564. raise ValidationError(f"Passed value ({value}) is not valid")
  565. class NoneType(Validator):
  566. def __init__(self):
  567. super().__init__(
  568. self._validate,
  569. "`None`",
  570. _internal_id="NoneType",
  571. )
  572. @staticmethod
  573. def _validate(value: ConfigAllowedTypes, /) -> None:
  574. if value not in {None, False, ""}:
  575. raise ValidationError(f"Passed value ({value}) is not None")
  576. return None
  577. class Hidden(Validator):
  578. def __init__(self, validator: typing.Optional[Validator] = None):
  579. if not validator:
  580. validator = String()
  581. super().__init__(
  582. functools.partial(self._validate, validator=validator),
  583. validator.doc,
  584. _internal_id="Hidden",
  585. )
  586. @staticmethod
  587. def _validate(
  588. value: ConfigAllowedTypes,
  589. /,
  590. *,
  591. validator: Validator,
  592. ) -> ConfigAllowedTypes:
  593. return validator.validate(value)
  594. class Emoji(Validator):
  595. """
  596. Checks whether passed argument is a valid emoji
  597. :param quantity: Number of emojis to be passed
  598. :param min_len: Minimum number of emojis
  599. :param max_len: Maximum number of emojis
  600. """
  601. def __init__(
  602. self,
  603. length: typing.Optional[int] = None,
  604. min_len: typing.Optional[int] = None,
  605. max_len: typing.Optional[int] = None,
  606. ):
  607. if length is not None:
  608. doc = {
  609. "en": f"{length} emojis",
  610. "ru": f"ровно {length} эмодзи",
  611. }
  612. elif min_len is not None and max_len is not None:
  613. doc = {
  614. "en": f"{min_len} to {max_len} emojis",
  615. "ru": f"от {min_len} до {max_len} эмодзи",
  616. }
  617. elif min_len is not None:
  618. doc = {
  619. "en": f"at least {min_len} emoji",
  620. "ru": f"не менее {min_len} эмодзи",
  621. }
  622. elif max_len is not None:
  623. doc = {
  624. "en": f"no more than {max_len} emojis",
  625. "ru": f"не более {max_len} эмодзи",
  626. }
  627. else:
  628. doc = {
  629. "en": "emoji",
  630. "ru": "эмодзи",
  631. }
  632. super().__init__(
  633. functools.partial(
  634. self._validate,
  635. length=length,
  636. min_len=min_len,
  637. max_len=max_len,
  638. ),
  639. doc,
  640. _internal_id="Emoji",
  641. )
  642. @staticmethod
  643. def _validate(
  644. value: ConfigAllowedTypes,
  645. /,
  646. *,
  647. length: typing.Optional[int],
  648. min_len: typing.Optional[int],
  649. max_len: typing.Optional[int],
  650. ) -> str:
  651. value = str(value)
  652. passed_length = len(list(grapheme.graphemes(value)))
  653. if length is not None and passed_length != length:
  654. raise ValidationError(f"Passed value ({value}) is not {length} emojis long")
  655. if (
  656. min_len is not None
  657. and max_len is not None
  658. and (passed_length < min_len or passed_length > max_len)
  659. ):
  660. raise ValidationError(
  661. f"Passed value ({value}) is not between {min_len} and {max_len} emojis"
  662. " long"
  663. )
  664. if min_len is not None and passed_length < min_len:
  665. raise ValidationError(
  666. f"Passed value ({value}) is not at least {min_len} emojis long"
  667. )
  668. if max_len is not None and passed_length > max_len:
  669. raise ValidationError(
  670. f"Passed value ({value}) is not no more than {max_len} emojis long"
  671. )
  672. if any(emoji not in ALLOWED_EMOJIS for emoji in grapheme.graphemes(value)):
  673. raise ValidationError(
  674. f"Passed value ({value}) is not a valid string with emojis"
  675. )
  676. return value
  677. class EntityLike(RegExp):
  678. def __init__(self):
  679. super().__init__(
  680. regex=r"^(?:@|https?://t\.me/)?(?:[a-zA-Z0-9_]{5,32}|[a-zA-Z0-9_]{1,32}\?[a-zA-Z0-9_]{1,32})$",
  681. description={
  682. "en": "link to entity, username or Telegram ID",
  683. "ru": "ссылка на сущность, имя пользователя или Telegram ID",
  684. },
  685. )