Skip to content

Commit f0b93f3

Browse files
authored
Merge pull request #8 from Gitfoe/dev
v1.0.1
2 parents 1329237 + 696bf5f commit f0b93f3

2 files changed

Lines changed: 128 additions & 44 deletions

File tree

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ One of the distinctive features of ChadCounting is the catch-up feature. In case
2222
- Counts any message that starts with a number.
2323

2424
## Usage
25-
To configure the bot for the first time, add it to your Discord server and use the `/setchannel` command to let the bot know where it should keep track of counting. After that, ChadCounting is up and running and you can start competing!
25+
To configure the bot for the first time, add it to your Discord server and use the `/set channel` command to let the bot know where it should keep track of counting. After that, ChadCounting is up and running and you can start competing!
2626

2727
## Commands
2828
The following commands could be used:
@@ -51,7 +51,11 @@ pip3 install -U pytz (confirmed with version 2023.3.post1)
5151
When creating your own fork of ChadCounting, ensure your bot has at least the `Send Message`, `Read Message History` and `Add Reactions` OAuth2 permissions. `Use External Emojis` is an optional permission, but recommended. The scope should be `bot`.
5252

5353
### Dev-mode
54-
In the `Initialisation` region of the `bot.py` file, `dev_mode` can be enabled to facilitate the testing of real-world scenarios. By enabling `dev-mode`, the bot can be configured to operate solely within a designated (testing) guild, thus preventing beta code from impacting production guilds. It is recommended to have two separate Discord bots, one for production and one for development, to safely test new code without putting the production environment at risk.
54+
In the `Initialisation` region of the `bot.py` file, two development options can be enabled to facilitate the testing of real-world scenarios.
55+
56+
The first option, `dev_active_single_guild`, can be enabled to configure the bot to operate solely within a designated (testing) guild, thus preventing beta code from impacting production guilds. The `dev_mode_guild_id` should be set to the ID of this testing guild. This allows you to safely test new code without putting the production environment at risk.
57+
58+
The second option, `dev_disable_apis`, can be enabled to disable connections to APIs such as the bot websites top.gg and discordbotlist during development. It is recommended to have two separate Discord bots, one for production and one for development, to safely test new code. This option should be enabled when using a development bot account to prevent pushing the development bot's data to the APIs.
5559

5660
[Link to add ChadCounting Dev to a guild](https://discord.com/api/oauth2/authorize?client_id=1069230219094921318&permissions=329792&scope=bot)
5761

@@ -62,12 +66,17 @@ Counting and guild data is saved to a JSON database, eliminating the need for ex
6266
For added security and to comply by Discord's ToS, create a .env file in the root directory of the ChadCounting bot folder (where bot.py is located). This file serves as the designated location to securely store bot tokens. To add your Discord bot tokens, follow the example below:
6367
```
6468
# .env
65-
DISCORD_TOKEN=your_discord_bot_token
66-
DEV_TOKEN=your_discord_dev_bot_token
69+
DISCORD_TOKEN=token_here
70+
DEV_TOKEN=token_here
71+
DISCORDBOTLIST_TOKEN=token_here
72+
TOPGG_TOKEN=token_here
73+
DISCORDS_TOKEN=token_here
74+
DISCORDBOTSGG_TOKEN=token_here
6775
```
6876

6977
### Versioning
7078
To properly manage versioning, it is recommended to update the `bot_version` variable in the `Initialization` section of the `bot.py` file every time a functional version is ready to be pulled to the `main` branch. This version will be displayed in the output of the `/help` command. ChadCounting uses semantic versioning and a version number is written as `MAJOR.MINOR.PATCH`, where:
7179
- `MAJOR` version is increased for incompatible changes to previous versions.
7280
- `MINOR` version is increased for new features that are backward-compatible.
7381
- `PATCH` version is increased for backward-compatible bug fixes.
82+
When you are working on a new version and commit to the `dev` branch, ensure the version temporarily ends with `-indev` to indicate that the version is in development and might not function yet.

bot.py

Lines changed: 115 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytz
99
import emoji
1010
import discord
11+
import requests
1112
import traceback
1213
import statistics
1314
import matplotlib.pyplot as plt
@@ -21,19 +22,30 @@
2122

2223
#region Initialisation
2324
# For developing only
24-
dev_mode = False # Make the bot only active in a certain guild
25-
dev_mode_guild_id = 574350984495628436 # Bot must be in this guild already
25+
dev_disable_apis = False # Disable connecting to APIs such as bot websites
26+
dev_active_single_guild = False # Make the bot only active in a certain guild
27+
dev_mode_guild_id = 574350984495628436 # If the above is true, bot must be in this guild already
2628
update_guild_data = False # Forces updating of newly added guild_data values after a ChadCounting update
2729

2830
# Initialize variables and load environment tables
2931
load_dotenv()
30-
TOKEN = os.getenv("DISCORD_TOKEN") # Normal ChadCounting token
32+
PROD_TOKEN = os.getenv("PROD_TOKEN") # Normal ChadCounting token
3133
DEV_TOKEN = os.getenv("DEV_TOKEN") # ChadCounting Dev bot account token
32-
guild_data = {} # DB
33-
bot_version = "1.0.0"
34-
chadcounting_color = 0xCA93FF
34+
guild_data = {} # Global variable for database
35+
bot_version = "1.0.1"
36+
chadcounting_color = 0xCA93FF # Color of the embeds
3537
image_gigachad = "https://github.com/Gitfoe/ChadCounting/blob/main/gigachad.jpeg?raw=true"
3638

39+
# Initialize API tokens and URIs
40+
DISCORDBOTLIST_TOKEN = os.getenv("DISCORDBOTLIST_TOKEN")
41+
TOPGG_TOKEN = os.getenv("TOPGG_TOKEN")
42+
DISCORDS_TOKEN = os.getenv("DISCORDS_TOKEN")
43+
DISCORDBOTSGG_TOKEN = os.getenv("DISCORDBOTSGG_TOKEN")
44+
api_discordbotslist = "https://discordbotlist.com/api/v1/bots/chadcounting"
45+
api_topgg = "https://top.gg/api/bots/1066081427935993886"
46+
api_discords = "https://discords.com/bots/api/bot/1066081427935993886"
47+
api_discordbotsgg = "https://discord.bots.gg/api/v1/bots/1066081427935993886"
48+
3749
# Initialize bot and intents
3850
intents = discord.Intents.default()
3951
intents.message_content = True
@@ -44,22 +56,26 @@
4456
@bot.event
4557
async def on_ready():
4658
"""Discord event that gets triggered once the connection has been established."""
59+
# Setup and sync commands
4760
await setup_grouped_commands(bot)
48-
try: # Sync bot commands
49-
synced = await bot.tree.sync()
61+
try:
62+
await bot.tree.sync() # Sync commands to Discord
63+
push_commands_to_discordbotlist() # Sync commands to Discordbotlist
64+
push_guilds_count_to_all_bot_websites() # Sync guilds count to bot lists
5065
except Exception as e:
5166
print(e)
67+
# Initialise database
5268
init_guild_data()
5369
for guild in bot.guilds:
54-
if not dev_mode or (dev_mode and guild.id == dev_mode_guild_id):
70+
if not dev_active_single_guild or (dev_active_single_guild and guild.id == dev_mode_guild_id):
5571
await check_for_missed_counts(guild.id)
5672
print(f"[{datetime.now()}] ChadCounting is ready.")
5773

5874
@bot.event
5975
async def on_resumed():
6076
"""Discord event that gets triggered once a bot gets resumed from a paused session."""
6177
for guild in bot.guilds:
62-
if not dev_mode or (dev_mode and guild.id == dev_mode_guild_id):
78+
if not dev_active_single_guild or (dev_active_single_guild and guild.id == dev_mode_guild_id):
6379
await check_for_missed_counts(guild.id)
6480
print(f"[{datetime.now()}] ChadCounting has resumed.")
6581

@@ -79,7 +95,7 @@ async def on_message_delete(message):
7995
guild_id = message.guild.id
8096
current_count = guild_data[guild_id]["current_count"]
8197
# Ignores messages sent by bots, and if dev_mode is on, exit if message is not from dev mode guild
82-
if message.author.bot or dev_mode and not guild_id == dev_mode_guild_id:
98+
if message.author.bot or dev_active_single_guild and not guild_id == dev_mode_guild_id:
8399
return
84100
last_count = guild_data[guild_id]["previous_message"]
85101
if message.created_at == last_count:
@@ -113,6 +129,7 @@ async def on_guild_join(guild):
113129
if bot.is_ready() is not True:
114130
return
115131
add_guild_to_guild_data(guild.id)
132+
push_guilds_count_to_all_bot_websites() # Sync guilds count with bot lists
116133
#endregion
117134

118135
#region Counting logic
@@ -168,7 +185,7 @@ async def check_count_message(message):
168185
"""Returns True if the message was a correct count and False if it was incorrect. Returns nothing if count wasn't checked."""
169186
global guild_data
170187
# Ignores messages sent by bots, and if dev_mode is on, exit if message is not from dev mode guild
171-
if message.author.bot or dev_mode and not message.guild.id == dev_mode_guild_id:
188+
if message.author.bot or dev_active_single_guild and not message.guild.id == dev_mode_guild_id:
172189
return
173190
# Checks if the message is sent in counting channel and starts with a number
174191
elif message.channel.id == guild_data[message.guild.id]["counting_channel"] and len(message.content) > 0 and message.content[0].isnumeric():
@@ -304,11 +321,11 @@ def init_guild_data():
304321
raise Exception("There was an error decoding guild_data.json.")
305322
finally:
306323
for guild in bot.guilds:
307-
if not dev_mode or (dev_mode and guild.id == dev_mode_guild_id):
324+
if not dev_active_single_guild or (dev_active_single_guild and guild.id == dev_mode_guild_id):
308325
add_or_update_new_guild_data(guild.id)
309326
if update_guild_data: # If updating is on, also check the existing guilds in guild_data (if the bot left a guild)
310327
for guild_id in guild_data:
311-
if not dev_mode or (dev_mode and guild_id == dev_mode_guild_id):
328+
if not dev_active_single_guild or (dev_active_single_guild and guild_id == dev_mode_guild_id):
312329
add_or_update_new_guild_data(guild_id)
313330
print(f"[{datetime.now()}] Successfully loaded {len(guild_data)} guild(s).")
314331

@@ -580,6 +597,12 @@ def chadcounting_embed(title, description=None):
580597
embed = discord.Embed(title=title, description=description, color=chadcounting_color)
581598
embed.set_thumbnail(url=image_gigachad)
582599
return embed
600+
601+
def check_dev_disable_apis(executing_method_name):
602+
"""Prints to the console if 'dev_disable_apis' is enabled."""
603+
if dev_disable_apis:
604+
print(f"[{datetime.now()}] {executing_method_name}: dev_disable_apis is enabled, method will exit.")
605+
return dev_disable_apis
583606
#endregion
584607

585608
#region Command helper functions
@@ -625,7 +648,7 @@ async def check_correct_channel(interaction):
625648
channel_error = f"You can only execute ChadCounting commands in the counting channel, "
626649
if counting_channel == None:
627650
channel_error += (f"however, it has not been set yet. " +
628-
f"If you are an admin of this server, use the command `/setchannel` in the channel you want to count in.")
651+
f"If you are an admin of this server, use the command `/set channel` in the channel you want to count in.")
629652
embed.add_field(name="", value=channel_error)
630653
await interaction.response.send_message(embed=embed, ephemeral=True)
631654
return False
@@ -649,6 +672,13 @@ async def check_bot_ready(interaction):
649672
await interaction.response.send_message(embed=embed, ephemeral=True)
650673
return False
651674
return True
675+
676+
def get_all_commands(bot):
677+
"""Returns a list of all the commands the bot has."""
678+
return [
679+
*bot.walk_commands(),
680+
*bot.tree.walk_commands()
681+
]
652682
#endregion
653683

654684
#region View subclasses
@@ -689,6 +719,7 @@ def __init__(self):
689719
{"label": "More information", "url": "https://github.com/Gitfoe/ChadCounting", "emoji": "ℹ️"},
690720
{"label": "Vote on top.gg", "url": "https://top.gg/bot/1066081427935993886/vote", "emoji": "⬆️"},
691721
{"label": "Vote on discordbotlist", "url": "https://discordbotlist.com/bots/chadcounting/upvote", "emoji": "⬆️"},
722+
{"label": "Vote on discords", "url": "https://discordbotlist.com/bots/chadcounting/upvote", "emoji": "⬆️"},
692723
]
693724
for button in buttons:
694725
self.add_item(Button(**button))
@@ -716,7 +747,7 @@ async def help(interaction: discord.Integration):
716747
except Exception as e:
717748
await command_exception(interaction, e)
718749

719-
class SetCog(commands.GroupCog, name="set"):
750+
class SetCog(commands.GroupCog, name="set", description="Admins only: sets and configures various settings for ChadCounting."):
720751
def __init__(self, bot: commands.Bot) -> None:
721752
self.bot = bot
722753
super().__init__()
@@ -934,7 +965,7 @@ async def set_reactions(self, interaction: discord.Integration, correct_reaction
934965
except Exception as e:
935966
await command_exception(interaction, e)
936967

937-
class CountCog(commands.GroupCog, name="count"):
968+
class CountCog(commands.GroupCog, name="count", description="Gives various counting statusses."):
938969
def __init__(self, bot: commands.Bot) -> None:
939970
self.bot = bot
940971
super().__init__()
@@ -1058,7 +1089,7 @@ async def banrate(interaction: discord.Integration):
10581089
except Exception as e:
10591090
await command_exception(interaction, e)
10601091

1061-
class StatsCog(commands.GroupCog, name="stats"):
1092+
class StatsCog(commands.GroupCog, name="stats", description="Gives various counting statistics."):
10621093
def __init__(self, bot: commands.Bot) -> None:
10631094
self.bot = bot
10641095
super().__init__()
@@ -1135,7 +1166,7 @@ async def stats_server(self, interaction: discord.Integration) -> None:
11351166
total_counts = correct_counts + incorrect_counts
11361167
if total_counts > 0:
11371168
percent_correct = round((correct_counts / (total_counts)) * 100, 2)
1138-
full_text += f"**{i+1}. {user_id}** {total_counts} total counts ({percent_correct}% correct) {correct_counts} correct and {incorrect_counts} incorrect counts\n"
1169+
full_text += f"**{i+1}. {user_id}**{total_counts} total counts ({percent_correct}% correct) {correct_counts} correct and {incorrect_counts} incorrect counts\n"
11391170
if len(full_text) > 0:
11401171
embed = chadcounting_embed("Here you go, the server statistics")
11411172
embed.add_field(name="", value=full_text)
@@ -1151,31 +1182,75 @@ async def serverstats(self, interaction: discord.Interaction) -> None:
11511182
try:
11521183
if not await check_bot_ready(interaction) or not await check_correct_channel(interaction):
11531184
return
1154-
server_stats = []
1155-
for guild_id, users in guild_data.items():
1156-
total_correct_counts = sum(user_data["correct_counts"] for user_data in users["users"].values())
1157-
total_incorrect_counts = sum(user_data["incorrect_counts"] for user_data in users["users"].values())
1158-
total_counts = total_correct_counts + total_incorrect_counts
1159-
highest_count = guild_data[guild_id]["highest_count"]
1160-
current_count = guild_data[guild_id]["current_count"]
1161-
if total_counts > 0:
1162-
percent_correct = round((total_correct_counts / total_counts) * 100, 2)
1163-
server_stats.append((guild_id, highest_count, total_counts, percent_correct, current_count))
1164-
sorted_server_stats = sorted(server_stats, key=lambda x: x[1], reverse=True)[:10]
1165-
full_text = ""
1166-
for i, (guild_id, highest_count, total_counts, percent_correct, current_count) in enumerate(sorted_server_stats):
1167-
guild = discord.utils.get(bot.guilds, id=guild_id)
1168-
name_of_guild = guild.name if guild else "*Unknown server*"
1169-
full_text += f"**{i+1}. {name_of_guild}** Highest count: {highest_count}, total: {total_counts} ({percent_correct}% correct), current: {current_count}\n"
1185+
guild_ids_bot_is_in = {guild.id for guild in bot.guilds}
1186+
server_stats = [
1187+
(
1188+
guild_id,
1189+
guild_data[guild_id]["highest_count"],
1190+
total_counts := sum(user_data["correct_counts"] + user_data["incorrect_counts"] for user_data in users["users"].values()),
1191+
round((sum(user_data["correct_counts"] for user_data in users["users"].values()) / total_counts) * 100, 2) if total_counts > 0 else 0,
1192+
guild_data[guild_id]["current_count"]
1193+
)
1194+
for guild_id, users in guild_data.items() if guild_id in guild_ids_bot_is_in
1195+
]
1196+
# Sort by high score, then percent correct, then total counts
1197+
sorted_server_stats = sorted(server_stats, key=lambda x: (x[1], x[3], x[2]), reverse=True)[:10]
1198+
full_text = "\n".join(
1199+
f"**{i+1}. {discord.utils.get(bot.guilds, id=guild_id).name}**Highest count: {highest_count}, total: {total_counts} ({percent_correct}% correct), current: {current_count}"
1200+
for i, (guild_id, highest_count, total_counts, percent_correct, current_count) in enumerate(sorted_server_stats)
1201+
)
11701202
embed = chadcounting_embed("Here you go, the best servers on ChadCounting")
1171-
if len(full_text) > 0:
1172-
embed.add_field(name="", value=full_text)
1173-
else:
1174-
embed.add_field(name="", value="No servers have participated in ChadCounting yet. Shame. Start counting!")
1203+
embed.add_field(name="", value=full_text if full_text else "No servers have participated in ChadCounting yet. Shame. Start counting!")
11751204
await interaction.response.send_message(embed=embed)
11761205
except Exception as e:
11771206
await command_exception(interaction, e)
11781207
#endregion
11791208

1180-
bot.run(TOKEN)
1209+
#region APIs
1210+
def discordbotlist_api_authorization_header():
1211+
"""Base headers for the Discordbotlist API"""
1212+
return {
1213+
"Authorization": f"Bot {DISCORDBOTLIST_TOKEN}",
1214+
"Content-Type": "application/json"
1215+
}
1216+
1217+
def generic_api_authorization_header(token):
1218+
"""Base headers for the Top.GG API."""
1219+
return {
1220+
"Authorization": token,
1221+
"Content-Type": "application/json"
1222+
}
1223+
1224+
def push_commands_to_discordbotlist():
1225+
"""Sends the bot's commands via the API."""
1226+
if check_dev_disable_apis(push_commands_to_discordbotlist.__name__): return
1227+
url = f"{api_discordbotslist}/commands"
1228+
headers = discordbotlist_api_authorization_header()
1229+
# Convert list of commands to json-serializable and API-understandable format
1230+
commands_list = []
1231+
for command in get_all_commands(bot):
1232+
commands_list.append({"name": command.qualified_name, "description": command.description})
1233+
response = requests.post(url, headers=headers, json=commands_list)
1234+
print(f"[{datetime.now()}] {push_commands_to_discordbotlist.__name__}: API response {response.status_code}.")
1235+
1236+
def push_guilds_count_to_all_bot_websites():
1237+
"""Pushes the current guild count to all bot websites configured."""
1238+
if check_dev_disable_apis(push_guilds_count_to_all_bot_websites.__name__): return
1239+
discordbotlist_headers = discordbotlist_api_authorization_header()
1240+
topgg_headers = generic_api_authorization_header(TOPGG_TOKEN)
1241+
discords_headers = generic_api_authorization_header(DISCORDS_TOKEN)
1242+
discordbotsgg_headers = generic_api_authorization_header(DISCORDBOTSGG_TOKEN)
1243+
push_guilds_count_to_bot_website(f"{api_discordbotslist}/stats", "guilds", discordbotlist_headers)
1244+
push_guilds_count_to_bot_website(f"{api_topgg}/stats", "server_count", topgg_headers)
1245+
push_guilds_count_to_bot_website(api_discords, "server_count", discords_headers)
1246+
push_guilds_count_to_bot_website(f"{api_discordbotsgg}/stats", "guildCount", discordbotsgg_headers)
1247+
1248+
def push_guilds_count_to_bot_website(url, payload_string, headers):
1249+
"""Sends the number of guilds via the API."""
1250+
payload = {payload_string: len(bot.guilds)}
1251+
response = requests.post(url, headers=headers, json=payload)
1252+
print(f"[{datetime.now()}] {push_guilds_count_to_bot_website.__name__}: {url} API response {response.status_code}.")
1253+
#endregion
1254+
1255+
bot.run(PROD_TOKEN)
11811256
# Coded by https://github.com/Gitfoe

0 commit comments

Comments
 (0)