diff --git a/.gitignore b/.gitignore index aee2a57..8f8b73c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ dist/ __pycache__/ tests/ .vscode/ -*.egg-info/ \ No newline at end of file +*.egg-info/ +.idea \ No newline at end of file diff --git a/README.md b/README.md index c77a805..fabfe5d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ pip install discordanalytics ## Usage -> **Note:** To use Discord Analytics, you need to have an API token. Check the docs for more informations : https://docs.discordanalytics.xyz/get-started/bot-registration +> **Note:** To use Discord Analytics, you need to have an API token. Check the docs for more informations : https://discordanalytics.xyz/docs/main/get-started/bot-registration ```python import discord diff --git a/discordanalytics/__init__.py b/discordanalytics/__init__.py index 25bc051..5a751e3 100644 --- a/discordanalytics/__init__.py +++ b/discordanalytics/__init__.py @@ -1,6 +1,6 @@ __title__ = 'discordanalytics' __author__ = "ValDesign" __license__ = "MIT" -__version__ = "3.1.6" +__version__ = "3.5.0" from .client import * diff --git a/discordanalytics/client.py b/discordanalytics/client.py index 07a8275..63f4256 100644 --- a/discordanalytics/client.py +++ b/discordanalytics/client.py @@ -1,4 +1,5 @@ import asyncio +from collections import Counter from datetime import datetime from typing import Literal import discord @@ -12,6 +13,7 @@ class ApiEndpoints: BASE_URL = "https://discordanalytics.xyz/api" BOT_URL = f"{BASE_URL}/bots/:id" STATS_URL = f"{BASE_URL}/bots/:id/stats" + EVENT_URL = f"{BASE_URL}/bots/:id/events/:event_key" class ErrorCodes: INVALID_CLIENT_TYPE = "Invalid client type, please use a valid client." @@ -21,6 +23,65 @@ class ErrorCodes: DATA_NOT_SENT = "Data cannot be sent to the API, I will try again in a minute." SUSPENDED_BOT = "Your bot has been suspended, please check your mailbox for more information." INVALID_EVENTS_COUNT = "invalid events count" + INVALID_VALUE_TYPE = "invalid value type" + INVALID_EVENT_KEY = "invalid event key" + +class Event: + def __init__(self, analytics, event_key: str): + self.analytics = analytics + self.event_key = event_key + self.last_action = "" + + self.ensure() + + async def ensure(self): + if not isinstance(self.event_key, str) or len(self.event_key) < 1 or len(self.event_key) > 50: + raise ValueError(ErrorCodes.INVALID_EVENTS_COUNT) + + if self.event_key not in self.analytics.stats["custom_events"]: + if self.analytics.debug: + print(f"[DISCORDANALYTICS] Fetching value for event {self.event_key}") + + url = ApiEndpoints.EVENT_URL.replace(":id", str(self.analytics.client.user.id)).replace(":event_key", self.event_key) + + res = await self.analytics.api_call_with_retries("GET", url, self.analytics.headers) + + if res is not None and self.last_action != 'set': + self.analytics.stats["custom_events"][self.event_key] = (self.analytics.stats["custom_events"].get(self.event_key, 0) + (await res.json()).get("value", 0)) + + if self.analytics.debug: + print(f"[DISCORDANALYTICS] Value fetched for event {self.event_key}") + + def increment(self, count: int = 1): + if self.analytics.debug: + print(f"[DISCORDANALYTICS] Incrementing event {self.event_key} by {count}") + if not isinstance(count, int) or count < 0: + raise ValueError(ErrorCodes.INVALID_VALUE_TYPE) + self.analytics.stats["custom_events"][self.event_key] = self.analytics.stats["custom_events"].get(self.event_key, 0) + count + self.last_action = "increment" + + def decrement(self, count: int = 1): + if self.analytics.debug: + print(f"[DISCORDANALYTICS] Decrementing event {self.event_key} by {count}") + if not isinstance(count, int) or count < 0 or self.get() - count < 0: + raise ValueError(ErrorCodes.INVALID_VALUE_TYPE) + self.analytics.stats["custom_events"][self.event_key] = self.analytics.stats["custom_events"].get(self.event_key, 0) - count + self.last_action = "decrement" + + def set(self, value: int): + if self.analytics.debug: + print(f"[DISCORDANALYTICS] Setting event {self.event_key} to {value}") + if not isinstance(value, int) or value < 0: + raise ValueError(ErrorCodes.INVALID_VALUE_TYPE) + self.analytics.stats["custom_events"][self.event_key] = value + self.last_action = "set" + + def get(self): + if self.analytics.debug: + print(f"[DISCORDANALYTICS] Getting event {self.event_key}") + if not isinstance(self.event_key, str) or len(self.event_key) < 1 or len(self.event_key) > 50: + raise ValueError(ErrorCodes.INVALID_EVENTS_COUNT) + return self.analytics.stats["custom_events"][self.event_key] class DiscordAnalytics(): def __init__(self, client: discord.Client, api_key: str, debug: bool = False, chunk_guilds_at_startup: bool = True): @@ -54,7 +115,8 @@ def __init__(self, client: discord.Client, api_key: str, debug: bool = False, ch "new_member": 0, "other": 0, "private_message": 0 - } + }, + "custom_events": {}, # {[event_key:str]: int} } def track_events(self): @@ -89,6 +151,8 @@ async def api_call_with_retries(self, method, url, headers, json, max_retries=5, raise ValueError(ErrorCodes.INVALID_API_TOKEN) elif response.status == 423: raise ValueError(ErrorCodes.SUSPENDED_BOT) + elif response.status == 404 and "events" in url: + raise ValueError(ErrorCodes.INVALID_EVENT_KEY) else: raise ValueError(ErrorCodes.INVALID_RESPONSE) except (aiohttp.ClientError, ValueError) as e: @@ -121,10 +185,10 @@ async def init(self): print("[DISCORDANALYTICS] Instance successfully initialized") if self.debug: - if "--dev" in sys.argv: - print("[DISCORDANALYTICS] DevMode is enabled. Stats will be sent every 30s.") + if "--fast" in sys.argv: + print("[DISCORDANALYTICS] Fast mode is enabled. Stats will be sent every 30s.") else: - print("[DISCORDANALYTICS] DevMode is disabled. Stats will be sent every 5 minutes.") + print("[DISCORDANALYTICS] Fast mode is disabled. Stats will be sent every 5 minutes.") if not self.chunk_guilds: await self.load_members_for_all_guilds() @@ -193,30 +257,28 @@ async def send_stats(self): "new_member": 0, "other": 0, "private_message": 0 - } + }, + "custom_events": self.stats["custom_events"], } - await asyncio.sleep(30 if "--dev" in sys.argv else 300) + await asyncio.sleep(30 if "--fast" in sys.argv else 300) def calculate_guild_members_repartition(self): - result = { - "little": 0, - "medium": 0, - "big": 0, - "huge": 0 + thresholds = { + "little": lambda count: count <= 100, + "medium": lambda count: 100 < count <= 500, + "big": lambda count: 500 < count <= 1500, + "huge": lambda count: count > 1500 } - for guild in self.client.guilds: - if guild.member_count <= 100: - result["little"] += 1 - elif 100 < guild.member_count <= 500: - result["medium"] += 1 - elif 500 < guild.member_count <= 1500: - result["big"] += 1 - else: - result["huge"] += 1 + counter = Counter() - return result + for guild in self.client.guilds: + for key, condition in thresholds.items(): + if condition(guild.member_count): + counter[key] += 1 + break + return dict(counter) def track_interactions(self, interaction: discord.Interaction): if self.debug: @@ -314,4 +376,13 @@ def trackGuilds(self, guild: discord.Guild, type: Literal["create", "delete"]): if type == "create": self.stats["addedGuilds"] += 1 elif type == "delete": - self.stats["removedGuilds"] += 1 \ No newline at end of file + self.stats["removedGuilds"] += 1 + + def events(self, event_key: str): + if self.debug: + print(f"[DISCORDANALYTICS] Event {event_key} triggered") + if not self.client.is_ready(): + raise ValueError(ErrorCodes.CLIENT_NOT_READY) + if event_key not in self.stats["custom_events"]: + self.stats["custom_events"][event_key] = 0 + return Event(self, event_key) diff --git a/pyproject.toml b/pyproject.toml index 5f08eb3..ea20143 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "discordanalytics" description = "A Python package for interacting with Discord Analytics API" -version = "3.1.6" +version = "3.5.0" authors = [{ name = "ValDesign", email = "valdesign.dev@gmail.com" }] requires-python = ">=3.8" readme = "README.md" diff --git a/requirements.txt b/requirements.txt index b6f09b0..8583ee3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.4.0 -aiohttp==3.10.10 -setuptools==75.2.0 -wheel==0.44.0 \ No newline at end of file +discord.py==2.6.4 +aiohttp==3.13.2 +setuptools==80.9.0 +wheel==0.45.1 \ No newline at end of file