CustomModules 1.0.1__tar.gz → 2.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {custommodules-1.0.1 → custommodules-2.0.3}/CustomModules/__init__.py +19 -19
  2. custommodules-2.0.3/CustomModules/app_translation.py +80 -0
  3. custommodules-2.0.3/CustomModules/bitmap_handler.py +298 -0
  4. custommodules-2.0.3/CustomModules/bot_directory.py +169 -0
  5. custommodules-2.0.3/CustomModules/database_handler.py +1194 -0
  6. custommodules-2.0.3/CustomModules/googletrans.py +176 -0
  7. custommodules-2.0.3/CustomModules/invite_tracker.py +202 -0
  8. custommodules-2.0.3/CustomModules/killswitch.py +125 -0
  9. custommodules-2.0.3/CustomModules/libretrans.py +320 -0
  10. custommodules-2.0.3/CustomModules/log_handler.py +175 -0
  11. custommodules-2.0.3/CustomModules/patchnotes.py +140 -0
  12. custommodules-2.0.3/CustomModules/private_voice.py +1429 -0
  13. custommodules-2.0.3/CustomModules/py.typed +0 -0
  14. custommodules-2.0.3/CustomModules/random_usernames.py +848 -0
  15. custommodules-2.0.3/CustomModules/stat_dock.py +965 -0
  16. custommodules-2.0.3/CustomModules/steam.py +300 -0
  17. custommodules-2.0.3/CustomModules/steam_charts.py +119 -0
  18. custommodules-2.0.3/CustomModules/twitch.py +289 -0
  19. custommodules-2.0.3/CustomModules.egg-info/PKG-INFO +198 -0
  20. {custommodules-1.0.1 → custommodules-2.0.3}/CustomModules.egg-info/SOURCES.txt +2 -16
  21. custommodules-2.0.3/CustomModules.egg-info/requires.txt +73 -0
  22. custommodules-2.0.3/PKG-INFO +198 -0
  23. {custommodules-1.0.1 → custommodules-2.0.3}/README.md +36 -5
  24. {custommodules-1.0.1 → custommodules-2.0.3}/pyproject.toml +1 -1
  25. {custommodules-1.0.1 → custommodules-2.0.3}/setup.cfg +0 -19
  26. custommodules-2.0.3/setup.py +121 -0
  27. custommodules-1.0.1/AppTranslation/requirements.txt +0 -8
  28. custommodules-1.0.1/BotDirectory/requirements.txt +0 -7
  29. custommodules-1.0.1/CustomModules/app_translation.py +0 -10
  30. custommodules-1.0.1/CustomModules/bitmap_handler.py +0 -10
  31. custommodules-1.0.1/CustomModules/bot_directory.py +0 -10
  32. custommodules-1.0.1/CustomModules/database_handler.py +0 -10
  33. custommodules-1.0.1/CustomModules/googletrans.py +0 -10
  34. custommodules-1.0.1/CustomModules/invite_tracker.py +0 -10
  35. custommodules-1.0.1/CustomModules/killswitch.py +0 -10
  36. custommodules-1.0.1/CustomModules/libretrans.py +0 -10
  37. custommodules-1.0.1/CustomModules/log_handler.py +0 -10
  38. custommodules-1.0.1/CustomModules/patchnotes.py +0 -10
  39. custommodules-1.0.1/CustomModules/private_voice.py +0 -10
  40. custommodules-1.0.1/CustomModules/random_usernames.py +0 -10
  41. custommodules-1.0.1/CustomModules/stat_dock.py +0 -10
  42. custommodules-1.0.1/CustomModules/steam.py +0 -10
  43. custommodules-1.0.1/CustomModules/steam_charts.py +0 -10
  44. custommodules-1.0.1/CustomModules/twitch.py +0 -10
  45. custommodules-1.0.1/CustomModules.egg-info/PKG-INFO +0 -252
  46. custommodules-1.0.1/CustomModules.egg-info/requires.txt +0 -158
  47. custommodules-1.0.1/DatabaseHandler/requirements.txt +0 -30
  48. custommodules-1.0.1/Googletrans/requirements.txt +0 -18
  49. custommodules-1.0.1/InviteTracker/requirements.txt +0 -8
  50. custommodules-1.0.1/Killswitch/requirements.txt +0 -10
  51. custommodules-1.0.1/Libretrans/requirements.txt +0 -1
  52. custommodules-1.0.1/LogHandler/requirements.txt +0 -1
  53. custommodules-1.0.1/PKG-INFO +0 -252
  54. custommodules-1.0.1/Patchnotes/requirements.txt +0 -3
  55. custommodules-1.0.1/PrivateVoice/requirements.txt +0 -1
  56. custommodules-1.0.1/RandomUsernames/requirements.txt +0 -1
  57. custommodules-1.0.1/StatDock/requirements.txt +0 -2
  58. custommodules-1.0.1/Steam/requirements.txt +0 -7
  59. custommodules-1.0.1/SteamCharts/requirements.txt +0 -10
  60. custommodules-1.0.1/Twitch/requirements.txt +0 -2
  61. custommodules-1.0.1/setup.py +0 -83
  62. {custommodules-1.0.1 → custommodules-2.0.3}/CustomModules.egg-info/dependency_links.txt +0 -0
  63. {custommodules-1.0.1 → custommodules-2.0.3}/CustomModules.egg-info/not-zip-safe +0 -0
  64. {custommodules-1.0.1 → custommodules-2.0.3}/CustomModules.egg-info/top_level.txt +0 -0
  65. {custommodules-1.0.1 → custommodules-2.0.3}/LICENSE.txt +0 -0
  66. {custommodules-1.0.1 → custommodules-2.0.3}/MANIFEST.in +0 -0
@@ -20,9 +20,9 @@ Available modules:
20
20
  - twitch: Twitch API integration
21
21
  """
22
22
 
23
- __version__ = '1.0.1'
24
- __author__ = 'Serpensin'
25
- __license__ = 'AGPL-3.0'
23
+ __version__ = "2.0.3"
24
+ __author__ = "Serpensin"
25
+ __license__ = "AGPL-3.0"
26
26
 
27
27
  # Conditional imports based on available dependencies
28
28
  try:
@@ -106,20 +106,20 @@ except ImportError:
106
106
  pass
107
107
 
108
108
  __all__ = [
109
- 'app_translation',
110
- 'bitmap_handler',
111
- 'bot_directory',
112
- 'database_handler',
113
- 'googletrans',
114
- 'invite_tracker',
115
- 'killswitch',
116
- 'libretrans',
117
- 'log_handler',
118
- 'patchnotes',
119
- 'private_voice',
120
- 'random_usernames',
121
- 'stat_dock',
122
- 'steam',
123
- 'steam_charts',
124
- 'twitch',
109
+ "app_translation",
110
+ "bitmap_handler",
111
+ "bot_directory",
112
+ "database_handler",
113
+ "googletrans",
114
+ "invite_tracker",
115
+ "killswitch",
116
+ "libretrans",
117
+ "log_handler",
118
+ "patchnotes",
119
+ "private_voice",
120
+ "random_usernames",
121
+ "stat_dock",
122
+ "steam",
123
+ "steam_charts",
124
+ "twitch",
125
125
  ]
@@ -0,0 +1,80 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import discord
5
+
6
+
7
+ class Translator(discord.app_commands.Translator):
8
+ def __init__(self, logger: Optional[logging.Logger] = None):
9
+ """
10
+ Initializes the Translator class with predefined translations for German and Japanese locales.
11
+
12
+ Args:
13
+ logger (Optional[logging.Logger]): Parent logger. If provided, creates a child logger
14
+ under CustomModules.AppTranslation. Defaults to None.
15
+ """
16
+ # Setup logger with child hierarchy: parent -> CustomModules -> AppTranslation
17
+ if logger:
18
+ self.logger = logger.getChild("CustomModules").getChild("AppTranslation")
19
+ else:
20
+ self.logger = logging.getLogger("CustomModules.AppTranslation")
21
+
22
+ self.logger.debug("Initializing AppTranslation Translator")
23
+
24
+ self.translations = {
25
+ discord.Locale.german: {
26
+ "Test, if the bot is responding.": "Teste, ob der Bot antwortet.",
27
+ "Get information about the bot.": "Erhalte Informationen über den Bot.",
28
+ "change_nickname": "nickname_ändern",
29
+ },
30
+ discord.Locale.japanese: {
31
+ "ping": "ピング",
32
+ "Test, if the bot is responding.": "ボットが応答しているかテストします。",
33
+ "botinfo": "ボット情報",
34
+ "Get information about the bot.": "ボットに関する情報を取得します。",
35
+ "change_nickname": "ニックネームを変更する",
36
+ },
37
+ }
38
+
39
+ total_translations = sum(len(trans) for trans in self.translations.values())
40
+ self.logger.info(
41
+ f"Translator initialized with {len(self.translations)} locales and {total_translations} total translations"
42
+ )
43
+
44
+ async def load(self) -> None:
45
+ """
46
+ Placeholder method for loading translations.
47
+ Currently does nothing.
48
+ """
49
+ self.logger.debug("Load method called (currently no-op)")
50
+
51
+ async def translate(
52
+ self,
53
+ string: discord.app_commands.locale_str,
54
+ locale: discord.Locale,
55
+ context: discord.app_commands.TranslationContext,
56
+ ) -> Optional[str]:
57
+ """
58
+ Translates a given string to the specified locale.
59
+
60
+ Parameters:
61
+ string (discord.app_commands.locale_str): The string that is requesting to be translated.
62
+ locale (discord.Locale): The target language to translate to.
63
+ context (discord.app_commands.TranslationContext): The origin of this string, e.g., TranslationContext.command_name, etc.
64
+
65
+ Returns:
66
+ Optional[str]: The translated string if available, otherwise the original string.
67
+ """
68
+ original = string.message
69
+ translated = self.translations.get(locale, {}).get(original, original)
70
+
71
+ if translated != original:
72
+ self.logger.debug(
73
+ f"Translated '{original}' to '{translated}' for locale {locale.value}"
74
+ )
75
+ else:
76
+ self.logger.debug(
77
+ f"No translation found for '{original}' in locale {locale.value}, using original"
78
+ )
79
+
80
+ return translated
@@ -0,0 +1,298 @@
1
+ import logging
2
+ from typing import Dict, List, Optional, Union
3
+
4
+
5
+ class BitmapHandler:
6
+ def __init__(self, keys: List[str], logger: Optional[logging.Logger] = None):
7
+ """
8
+ Initialize the BitmapHandler with a list of keys.
9
+
10
+ Args:
11
+ keys (List[str]): A list of keys to initialize the bitmap.
12
+ A maximum of 64 keys can be provided.
13
+ logger (Optional[logging.Logger]): Parent logger. If provided, creates a child logger
14
+ under CustomModules.BitmapHandler. Defaults to None.
15
+
16
+ Raises:
17
+ ValueError: If more than 64 keys are provided.
18
+ """
19
+ # Setup logger with child hierarchy: parent -> CustomModules -> BitmapHandler
20
+ if logger:
21
+ self.logger = logger.getChild("CustomModules").getChild("BitmapHandler")
22
+ else:
23
+ self.logger = logging.getLogger("CustomModules.BitmapHandler")
24
+
25
+ self.logger.debug(f"Initializing BitmapHandler with {len(keys)} keys")
26
+
27
+ if len(keys) > 64:
28
+ self.logger.error(f"Too many keys provided: {len(keys)} > 64")
29
+ raise ValueError(
30
+ "Warning: You are trying to initialize with more than 64 keys. This may exceed the limit for bit manipulation."
31
+ )
32
+
33
+ # Generate the bitmap dictionary dynamically
34
+ self.bitmap: Dict[str, int] = {key: index for index, key in enumerate(keys)}
35
+ self.key_list: List[str] = keys.copy() # Store keys in a list for ordering
36
+
37
+ self.logger.info(
38
+ f"BitmapHandler initialized successfully with keys: {', '.join(keys)}"
39
+ )
40
+
41
+ def get_bitkey(self, *args: str) -> int:
42
+ """
43
+ Converts a list of keys into a single bitkey.
44
+
45
+ Args:
46
+ *args (str): A variable number of keys to be converted to a bitkey.
47
+
48
+ Returns:
49
+ int: The bitkey representing the provided keys.
50
+
51
+ Raises:
52
+ KeyError: If any key is invalid (not in the bitmap).
53
+ """
54
+ self.logger.debug(f"Converting keys to bitkey: {args}")
55
+ bitkey = 0
56
+ for key in args:
57
+ if key in self.bitmap:
58
+ bitkey |= 1 << self.bitmap[key]
59
+ self.logger.debug(
60
+ f"Added key '{key}' at position {self.bitmap[key]} to bitkey"
61
+ )
62
+ elif key:
63
+ self.logger.error(f"Invalid key provided: {key}")
64
+ raise KeyError(f"Invalid key: {key}")
65
+
66
+ self.logger.debug(f"Generated bitkey: {bitkey} (binary: {bin(bitkey)})")
67
+ return bitkey
68
+
69
+ def check_key_in_bitkey(self, key: str, bitkey: int) -> bool:
70
+ """
71
+ Checks if a given key is present in the bitkey.
72
+
73
+ Args:
74
+ key (str): The key to check.
75
+ bitkey (int): The bitkey in which to check for the key.
76
+
77
+ Returns:
78
+ bool: True if the key is present in the bitkey, False otherwise.
79
+ """
80
+ result = key in self.bitmap and bool(bitkey & (1 << self.bitmap[key]))
81
+ self.logger.debug(f"Checking if key '{key}' is in bitkey {bitkey}: {result}")
82
+ return result
83
+
84
+ def get_active_keys(
85
+ self, bitkey: int, single: bool = False
86
+ ) -> Union[str, List[str]]:
87
+ """
88
+ Retrieves the active keys from a bitkey.
89
+
90
+ Args:
91
+ bitkey (int): The bitkey from which to retrieve active keys.
92
+ single (bool, optional): If True, returns the last active key as a string.
93
+ If False, returns a list of active keys. Defaults to False.
94
+
95
+ Returns:
96
+ Union[str, List[str]]: The active keys in the bitkey.
97
+
98
+ Raises:
99
+ ValueError: If the bitkey is invalid (not within the valid range).
100
+ """
101
+ self.logger.debug(
102
+ f"Retrieving active keys from bitkey {bitkey} (single={single})"
103
+ )
104
+
105
+ max_bitkey = (1 << len(self.bitmap)) - 1
106
+ if bitkey < 0 or bitkey > max_bitkey:
107
+ self.logger.error(
108
+ f"Invalid bitkey: {bitkey}. Must be between 0 and {max_bitkey}"
109
+ )
110
+ raise ValueError(
111
+ f"Invalid bitkey: {bitkey}. It must be between 0 and {max_bitkey}."
112
+ )
113
+
114
+ active_keys = [
115
+ key
116
+ for key, bit_position in self.bitmap.items()
117
+ if bitkey & (1 << bit_position)
118
+ ]
119
+
120
+ self.logger.debug(f"Found {len(active_keys)} active keys: {active_keys}")
121
+
122
+ if single:
123
+ result = active_keys[-1] if active_keys else ""
124
+ self.logger.debug(f"Returning single active key: '{result}'")
125
+ return result
126
+ return active_keys
127
+
128
+ def toggle_key_in_bitkey(self, key: str, bitkey: int, add: bool = True) -> int:
129
+ """
130
+ Adds or removes a given key from an existing bitkey based on the 'add' parameter.
131
+
132
+ Args:
133
+ key (str): The key to add or remove.
134
+ bitkey (int): The existing bitkey.
135
+ add (bool, optional): If True, the key is added to the bitkey;
136
+ if False, the key is removed. Defaults to True.
137
+
138
+ Returns:
139
+ int: The updated bitkey after the operation.
140
+
141
+ Raises:
142
+ KeyError: If the key is invalid (not in the bitmap).
143
+ """
144
+ self.logger.debug(f"Toggling key '{key}' in bitkey {bitkey} (add={add})")
145
+
146
+ if key not in self.bitmap:
147
+ self.logger.error(f"Invalid key: {key}")
148
+ raise KeyError(f"Invalid key: {key}")
149
+
150
+ result = (
151
+ bitkey | (1 << self.bitmap[key])
152
+ if add
153
+ else bitkey & ~(1 << self.bitmap[key])
154
+ )
155
+
156
+ self.logger.debug(
157
+ f"Key '{key}' {'added to' if add else 'removed from'} bitkey. Result: {result}"
158
+ )
159
+ return result
160
+
161
+ def invert_bitkey(self, bitkey: int) -> int:
162
+ """
163
+ Inverts all bits in the given bitkey.
164
+
165
+ Args:
166
+ bitkey (int): The bitkey to be inverted.
167
+
168
+ Returns:
169
+ int: The inverted bitkey.
170
+ """
171
+ max_bitkey = (1 << len(self.bitmap)) - 1
172
+ result = ~bitkey & max_bitkey
173
+ self.logger.debug(f"Inverted bitkey {bitkey} to {result}")
174
+ return result
175
+
176
+ def count_active_bits(self, bitkey: int) -> int:
177
+ """
178
+ Counts the number of active (set) bits in the bitkey.
179
+
180
+ Args:
181
+ bitkey (int): The bitkey for which to count active bits.
182
+
183
+ Returns:
184
+ int: The count of active bits in the bitkey.
185
+ """
186
+ count = bin(bitkey).count("1")
187
+ self.logger.debug(f"Counted {count} active bits in bitkey {bitkey}")
188
+ return count
189
+
190
+ def compare_bitkeys(self, bitkey1: int, bitkey2: int) -> Dict[str, List[str]]:
191
+ """
192
+ Compares two bitkeys and returns a dictionary of differences.
193
+
194
+ Args:
195
+ bitkey1 (int): The first bitkey to compare.
196
+ bitkey2 (int): The second bitkey to compare.
197
+
198
+ Returns:
199
+ Dict[str, List[str]]: A dictionary containing the common keys,
200
+ keys only in the first bitkey, and keys only in the second bitkey.
201
+ """
202
+ self.logger.debug(f"Comparing bitkeys: {bitkey1} and {bitkey2}")
203
+
204
+ common = bitkey1 & bitkey2
205
+ only_in_1 = bitkey1 & ~bitkey2
206
+ only_in_2 = bitkey2 & ~bitkey1
207
+
208
+ # get_active_keys with single=False always returns List[str]
209
+ common_keys = self.get_active_keys(common, single=False)
210
+ only_in_1_keys = self.get_active_keys(only_in_1, single=False)
211
+ only_in_2_keys = self.get_active_keys(only_in_2, single=False)
212
+
213
+ result = {
214
+ "common_keys": (
215
+ common_keys if isinstance(common_keys, list) else [common_keys]
216
+ ),
217
+ "only_in_bitkey1": (
218
+ only_in_1_keys if isinstance(only_in_1_keys, list) else [only_in_1_keys]
219
+ ),
220
+ "only_in_bitkey2": (
221
+ only_in_2_keys if isinstance(only_in_2_keys, list) else [only_in_2_keys]
222
+ ),
223
+ }
224
+
225
+ self.logger.debug(
226
+ f"Comparison result: {len(result['common_keys'])} common, "
227
+ f"{len(result['only_in_bitkey1'])} only in first, "
228
+ f"{len(result['only_in_bitkey2'])} only in second"
229
+ )
230
+
231
+ return result
232
+
233
+ def add_key(self, key: str):
234
+ """
235
+ Adds a new key to the bitmap.
236
+
237
+ Args:
238
+ key (str): The key to add.
239
+
240
+ Raises:
241
+ KeyError: If the key already exists in the bitmap.
242
+ """
243
+ self.logger.debug(f"Attempting to add key: {key}")
244
+
245
+ if key in self.bitmap:
246
+ self.logger.error(f"Key '{key}' already exists in bitmap")
247
+ raise KeyError(f"Key '{key}' already exists.")
248
+
249
+ # Add the new key and update the bitmap and key_list
250
+ new_index = len(self.bitmap)
251
+ self.bitmap[key] = new_index # Assign the next available index
252
+ self.key_list.append(key) # Append the new key to the list
253
+
254
+ self.logger.info(
255
+ f"Added key '{key}' at position {new_index}. Total keys: {len(self.bitmap)}"
256
+ )
257
+
258
+ def remove_key(self, key: str):
259
+ """
260
+ Removes a key from the bitmap.
261
+
262
+ Args:
263
+ key (str): The key to remove.
264
+
265
+ Raises:
266
+ KeyError: If the key does not exist in the bitmap.
267
+ """
268
+ self.logger.debug(f"Attempting to remove key: {key}")
269
+
270
+ if key not in self.bitmap:
271
+ self.logger.error(f"Key '{key}' does not exist in bitmap")
272
+ raise KeyError(f"Key '{key}' does not exist.")
273
+
274
+ # Remove the key and reassign indices for the remaining keys
275
+ index_to_remove = self.bitmap[key]
276
+ del self.bitmap[key]
277
+
278
+ # Reassign keys and their bit positions only if the key is not the last one
279
+ if len(self.bitmap) > 0:
280
+ for k in self.bitmap:
281
+ if self.bitmap[k] > index_to_remove:
282
+ self.bitmap[k] -= 1
283
+
284
+ self.key_list.remove(key) # Remove from key list
285
+
286
+ self.logger.info(
287
+ f"Removed key '{key}' from position {index_to_remove}. Remaining keys: {len(self.bitmap)}"
288
+ )
289
+
290
+ def get_keys(self) -> Dict[str, int]:
291
+ """
292
+ Returns the current bitmap dictionary.
293
+
294
+ Returns:
295
+ Dict[str, int]: The current bitmap dictionary mapping keys to their bit positions.
296
+ """
297
+ self.logger.debug(f"Retrieving bitmap with {len(self.bitmap)} keys")
298
+ return self.bitmap
@@ -0,0 +1,169 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Optional
4
+
5
+ import aiohttp
6
+
7
+
8
+ class Stats:
9
+ # Constants for repeated string literals
10
+ CONTENT_TYPE_JSON = "application/json"
11
+ CONTENT_TYPE_JSON_UTF8 = "application/json; charset=utf-8"
12
+
13
+ def __init__(
14
+ self,
15
+ bot,
16
+ logger: Optional[logging.Logger] = None,
17
+ topgg_token: str = "",
18
+ discordbots_token: str = "",
19
+ discordbotlistcom_token: str = "",
20
+ discordlist_token: str = "",
21
+ ):
22
+ # Setup logger with child hierarchy: parent -> CustomModules -> BotDirectory
23
+ if logger:
24
+ self.logger = logger.getChild("CustomModules").getChild("BotDirectory")
25
+ else:
26
+ self.logger = logging.getLogger("CustomModules.BotDirectory")
27
+
28
+ self.logger.debug("Initializing Stats module")
29
+
30
+ self.bot = bot
31
+ self.topgg_token = topgg_token
32
+ self.discordbots_token = discordbots_token
33
+ self.discordbotlistcom_token = discordbotlistcom_token
34
+ self.discordlist_token = discordlist_token
35
+
36
+ self._tasks = []
37
+
38
+ active_tokens = sum(
39
+ [
40
+ bool(topgg_token),
41
+ bool(discordbots_token),
42
+ bool(discordbotlistcom_token),
43
+ bool(discordlist_token),
44
+ ]
45
+ )
46
+ self.logger.info(
47
+ f"Stats initialized with {active_tokens} active bot directory tokens"
48
+ )
49
+
50
+ async def _post_stats(self, url, headers, json_data):
51
+ """Post statistics to a given URL with error logging."""
52
+ self.logger.debug(f"Posting stats to {url}")
53
+ try:
54
+ async with aiohttp.ClientSession() as session:
55
+ async with session.post(url, headers=headers, json=json_data) as resp:
56
+ if resp.status != 200:
57
+ text = await resp.text()
58
+ self.logger.error(
59
+ f"Failed to update {url}: {resp.status} {text}"
60
+ )
61
+ else:
62
+ self.logger.debug(f"Successfully posted stats to {url}")
63
+ except Exception as e:
64
+ self.logger.error(f"Exception while posting stats to {url}: {e}")
65
+
66
+ async def _loop_post(self, url, headers, json_func, interval=60 * 30):
67
+ """Generic loop for posting stats periodically."""
68
+ self.logger.debug(
69
+ f"Starting stats update loop for {url} (interval: {interval}s)"
70
+ )
71
+ while True:
72
+ try:
73
+ json_data = json_func()
74
+ await self._post_stats(url, headers, json_data)
75
+ await asyncio.sleep(interval)
76
+ except asyncio.CancelledError:
77
+ self.logger.debug(f"Stats loop for {url} cancelled")
78
+ raise # Re-raise to properly handle cancellation
79
+ except Exception as e:
80
+ self.logger.error(f"Exception in stats loop for {url}: {e}")
81
+ await asyncio.sleep(interval)
82
+
83
+ def _topgg_data(self):
84
+ return {
85
+ "server_count": len(self.bot.guilds),
86
+ "shard_count": len(self.bot.shards),
87
+ }
88
+
89
+ def _discordbots_data(self):
90
+ return {"guildCount": len(self.bot.guilds), "shardCount": len(self.bot.shards)}
91
+
92
+ def _discordbotlist_com_data(self):
93
+ return {
94
+ "guilds": len(self.bot.guilds),
95
+ "users": sum(guild.member_count for guild in self.bot.guilds),
96
+ }
97
+
98
+ def _discordlist_data(self):
99
+ return {"count": len(self.bot.guilds)}
100
+
101
+ def start_stats_update(self):
102
+ """Start all stats update tasks in parallel."""
103
+ self.logger.info("Starting stats update tasks")
104
+
105
+ if self.topgg_token:
106
+ self.logger.debug("Configuring top.gg stats updates")
107
+ url = f"https://top.gg/api/bots/{self.bot.user.id}/stats"
108
+ headers = {
109
+ "Authorization": self.topgg_token,
110
+ "Content-Type": self.CONTENT_TYPE_JSON,
111
+ }
112
+ self._tasks.append(
113
+ asyncio.create_task(self._loop_post(url, headers, self._topgg_data))
114
+ )
115
+
116
+ if self.discordbots_token:
117
+ self.logger.debug("Configuring discord.bots.gg stats updates")
118
+ url = f"https://discord.bots.gg/api/v1/bots/{self.bot.user.id}/stats"
119
+ headers = {
120
+ "Authorization": self.discordbots_token,
121
+ "Content-Type": self.CONTENT_TYPE_JSON,
122
+ }
123
+ self._tasks.append(
124
+ asyncio.create_task(
125
+ self._loop_post(url, headers, self._discordbots_data)
126
+ )
127
+ )
128
+
129
+ if self.discordbotlistcom_token:
130
+ self.logger.debug("Configuring discordbotlist.com stats updates")
131
+ url = f"https://discordbotlist.com/api/v1/bots/{self.bot.user.id}/stats"
132
+ headers = {
133
+ "Authorization": self.discordbotlistcom_token,
134
+ "Content-Type": self.CONTENT_TYPE_JSON,
135
+ }
136
+ self._tasks.append(
137
+ asyncio.create_task(
138
+ self._loop_post(url, headers, self._discordbotlist_com_data)
139
+ )
140
+ )
141
+
142
+ if self.discordlist_token:
143
+ self.logger.debug("Configuring discordlist.gg stats updates")
144
+ url = f"https://api.discordlist.gg/v0/bots/{self.bot.user.id}/guilds"
145
+ headers = {
146
+ "Authorization": f"Bearer {self.discordlist_token}",
147
+ "Content-Type": self.CONTENT_TYPE_JSON_UTF8,
148
+ }
149
+ self._tasks.append(
150
+ asyncio.create_task(
151
+ self._loop_post(url, headers, self._discordlist_data)
152
+ )
153
+ )
154
+
155
+ self.logger.info(f"Started {len(self._tasks)} stats update tasks")
156
+ return self._tasks
157
+
158
+ async def stop_stats_update(self):
159
+ """Cancel all running stats update tasks."""
160
+ self.logger.info(f"Stopping {len(self._tasks)} stats update tasks")
161
+ for task in self._tasks:
162
+ task.cancel()
163
+ await asyncio.gather(*self._tasks, return_exceptions=True)
164
+ self._tasks.clear()
165
+ self.logger.debug("All stats update tasks stopped")
166
+
167
+
168
+ if __name__ == "__main__":
169
+ print("This is a module. Do not run it directly.")