Skip to content

Commit 6c22c9b

Browse files
authored
Merge pull request #3 from JamsRepos/master
Added Support for Chunks & Async Changes
2 parents da6be77 + 855f3aa commit 6c22c9b

File tree

4 files changed

+124
-94
lines changed

4 files changed

+124
-94
lines changed

discordanalytics/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
__title__ = 'discordanalytics'
22
__author__ = "ValDesign"
33
__license__ = "MIT"
4-
__version__ = "3.0.4"
4+
__version__ = "3.1.0"
55

66
from .client import *

discordanalytics/client.py

Lines changed: 121 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import asyncio
2-
from datetime import datetime, timezone
2+
from datetime import datetime
33
from typing import Literal
44
import discord
55
from discord.enums import InteractionType
6-
import requests
6+
import aiohttp
77
import sys
88

99
from .__init__ import __version__
@@ -20,15 +20,14 @@ class ErrorCodes:
2020
INVALID_API_TOKEN = "Invalid API token, please get one at " + ApiEndpoints.BASE_URL.split("/api")[0] + " and try again."
2121
DATA_NOT_SENT = "Data cannot be sent to the API, I will try again in a minute."
2222
SUSPENDED_BOT = "Your bot has been suspended, please check your mailbox for more information."
23-
INSTANCE_NOT_INITIALIZED = "It seem that you didn't initialize your instance. Please check the docs for more informations."
2423
INVALID_EVENTS_COUNT = "invalid events count"
2524

2625
class DiscordAnalytics():
27-
def __init__(self, client: discord.Client, api_key: str, debug: bool = False):
26+
def __init__(self, client: discord.Client, api_key: str, debug: bool = False, chunk_guilds_at_startup: bool = True):
2827
self.client = client
2928
self.api_key = api_key
3029
self.debug = debug
31-
self.is_ready = False
30+
self.chunk_guilds = chunk_guilds_at_startup
3231
self.headers = {
3332
"Content-Type": "application/json",
3433
"Authorization": f"Bot {api_key}"
@@ -57,105 +56,135 @@ def __init__(self, client: discord.Client, api_key: str, debug: bool = False):
5756
"private_message": 0
5857
}
5958
}
60-
59+
6160
def track_events(self):
6261
if not self.client.is_ready():
6362
@self.client.event
6463
async def on_ready():
65-
self.init()
64+
await self.init()
6665
else:
67-
self.init()
66+
asyncio.create_task(self.init())
67+
6868
@self.client.event
6969
async def on_interaction(interaction: discord.Interaction):
7070
self.track_interactions(interaction)
71+
7172
@self.client.event
7273
async def on_guild_join(guild: discord.Guild):
7374
self.trackGuilds(guild, "create")
75+
7476
@self.client.event
7577
async def on_guild_remove(guild: discord.Guild):
7678
self.trackGuilds(guild, "delete")
77-
78-
def init(self):
79+
80+
async def init(self):
7981
if not isinstance(self.client, discord.Client):
8082
raise ValueError(ErrorCodes.INVALID_CLIENT_TYPE)
8183
if not self.client.is_ready():
8284
raise ValueError(ErrorCodes.CLIENT_NOT_READY)
83-
84-
response = requests.patch(
85-
ApiEndpoints.BOT_URL.replace(":id", str(self.client.user.id)),
86-
headers=self.headers,
87-
json={
88-
"username": self.client.user.name,
89-
"avatar": self.client.user._avatar,
90-
"framework": "discord.py",
91-
"version": __version__
92-
}
93-
)
94-
95-
if response.status_code == 401:
96-
raise ValueError(ErrorCodes.INVALID_API_TOKEN)
97-
if response.status_code == 423:
98-
raise ValueError(ErrorCodes.SUSPENDED_BOT)
99-
if response.status_code != 200:
100-
raise ValueError(ErrorCodes.INVALID_RESPONSE)
101-
85+
86+
# Proceed with initialization, API calls, etc.
87+
async with aiohttp.ClientSession() as session:
88+
async with session.patch(
89+
ApiEndpoints.BOT_URL.replace(":id", str(self.client.user.id)),
90+
headers=self.headers,
91+
json={
92+
"username": self.client.user.name,
93+
"avatar": self.client.user._avatar,
94+
"framework": "discord.py",
95+
"version": __version__
96+
}
97+
) as response:
98+
if response.status == 401:
99+
raise ValueError(ErrorCodes.INVALID_API_TOKEN)
100+
elif response.status == 423:
101+
raise ValueError(ErrorCodes.SUSPENDED_BOT)
102+
elif response.status != 200:
103+
raise ValueError(ErrorCodes.INVALID_RESPONSE)
104+
102105
if self.debug:
103106
print("[DISCORDANALYTICS] Instance successfully initialized")
104-
self.is_ready = True
105107

106108
if self.debug:
107109
if "--dev" in sys.argv:
108110
print("[DISCORDANALYTICS] DevMode is enabled. Stats will be sent every 30s.")
109111
else:
110112
print("[DISCORDANALYTICS] DevMode is disabled. Stats will be sent every 5 minutes.")
111113

114+
if not self.chunk_guilds:
115+
await self.load_members_for_all_guilds()
116+
112117
self.client.loop.create_task(self.send_stats())
113118

119+
async def load_members_for_all_guilds(self):
120+
"""Load members for each guild when chunk_guilds_at_startup is False."""
121+
tasks = [self.load_members_for_guild(guild) for guild in self.client.guilds]
122+
await asyncio.gather(*tasks)
123+
124+
async def load_members_for_guild(self, guild: discord.Guild):
125+
"""Load members for a single guild."""
126+
try:
127+
await guild.chunk()
128+
if self.debug:
129+
print(f"[DISCORDANALYTICS] Chunked members for guild {guild.name}")
130+
except Exception:
131+
await self.query_members(guild)
132+
133+
async def query_members(self, guild: discord.Guild):
134+
"""Query members by prefix if chunking fails."""
135+
try:
136+
members = await guild.query_members(query="", limit=1000)
137+
if self.debug:
138+
print(f"[DISCORDANALYTICS] Queried members for guild {guild.name}: {len(members)} members found.")
139+
except Exception as e:
140+
print(f"[DISCORDANALYTICS] Error querying members for guild {guild.name}: {e}")
141+
114142
async def send_stats(self):
115143
await self.client.wait_until_ready()
116144
while not self.client.is_closed():
117145
if self.debug:
118146
print("[DISCORDANALYTICS] Sending stats...")
119-
147+
120148
guild_count = len(self.client.guilds)
121149
user_count = len(self.client.users)
122150

123-
response = requests.post(
124-
ApiEndpoints.STATS_URL.replace(":id", str(self.client.user.id)),
125-
headers=self.headers,
126-
json=self.stats
127-
)
128-
129-
if response.status_code == 401:
130-
raise ValueError(ErrorCodes.INVALID_API_TOKEN)
131-
if response.status_code == 423:
132-
raise ValueError(ErrorCodes.SUSPENDED_BOT)
133-
if response.status_code != 200:
134-
raise ValueError(ErrorCodes.INVALID_RESPONSE)
135-
if response.status_code == 200:
136-
if self.debug:
137-
print(f"[DISCORDANALYTICS] Stats {self.stats} sent to the API")
138-
139-
self.stats = {
140-
"date": datetime.today().strftime("%Y-%m-%d"),
141-
"guilds": guild_count,
142-
"users": user_count,
143-
"interactions": [],
144-
"locales": [],
145-
"guildsLocales": [],
146-
"guildMembers": self.calculate_guild_members_repartition(),
147-
"guildsStats": [],
148-
"addedGuilds": 0,
149-
"removedGuilds": 0,
150-
"users_type": {
151-
"admin": 0,
152-
"moderator": 0,
153-
"new_member": 0,
154-
"other": 0,
155-
"private_message": 0
156-
}
157-
}
158-
151+
async with aiohttp.ClientSession() as session:
152+
async with session.post(
153+
ApiEndpoints.STATS_URL.replace(":id", str(self.client.user.id)),
154+
headers=self.headers,
155+
json=self.stats
156+
) as response:
157+
if response.status == 401:
158+
raise ValueError(ErrorCodes.INVALID_API_TOKEN)
159+
elif response.status == 423:
160+
raise ValueError(ErrorCodes.SUSPENDED_BOT)
161+
elif response.status != 200:
162+
raise ValueError(ErrorCodes.INVALID_RESPONSE)
163+
164+
if response.status == 200:
165+
if self.debug:
166+
print(f"[DISCORDANALYTICS] Stats {self.stats} sent to the API")
167+
168+
self.stats = {
169+
"date": datetime.today().strftime("%Y-%m-%d"),
170+
"guilds": guild_count,
171+
"users": user_count,
172+
"interactions": [],
173+
"locales": [],
174+
"guildsLocales": [],
175+
"guildMembers": self.calculate_guild_members_repartition(),
176+
"guildsStats": [],
177+
"addedGuilds": 0,
178+
"removedGuilds": 0,
179+
"users_type": {
180+
"admin": 0,
181+
"moderator": 0,
182+
"new_member": 0,
183+
"other": 0,
184+
"private_message": 0
185+
}
186+
}
187+
159188
await asyncio.sleep(10 if "--dev" in sys.argv else 300)
160189

161190
def calculate_guild_members_repartition(self):
@@ -177,13 +206,14 @@ def calculate_guild_members_repartition(self):
177206
result["huge"] += 1
178207

179208
return result
180-
209+
181210
def track_interactions(self, interaction: discord.Interaction):
182211
if self.debug:
183212
print("[DISCORDANALYTICS] Track interactions triggered")
184-
if not self.is_ready:
185-
raise ValueError(ErrorCodes.INSTANCE_NOT_INITIALIZED)
186-
213+
214+
if not self.client.is_ready():
215+
raise ValueError(ErrorCodes.CLIENT_NOT_READY)
216+
187217
locale = next((x for x in self.stats["locales"] if x["locale"] == interaction.locale.value), None)
188218
if locale is not None:
189219
locale["number"] += 1
@@ -193,8 +223,9 @@ def track_interactions(self, interaction: discord.Interaction):
193223
"number": 1
194224
})
195225

196-
if interaction.type == InteractionType.application_command or interaction.type == InteractionType.autocomplete:
197-
interaction_data = next((x for x in self.stats["interactions"] if x["name"] == interaction.data["name"] and x["type"] == interaction.type.value), None)
226+
if interaction.type in {InteractionType.application_command, InteractionType.autocomplete}:
227+
interaction_data = next((x for x in self.stats["interactions"]
228+
if x["name"] == interaction.data["name"] and x["type"] == interaction.type.value), None)
198229
if interaction_data is not None:
199230
interaction_data["number"] += 1
200231
else:
@@ -203,8 +234,9 @@ def track_interactions(self, interaction: discord.Interaction):
203234
"number": 1,
204235
"type": interaction.type.value
205236
})
206-
elif interaction.type == InteractionType.component or interaction.type == InteractionType.modal_submit:
207-
interaction_data = next((x for x in self.stats["interactions"] if x["name"] == interaction.data["custom_id"] and x["type"] == interaction.type.value), None)
237+
elif interaction.type in {InteractionType.component, InteractionType.modal_submit}:
238+
interaction_data = next((x for x in self.stats["interactions"]
239+
if x["name"] == interaction.data["custom_id"] and x["type"] == interaction.type.value), None)
208240
if interaction_data is not None:
209241
interaction_data["number"] += 1
210242
else:
@@ -219,9 +251,9 @@ def track_interactions(self, interaction: discord.Interaction):
219251
else:
220252
guilds = []
221253
for guild in self.client.guilds:
222-
if guild.preferred_locale is not None:
254+
if guild.preferred_locale:
223255
guild_locale = next((x for x in guilds if x["locale"] == guild.preferred_locale.value), None)
224-
if guild_locale is not None:
256+
if guild_locale:
225257
guild_locale["number"] += 1
226258
else:
227259
guilds.append({
@@ -232,7 +264,7 @@ def track_interactions(self, interaction: discord.Interaction):
232264

233265
guild_data = next((x for x in self.stats["guildsStats"] if x["guildId"] == str(interaction.guild.id)), None)
234266
guild_icon = interaction.guild.icon.key if interaction.guild.icon else None
235-
if guild_data is not None:
267+
if guild_data:
236268
guild_data["interactions"] += 1
237269
guild_data["icon"] = guild_icon
238270
else:
@@ -243,30 +275,28 @@ def track_interactions(self, interaction: discord.Interaction):
243275
"members": interaction.guild.member_count,
244276
"interactions": 1
245277
})
246-
278+
247279
if interaction.user.guild_permissions.administrator or interaction.user.guild_permissions.manage_guild:
248280
self.stats["users_type"]["admin"] += 1
249-
elif (
250-
interaction.user.guild_permissions.manage_messages
251-
or interaction.user.guild_permissions.kick_members
252-
or interaction.user.guild_permissions.ban_members
253-
or interaction.user.guild_permissions.mute_members
254-
or interaction.user.guild_permissions.deafen_members
255-
or interaction.user.guild_permissions.move_members
256-
or interaction.user.guild_permissions.moderate_members
257-
):
281+
elif any(perm for perm in [
282+
interaction.user.guild_permissions.manage_messages,
283+
interaction.user.guild_permissions.kick_members,
284+
interaction.user.guild_permissions.ban_members,
285+
interaction.user.guild_permissions.mute_members,
286+
interaction.user.guild_permissions.deafen_members,
287+
interaction.user.guild_permissions.move_members,
288+
interaction.user.guild_permissions.moderate_members]):
258289
self.stats["users_type"]["moderator"] += 1
259-
elif interaction.user.joined_at is not None and (discord.utils.utcnow() - interaction.user.joined_at).days <= 7:
290+
elif interaction.user.joined_at and (discord.utils.utcnow() - interaction.user.joined_at).days <= 7:
260291
self.stats["users_type"]["new_member"] += 1
261292
else:
262293
self.stats["users_type"]["other"] += 1
263294

264-
# type = delete or create
265295
def trackGuilds(self, guild: discord.Guild, type: Literal["create", "delete"]):
266296
if self.debug:
267297
print(f"[DISCORDANALYTICS] trackGuilds({type}) triggered")
268298

269299
if type == "create":
270300
self.stats["addedGuilds"] += 1
271301
elif type == "delete":
272-
self.stats["removedGuilds"] += 1
302+
self.stats["removedGuilds"] += 1

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "discordanalytics"
77
description = "A Python package for interacting with Discord Analytics API"
8-
version = "3.0.4"
8+
version = "3.1.0"
99
authors = [
1010
{name = "ValDesign", email = "valdesign.dev@gmail.com"}
1111
]

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
discord.py==2.4.0
2-
requests==2.32.3
2+
aiohttp==3.10.10
33
setuptools==75.2.0
44
wheel==0.44.0

0 commit comments

Comments
 (0)