api_protection.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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 asyncio
  9. import io
  10. import json
  11. import logging
  12. import time
  13. from telethon.tl.types import Message
  14. from telethon.tl import functions
  15. from telethon.tl.tlobject import TLRequest
  16. from .. import loader, utils
  17. from ..inline.types import InlineCall
  18. logger = logging.getLogger(__name__)
  19. GROUPS = [
  20. "auth",
  21. "account",
  22. "users",
  23. "contacts",
  24. "messages",
  25. "updates",
  26. "photos",
  27. "upload",
  28. "help",
  29. "channels",
  30. "bots",
  31. "payments",
  32. "stickers",
  33. "phone",
  34. "langpack",
  35. "folders",
  36. "stats",
  37. ]
  38. def decapitalize(string: str) -> str:
  39. return string[0].lower() + string[1:]
  40. CONSTRUCTORS = {
  41. decapitalize(
  42. method.__class__.__name__.rsplit("Request", 1)[0]
  43. ): method.CONSTRUCTOR_ID
  44. for method in utils.array_sum(
  45. [
  46. [
  47. method
  48. for method in dir(getattr(functions, group))
  49. if isinstance(method, TLRequest)
  50. ]
  51. for group in GROUPS
  52. ]
  53. )
  54. }
  55. @loader.tds
  56. class APIRatelimiterMod(loader.Module):
  57. """Helps userbot avoid spamming Telegram API"""
  58. strings = {
  59. "name": "APILimiter",
  60. "warning": (
  61. "<emoji document_id=6319093650693293883>☣️</emoji>"
  62. " <b>WARNING!</b>\n\nYour account exceeded the limit of requests, specified"
  63. " in config. In order to prevent Telegram API Flood, userbot has been"
  64. " <b>fully frozen</b> for {} seconds. Further info is provided in attached"
  65. " file. \n\nIt is recommended to get help in <code>{prefix}support</code>"
  66. " group!\n\nIf you think, that it is an intended behavior, then wait until"
  67. " userbot gets unlocked and next time, when you will be going to perform"
  68. " such an operation, use <code>{prefix}suspend_api_protect</code> &lt;time"
  69. " in seconds&gt;"
  70. ),
  71. "args_invalid": (
  72. "<emoji document_id=6319093650693293883>☣️</emoji> <b>Invalid arguments</b>"
  73. ),
  74. "suspended_for": (
  75. "<emoji document_id=5458450833857322148>👌</emoji> <b>API Flood Protection"
  76. " is disabled for {} seconds</b>"
  77. ),
  78. "test": (
  79. "<emoji document_id=6319093650693293883>☣️</emoji> <b>This action will"
  80. " expose your account to flooding Telegram API.</b> <i>In order to confirm,"
  81. " that you really know, what you are doing, complete this simple test -"
  82. " find the emoji, differing from others</i>"
  83. ),
  84. "on": (
  85. "<emoji document_id=5458450833857322148>👌</emoji> <b>Protection enabled</b>"
  86. ),
  87. "off": (
  88. "<emoji document_id=5458450833857322148>👌</emoji> <b>Protection"
  89. " disabled</b>"
  90. ),
  91. "u_sure": (
  92. "<emoji document_id=6319093650693293883>☣️</emoji> <b>Are you sure?</b>"
  93. ),
  94. }
  95. strings_ru = {
  96. "warning": (
  97. "<emoji document_id=6319093650693293883>☣️</emoji>"
  98. " <b>ВНИМАНИЕ!</b>\n\nАккаунт вышел за лимиты запросов, указанные в"
  99. " конфиге. С целью предотвращения флуда Telegram API, юзербот был"
  100. " <b>полностью заморожен</b> на {} секунд. Дополнительная информация"
  101. " прикреплена в файле ниже. \n\nРекомендуется обратиться за помощью в"
  102. " <code>{prefix}support</code> группу!\n\nЕсли ты считаешь, что это"
  103. " запланированное поведение юзербота, просто подожди, пока закончится"
  104. " таймер и в следующий раз, когда запланируешь выполнять такую"
  105. " ресурсозатратную операцию, используй"
  106. " <code>{prefix}suspend_api_protect</code> &lt;время в секундах&gt;"
  107. ),
  108. "args_invalid": (
  109. "<emoji document_id=6319093650693293883>☣️</emoji> <b>Неверные"
  110. " аргументы</b>"
  111. ),
  112. "suspended_for": (
  113. "<emoji document_id=5458450833857322148>👌</emoji> <b>Защита API отключена"
  114. " на {} секунд</b>"
  115. ),
  116. "test": (
  117. "<emoji document_id=6319093650693293883>☣️</emoji> <b>Это действие"
  118. " открывает юзерботу возможность флудить Telegram API.</b> <i>Для того,"
  119. " чтобы убедиться, что ты действительно уверен в том, что делаешь - реши"
  120. " простенький тест - найди отличающийся эмодзи.</i>"
  121. ),
  122. "on": "<emoji document_id=5458450833857322148>👌</emoji> <b>Защита включена</b>",
  123. "off": (
  124. "<emoji document_id=5458450833857322148>👌</emoji> <b>Защита отключена</b>"
  125. ),
  126. "u_sure": "<emoji document_id=6319093650693293883>☣️</emoji> <b>Ты уверен?</b>",
  127. }
  128. _ratelimiter = []
  129. _suspend_until = 0
  130. _lock = False
  131. def __init__(self):
  132. self.config = loader.ModuleConfig(
  133. loader.ConfigValue(
  134. "time_sample",
  135. 15,
  136. lambda: "Time sample through which the bot will count requests",
  137. validator=loader.validators.Integer(minimum=1),
  138. ),
  139. loader.ConfigValue(
  140. "threshold",
  141. 100,
  142. lambda: "Threshold of requests to trigger protection",
  143. validator=loader.validators.Integer(minimum=10),
  144. ),
  145. loader.ConfigValue(
  146. "local_floodwait",
  147. 30,
  148. lambda: "Freeze userbot for this amount of time, if request limit exceeds",
  149. validator=loader.validators.Integer(minimum=10, maximum=3600),
  150. ),
  151. loader.ConfigValue(
  152. "forbidden_methods",
  153. ["joinChannel", "importChatInvite"],
  154. lambda: "Forbid specified methods from being executed throughout external modules",
  155. validator=loader.validators.MultiChoice(
  156. [
  157. "sendReaction",
  158. "joinChannel",
  159. "importChatInvite",
  160. ]
  161. ),
  162. on_change=lambda: self._client.forbid_constructors(
  163. map(
  164. lambda x: CONSTRUCTORS[x], self.config["forbidden_constructors"]
  165. )
  166. ),
  167. ),
  168. )
  169. async def client_ready(self):
  170. asyncio.ensure_future(self._install_protection())
  171. async def _install_protection(self):
  172. await asyncio.sleep(30) # Restart lock
  173. if hasattr(self._client._call, "_old_call_rewritten"):
  174. raise loader.SelfUnload("Already installed")
  175. old_call = self._client._call
  176. async def new_call(
  177. sender: "MTProtoSender", # type: ignore
  178. request: "TLRequest", # type: ignore
  179. ordered: bool = False,
  180. flood_sleep_threshold: int = None,
  181. ):
  182. if time.perf_counter() > self._suspend_until and not self.get(
  183. "disable_protection",
  184. True,
  185. ):
  186. request_name = type(request).__name__
  187. self._ratelimiter += [[request_name, time.perf_counter()]]
  188. self._ratelimiter = list(
  189. filter(
  190. lambda x: time.perf_counter() - x[1]
  191. < int(self.config["time_sample"]),
  192. self._ratelimiter,
  193. )
  194. )
  195. if (
  196. len(self._ratelimiter) > int(self.config["threshold"])
  197. and not self._lock
  198. ):
  199. self._lock = True
  200. report = io.BytesIO(
  201. json.dumps(
  202. self._ratelimiter,
  203. indent=4,
  204. ).encode("utf-8")
  205. )
  206. report.name = "local_fw_report.json"
  207. await self.inline.bot.send_document(
  208. self.tg_id,
  209. report,
  210. caption=self.strings("warning").format(
  211. self.config["local_floodwait"],
  212. prefix=self.get_prefix(),
  213. ),
  214. )
  215. # It is intented to use time.sleep instead of asyncio.sleep
  216. time.sleep(int(self.config["local_floodwait"]))
  217. self._lock = False
  218. return await old_call(sender, request, ordered, flood_sleep_threshold)
  219. self._client._call = new_call
  220. self._client._old_call_rewritten = old_call
  221. self._client._call._hikka_overwritten = True
  222. logger.debug("Successfully installed ratelimiter")
  223. async def on_unload(self):
  224. if hasattr(self._client, "_old_call_rewritten"):
  225. self._client._call = self._client._old_call_rewritten
  226. delattr(self._client, "_old_call_rewritten")
  227. logger.debug("Successfully uninstalled ratelimiter")
  228. @loader.command(ru_doc="<время в секундах> - Заморозить защиту API на N секунд")
  229. async def suspend_api_protect(self, message: Message):
  230. """<time in seconds> - Suspend API Ratelimiter for n seconds"""
  231. args = utils.get_args_raw(message)
  232. if not args or not args.isdigit():
  233. await utils.answer(message, self.strings("args_invalid"))
  234. return
  235. self._suspend_until = time.perf_counter() + int(args)
  236. await utils.answer(message, self.strings("suspended_for").format(args))
  237. @loader.command(ru_doc="Включить/выключить защиту API")
  238. async def api_fw_protection(self, message: Message):
  239. """Toggle API Ratelimiter"""
  240. await self.inline.form(
  241. message=message,
  242. text=self.strings("u_sure"),
  243. reply_markup=[
  244. {"text": "🚫 No", "action": "close"},
  245. {"text": "✅ Yes", "callback": self._finish},
  246. ],
  247. )
  248. async def _finish(self, call: InlineCall):
  249. state = self.get("disable_protection", True)
  250. self.set("disable_protection", not state)
  251. await call.edit(self.strings("on" if state else "off"))