diff --git a/anjani/internal_plugins/spam_prediction.py b/anjani/internal_plugins/spam_prediction.py index 1de4ce864..e882e6e3d 100644 --- a/anjani/internal_plugins/spam_prediction.py +++ b/anjani/internal_plugins/spam_prediction.py @@ -17,10 +17,9 @@ import asyncio import re -from datetime import datetime, time, timedelta from hashlib import md5, sha256 from random import randint -from typing import Any, Callable, ClassVar, List, MutableMapping, Optional, Tuple +from typing import Any, Callable, ClassVar, List, Literal, MutableMapping, Optional, Tuple from pyrogram.errors import ( ChatAdminRequired, @@ -38,43 +37,60 @@ InlineKeyboardMarkup, Message, ) - -try: - from userbotindo import Classifier - - _run_predict = True -except ImportError: - from anjani.util.types import Classifier - - _run_predict = False +from pydantic import BaseModel, field_validator from anjani import command, filters, listener, plugin, util from anjani.core.metrics import SpamPredictionStat from anjani.util.misc import StopPropagation +class TextLanguage(BaseModel): + language: str + probability: float + + +class PredictionResult(BaseModel): + is_spam: bool + spam_score: float + ham_score: float + + @field_validator("spam_score", "ham_score") + def calc_score(cls, value: float) -> float: + return value * 100 + + def get_raw(self, field: Literal["ham", "spam"]) -> float: + return getattr(self, f"{field}_score") / 100 + + +class SpamDetectionResponse(BaseModel): + prediction: PredictionResult + language: TextLanguage + processed_text: str + + class SpamPrediction(plugin.Plugin): name: ClassVar[str] = "SpamPredict" helpable: ClassVar[bool] = True - disabled: ClassVar[bool] = not _run_predict db: util.db.AsyncCollection user_db: util.db.AsyncCollection setting_db: util.db.AsyncCollection - model: Classifier __predict_cost: int = 10 __log_channel: int = -1001314588569 async def on_load(self) -> None: - self.model = Classifier() + self._api_key = self.bot.config.SPAM_PREDICTION_API + self._predict_url = self.bot.config.SPAM_PREDICTION_URL + + if not self._api_key or not self._predict_url: + self.bot.unload_plugin(self) + return + self.db = self.bot.db.get_collection("SPAM_DUMP") self.user_db = self.bot.db.get_collection("USERS") self.setting_db = self.bot.db.get_collection("SPAM_PREDICT_SETTING") - await self.__load_model() - self.bot.loop.create_task(self.__refresh_model()) - async def on_chat_migrate(self, message: Message) -> None: await self.db.update_one( {"chat_id": message.migrate_from_chat_id}, @@ -90,26 +106,6 @@ async def on_plugin_restore(self, chat_id: int, data: MutableMapping[str, Any]) {"chat_id": chat_id}, {"$set": data[self.name]}, upsert=True ) - async def __refresh_model(self) -> None: - scheduled_time = time(hour=17) # Run at 00:00 WIB - while True: - now = datetime.utcnow() - date = now.date() - if now.time() > scheduled_time: - date = now.date() + timedelta(days=1) - then = datetime.combine(date, scheduled_time) - self.log.debug("Next model refresh at %s UTC", then) - await asyncio.sleep((then - now).total_seconds()) - await self.__load_model() - - async def __load_model(self) -> None: - self.log.info("Downloading spam prediction model!") - try: - await self.model.load_model(self.bot.http) - except RuntimeError: - self.log.warning("Failed to download prediction model!") - self.bot.unload_plugin(self) - @staticmethod def _build_hash(content: str) -> str: return sha256(content.strip().encode()).hexdigest() @@ -136,6 +132,20 @@ async def _collect_random_sample(self, proba: float, uid: Optional[int]) -> None }, ) # Do not upsert + async def check_spam(self, text: str) -> SpamDetectionResponse: + async with self.bot.http.post( + self._predict_url, + json={"text": text}, + headers={"x-api-key": self._api_key}, + ) as resp: + if resp.status != 200: + raise ValueError(f"Failed to get prediction: {resp.status}") + res = await resp.json() + if not res["data"]: + raise ValueError("Unexpected response") + + return SpamDetectionResponse(**res["data"]) + @listener.filters( filters.regex(r"spam_check_(?Pt|f)") | filters.regex(r"spam_ban_(?P.*)") ) @@ -314,26 +324,25 @@ async def spam_check(self, message: Message, text: str) -> None: except AttributeError: user = None - text_norm = self.model.normalize(text) - if len(text_norm.split()) < 4: # Skip short messages + try: + result = await self.check_spam(text) + except ValueError: + self.bot.log.debug("Failed to get prediction") return - response = await self.model.predict(text_norm) await self.bot.log_stat("predicted") SpamPredictionStat.labels("predicted").inc() - if response.size == 0: - return - probability = response[0][1] + probability = result.prediction.spam_score - await self._collect_random_sample(probability, user) + await self._collect_random_sample(result.prediction.get_raw("spam"), user) - if probability <= 0.5: + if probability <= 50: return content_hash = self._build_hash(text) identifier = self._build_hex(user) - proba_str = self.model.prob_to_string(probability) + proba_str = str(probability) msg_id = None # only log public chat @@ -372,7 +381,7 @@ async def spam_check(self, message: Message, text: str) -> None: "proba": probability, "msg_id": msg.id, "date": util.time.sec(), - "text": text_norm, + "text": result.processed_text, }, ) @@ -451,11 +460,6 @@ async def spam_check(self, message: Message, text: str) -> None: ) raise StopPropagation - @command.filters(filters.staff_only) - async def cmd_update_model(self, ctx: command.Context) -> Optional[str]: - await self.__load_model() - await ctx.respond("Done", delete_after=5) - @command.filters(filters.staff_only) async def cmd_spam(self, ctx: command.Context) -> Optional[str]: """Manual spam detection by bot staff""" @@ -475,13 +479,13 @@ async def cmd_spam(self, ctx: command.Context) -> Optional[str]: identifier = self._build_hex(user_id) content_hash = self._build_hash(content) - content_normalized = self.model.normalize(content.strip()) - pred = await self.model.predict(content_normalized) - if pred.size == 0: + try: + prediction = await self.check_spam(content) + except ValueError: return "Prediction failed" - proba = pred[0][1] - text = f"#SPAM\n\n**CPU Prediction**: `{self.model.prob_to_string(proba)}`\n" + proba = prediction.prediction.spam_score + text = f"#SPAM\n\n**CPU Prediction**: `{proba}`\n" if identifier: text += f"**Identifier**: `{identifier}`\n" @@ -491,7 +495,7 @@ async def cmd_spam(self, ctx: command.Context) -> Optional[str]: {"_id": content_hash}, { "$set": { - "text": content_normalized, + "text": prediction.processed_text, "spam": 1, "ham": 0, } @@ -538,16 +542,17 @@ async def cmd_predict(self, ctx: command.Context) -> Optional[str]: content = replied.text or replied.caption if not content: return await ctx.get_text("spampredict-empty") - content = self.model.normalize(content.strip()) - pred = await self.model.predict(content) - await self.bot.log_stat("predicted") - if pred.size == 0: + try: + prediction = await self.check_spam(content) + except ValueError: return await ctx.get_text("spampredict-failed") + await self.bot.log_stat("predicted") + textPrediction = ( - f"**Is Spam**: {await self.model.is_spam(content)}\n" - f"**Spam Prediction**: `{self.model.prob_to_string(pred[0][1])}`\n" - f"**Ham Prediction**: `{self.model.prob_to_string(pred[0][0])}`" + f"**Is Spam**: {prediction.prediction.is_spam}\n" + f"**Spam Prediction**: `{prediction.prediction.spam_score}`\n" + f"**Ham Prediction**: `{prediction.prediction.ham_score}`" ) await asyncio.gather( self.bot.log_stat("predicted"), diff --git a/anjani/util/config.py b/anjani/util/config.py index 0f8ba8f21..01aa127c7 100644 --- a/anjani/util/config.py +++ b/anjani/util/config.py @@ -28,6 +28,9 @@ class Config: HEALTH_CHECK_INTERVAL: Optional[int] HEALTH_CHECK_WEBHOOK_URL: Optional[str] + SPAM_PREDICTION_URL: Optional[str] + SPAM_PREDICTION_API: Optional[str] + IS_CI: bool def __init__(self) -> None: @@ -59,6 +62,9 @@ def __init__(self) -> None: self.HEALTH_CHECK_INTERVAL = int(getenv("HEALTH_CHECK_INTERVAL", 60)) self.HEALTH_CHECK_WEBHOOK_URL = getenv("HEALTH_CHECK_WEBHOOK_URL") + self.SPAM_PREDICTION_URL = getenv("SPAM_PREDICTION_URL") + self.SPAM_PREDICTION_API = getenv("SPAM_PREDICTION_API") + self.IS_CI = getenv("IS_CI", "false").lower() == "true" # check if all the required variables are set diff --git a/anjani/util/types.py b/anjani/util/types.py index 20172c9c9..2cefd557e 100644 --- a/anjani/util/types.py +++ b/anjani/util/types.py @@ -1,4 +1,5 @@ """Anjani custom types""" + # Copyright (C) 2020 - 2023 UserbotIndo Team, # # This program is free software: you can redistribute it and/or modify @@ -14,10 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from abc import abstractmethod, abstractproperty from typing import TYPE_CHECKING, Any, Callable, Protocol, TypeVar -from aiohttp import ClientSession from pyrogram.filters import Filter if TYPE_CHECKING: @@ -27,74 +26,8 @@ ChatId = TypeVar("ChatId", int, None, covariant=True) TextName = TypeVar("TextName", bound=str, covariant=True) NoFormat = TypeVar("NoFormat", bound=bool, covariant=True) -TypeData = TypeVar("TypeData", covariant=True) -DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) - - -class Instantiable(Protocol): - def __init__(self, *args: Any, **kwargs: Any) -> None: - raise NotImplementedError class CustomFilter(Filter): # skipcq: PYL-W0223 anjani: "Anjani" include_bot: bool - - -class NDArray(Protocol[TypeData]): - @abstractmethod - def __getitem__(self, key: int) -> Any: - raise NotImplementedError - - @abstractproperty - def size(self) -> int: - raise NotImplementedError - - -class Classifier(Protocol): - @abstractmethod - async def predict(self, text: str, **predict_params: Any) -> NDArray[Any]: - raise NotImplementedError - - @abstractmethod - async def load_model(self, http_client: ClientSession) -> None: - raise NotImplementedError - - @abstractmethod - async def is_spam(self, text: str) -> bool: - raise NotImplementedError - - @staticmethod - @abstractmethod - def normalize(text: str) -> str: - raise NotImplementedError - - @staticmethod - @abstractmethod - def prob_to_string(value: float) -> str: - raise NotImplementedError - - -class WebServer(Protocol): - @abstractmethod - async def run(self) -> None: - raise NotImplementedError - - @abstractmethod - async def stop(self) -> None: - raise NotImplementedError - - @abstractmethod - async def add_router(self, **router_param: Any) -> None: - raise NotImplementedError - - -class Router(Instantiable): - def get(self, *args, **kwargs) -> Callable[[DecoratedCallable], DecoratedCallable]: - raise NotImplementedError - - def post(self, *args, **kwargs) -> Callable[[DecoratedCallable], DecoratedCallable]: - raise NotImplementedError - - def put(self, *args, **kwargs) -> Callable[[DecoratedCallable], DecoratedCallable]: - raise NotImplementedError diff --git a/poetry.lock b/poetry.lock index ac0f7b66f..856920877 100644 --- a/poetry.lock +++ b/poetry.lock @@ -188,6 +188,17 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "3.7.1" @@ -868,6 +879,129 @@ files = [ {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, ] +[[package]] +name = "pydantic" +version = "2.8.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.20.1" +typing-extensions = [ + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pyflakes" version = "3.2.0" @@ -1469,4 +1603,4 @@ uvloop = ["uvloop"] [metadata] lock-version = "2.0" python-versions = "~=3.9" -content-hash = "dc079f406d307784ed003362b018f7de134c4a91f8f07c1094a8594a57c4548f" +content-hash = "780b309916bcdb50e3305939b8241be25313e398fb6356eda2fcababb8fd99c6" diff --git a/pyproject.toml b/pyproject.toml index f2b9565cc..bfb28db40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "anjani" -version = "2.14.8" +version = "2.14.9" description = "Telegram group management bot" license = "GPL-3.0-or-later" authors = [ @@ -52,6 +52,7 @@ uvloop = { version = ">=0.17,<0.20", optional = true, platform = "linux" } yarl = "^1.8.2" aiocache = "^0.12.0" prometheus-client = "^0.20.0" +pydantic = "^2.8.2" [tool.poetry.extras] all = ["uvloop"]