Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
aa05f5a
fix: Update documentation link for bot registration
ValDesign22 Apr 23, 2025
4465b00
chore: Bump version to 3.2.0 and add custom event handling
ValDesign22 Apr 23, 2025
0dd3f28
fix: Added custom_events reset after stats are sent
ValDesign22 Apr 23, 2025
41ec386
fix: Update error handling for invalid value types in Event methods
ValDesign22 Apr 23, 2025
d8c69d1
fix: Add event existence check in Event methods and improve error han…
ValDesign22 Apr 23, 2025
59f199b
refactor: Simplify guild members repartition calculation using Counte…
ValDesign22 Apr 24, 2025
3691a4b
fix: Correct typo in variable name from 'tresholds' to 'thresholds' i…
ValDesign22 Apr 24, 2025
a05c5b8
chore: Update version number to 3.5.0 in __init__.py and pyproject.toml
ValDesign22 Apr 24, 2025
e4b6853
feat: Add event URL and enhance event validation in Event class
ValDesign22 Apr 24, 2025
5d41bbc
fix: Remove redundant ensure() calls in Event methods to streamline e…
ValDesign22 Apr 25, 2025
1558d86
refactor: Change event API call from POST to GET in Event class to al…
ValDesign22 Apr 25, 2025
8c96929
chore: Update package versions in requirements.txt to latest stable r…
ValDesign22 Apr 27, 2025
63e5ef2
fix: Fixed negative value on custom_events
ValDesign22 Apr 30, 2025
7efb75e
chore: upgraded dependencies
Nonolanlan1007 Dec 19, 2025
b3b809b
chore: added .idea to .gitignore
Nonolanlan1007 Dec 19, 2025
11e31dc
fix: custom events
Nonolanlan1007 Dec 19, 2025
dd123b4
refactor: changed --dev to --fast
Nonolanlan1007 Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ dist/
__pycache__/
tests/
.vscode/
*.egg-info/
*.egg-info/
.idea
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion discordanalytics/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__title__ = 'discordanalytics'
__author__ = "ValDesign"
__license__ = "MIT"
__version__ = "3.1.6"
__version__ = "3.5.0"

from .client import *
115 changes: 93 additions & 22 deletions discordanalytics/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
from collections import Counter
from datetime import datetime
from typing import Literal
import discord
Expand All @@ -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."
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
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)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
discord.py==2.4.0
aiohttp==3.10.10
setuptools==75.2.0
wheel==0.44.0
discord.py==2.6.4
aiohttp==3.13.2
setuptools==80.9.0
wheel==0.45.1