Compare commits

...
Author SHA1 Message Date
alwayscompiling 04051eeacc Merge branch '14-score-entries-as-dictsjson-objects' into develop 2022-07-29 16:49:50 -04:00
alwayscompiling f33dc8ce37 Skeleton needs to have dict for category scores. 2022-07-29 16:48:25 -04:00
alwayscompiling 306f983c8e Had to change access to scores. Somewhat weird due to them being dicts and dicts being difficult to access by index. 2022-07-29 16:35:45 -04:00
alwayscompiling 9e9d616814 Change tuple to list. Its stored as a list anyways. 2022-07-29 16:16:16 -04:00
alwayscompiling 5a09beea31 Convert add_score to have scores as a dict. 2022-07-29 15:56:37 -04:00
alwayscompiling 8c2b3fa487 Server count on all presense strings. 2022-07-29 15:10:41 -04:00
alwayscompiling 62e89960f7 Changed presence update functionability to a cog. Randomly chooses a string and updates the presence every 2 minutes. 2022-07-29 15:08:42 -04:00
alwayscompiling e2b0c0ea89 Enforcing python version because ordered dicts in 3.7 is required. 2022-07-29 14:27:10 -04:00
alwayscompiling 3eed30567d Fixed single digit second submissions. Covered bases to ensure time format is consistant. 2022-07-29 14:12:58 -04:00
alwayscompiling 565babd1d3 Correct submission listener usage of HighscoresData. 2022-07-28 11:14:38 -04:00
alwayscompiling 455b51a998 Remove outdated import. 2022-07-28 11:02:47 -04:00
alwayscompiling f136181216 Message map has guild id as an int. Update submission view to use HighscoresData. 2022-07-28 11:01:40 -04:00
alwayscompiling 463ef48052 Move message map creation to highscores.py. Was a circular import. 2022-07-28 10:41:10 -04:00
alwayscompiling 64004329da Add HighscoresData to register command. Add channel setting functions. 2022-07-28 10:25:56 -04:00
alwayscompiling f497928054 Get table names from data. Update post scores to use HighscoresData class. 2022-07-28 10:17:46 -04:00
alwayscompiling 0911ebfa45 Updated guild listener to HighscoresData class. 2022-07-27 22:32:28 -04:00
alwayscompiling beedf946c9 Update delete scores to new HighscoresData class. 2022-07-27 22:27:27 -04:00
alwayscompiling 5b31314f02 Fill out HighscoreData class with access functions for utils.py. 2022-07-27 22:15:06 -04:00
alwayscompiling 4b15fef30c Set highscore_data type. 2022-07-27 21:59:57 -04:00
alwayscompiling e11a86af7b Update highscores.py to use HighscoreData class. 2022-07-27 20:48:04 -04:00
alwayscompiling f4f8f426d0 Move create_message_map to here. 2022-07-27 20:44:35 -04:00
alwayscompiling b8eb84165e Create new class for storing data. 2022-07-27 20:44:07 -04:00
alwayscompiling 273ff757af Correct delete scores description. 2022-07-26 22:44:32 -04:00
alwayscompiling f84e2eef29 Preventing user from having multiple submissions. 2022-07-26 22:17:24 -04:00
alwayscompiling b4eb5ab6d1 Removed deleting of scores pushed off table. 2022-07-26 22:12:12 -04:00
alwayscompiling 1ae6a4a223 Ensure formatting lines up times and right justifies int scores. 2022-07-26 22:09:35 -04:00
alwayscompiling e1a429c42a Trim leading 0s from time scores. 2022-07-26 21:48:31 -04:00
alwayscompiling 790e8ca50f Create delete-score command. 2022-07-26 21:35:56 -04:00
alwayscompiling 7ba43ea4f4 Creating message map entry if guild id isn't there already. 2022-07-26 21:22:12 -04:00
alwayscompiling 39c858af64 Correct regex to ensure start and end of string. 2022-07-23 15:23:16 -04:00
alwayscompiling 093861b2e6 Improve readability with guild id variable. 2022-07-23 14:26:07 -04:00
alwayscompiling 4ef487a8c4 Saving deleted data from left servers. 2022-07-23 14:12:32 -04:00
alwayscompiling 849ebb966a Guild id is a string in message map. 2022-07-23 14:02:47 -04:00
alwayscompiling f1b0e34194 message_map doesn't need guild id as a string. 2022-07-23 00:06:56 -04:00
alwayscompiling 7cdc7d2cef Need empty dict before adding to it. 2022-07-23 00:00:18 -04:00
alwayscompiling db4672dcda Correct spacing. 2022-07-22 23:59:46 -04:00
alwayscompiling fc2b601687 Somehow removed await on fetch message. 2022-07-22 23:44:33 -04:00
alwayscompiling 493d44fa19 Pass guild_data when needed. 2022-07-22 23:41:00 -04:00
alwayscompiling 5d3a1568a2 Guild id is a string in highscores data json. 2022-07-22 23:39:51 -04:00
alwayscompiling be29194a57 Get guild data everywhere highscores_data was used. 2022-07-22 23:33:11 -04:00
alwayscompiling d067da2300 Changed config table name. 2022-07-22 23:02:24 -04:00
alwayscompiling 6d0c10e1cc Create empty dict for each guild during message_map creation. 2022-07-22 22:41:35 -04:00
alwayscompiling 609bfa1277 Update message map and send highscore message for multiple guilds. 2022-07-22 22:36:47 -04:00
alwayscompiling 158e53e799 Add on_guild_remove listener. 2022-07-22 22:09:09 -04:00
alwayscompiling 3cc67a4e9e Corrected skeleton provider. Add cog for guild listener. 2022-07-22 22:05:21 -04:00
alwayscompiling df80eb2952 Move data skeleton creation out of opening highscores data. 2022-07-22 21:52:13 -04:00
alwayscompiling b78eb7b9bf Changed order of functions. 2022-07-22 21:41:45 -04:00
alwayscompiling 65ce143cfc Merge branch '18-remove-boss-message-map' into develop 2022-07-22 21:37:29 -04:00
alwayscompiling 8dbb2a6895 Missed adding tables one place. 2022-07-22 21:34:23 -04:00
alwayscompiling a24fe52182 Message id is no longer a string in the message map. 2022-07-22 21:32:18 -04:00
alwayscompiling d0131b1044 Updated all table data accesses to have table as part of access. 2022-07-22 21:30:46 -04:00
alwayscompiling 307ef86493 Removed old usage of message map from file. 2022-07-22 21:22:17 -04:00
alwayscompiling a1a9fae9fa Generating message map on bot start. 2022-07-22 21:18:01 -04:00
alwayscompiling 56984b3df0 Remove unnecessary intent activation. 2022-07-22 20:59:25 -04:00
alwayscompiling 7a261d56fc Move formatting to when score is retrieved. 2022-07-21 10:31:07 -04:00
alwayscompiling 4ee2280565 Correcting example runescape config. 2022-07-21 10:04:52 -04:00
alwayscompiling a24900a957 Merge branch '15-format-messages-to-look-cleaner' into develop 2022-07-20 22:56:22 -04:00
alwayscompiling 31fa0b50a3 Added regex for time and int submissions. 2022-07-20 22:06:07 -04:00
14 changed files with 599 additions and 235 deletions
+2 -1
View File
@@ -133,4 +133,5 @@ nextcord.log
# data file for bot # data file for bot
highscores_data.json highscores_data.json
highscores_message_map.json highscores_message_map.json
highscores_config.json highscores_config.json
deleted_backups/
+40
View File
@@ -0,0 +1,40 @@
"""
Bot command for deleting the guild's scores.
"""
from nextcord import Embed
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
from utilities.highscores_data import save_highscores_data # pylint: disable=import-error
class DeleteScores(commands.Cog, name="Delete Scores"):
"""
Warning: This command only works for developers.
Deletes this guild's scores.
"""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="delete-scores")
@commands.has_permissions(administrator=True)
async def delete_scores(self, ctx: commands.Context):
"""A command which deletes all scores for this guild.
Usage:
```
?delete_scores
```
"""
# only allow AlwaysCompiling to use.
# If you are a developer and want to use command, can add name to list
if ctx.author.id == 206602713511886849:
highscores_data.reset_guild_data(ctx.guild.id)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(DeleteScores(bot))
+40
View File
@@ -0,0 +1,40 @@
"""
Defines cog for guild listeners. Contains functions for various guild events.
"""
from nextcord.ext import commands
import nextcord
from highscores import highscores_data # pylint: disable=import-error
class GuildListener(commands.Cog, name="Guild Listener"):
"""Sets up message listener."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild):
"""Listener for Guild Join. Creates skeleton for new guild joined and saves it."""
highscores_data.add_new_server(guild.id)
welcome_message: str = "Hello, I keep track of Highscores for you!"
welcome_message += " Currently only supporting RuneScape boss highscores,"
welcome_message += " but will be more dynamic support in future."
welcome_message += ' For assistance on getting started, use "?register help".'
await guild.system_channel.send(content=welcome_message)
@commands.Cog.listener()
async def on_guild_remove(self, guild: nextcord.Guild):
"""Listener for Guild Remove. Removes data from highscores_data."""
print(f"Removing data for guild {guild.name} (id {guild.id}).")
highscores_data.remove_guild(guild.id, guild.name)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(GuildListener(bot))
+49 -7
View File
@@ -2,6 +2,8 @@
Defines cog for on message listener. Defines cog for on message listener.
""" """
import re
from nextcord.ext import commands from nextcord.ext import commands
import nextcord import nextcord
@@ -20,18 +22,58 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message: nextcord.Message): async def on_message(self, message: nextcord.Message):
"""Listener for message creation. Adds score and attachments to submission.""" """Listener for message creation. Adds score and attachments to submission."""
guild_id = message.guild.id
if message.author.id in submission_objects: if message.author.id in submission_objects:
message_id = submission_objects[message.author.id]["message_id"] message_id = submission_objects[message.author.id]["message_id"]
submission_message: nextcord.Message = await self.bot.get_channel( submission_channel_id = highscores_data.get_guild_submission_channel(guild_id)
highscores_data["submission_channel_id"] submission_message: nextcord.Message = (
).fetch_message(message_id) await self.bot.get_guild(guild_id)
.get_channel(submission_channel_id)
.fetch_message(message_id)
)
# need check if correct user and correct channel # need check if correct user and correct channel
if message.channel.id == highscores_data["submission_channel_id"]: if message.channel.id == submission_channel_id:
state = submission_objects[message.author.id]["submission_state"] state = submission_objects[message.author.id]["submission_state"]
if state == SubmissionState.SCORE and message.content: if state == SubmissionState.SCORE and message.content:
submission_objects[message.author.id]["score"] = message.content
embed = get_submission_embed(message.author.id) # check with regex if score matches correctly
is_time_record = highscores_data.get_config_time_record(
submission_objects[message.author.id]["category"]
)
time_regex = re.search(
"^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", message.content
)
int_regex = re.search("^[0-9]+$", message.content)
if time_regex is None and is_time_record:
embed = submission_message.embeds[0]
embed.add_field(
name="Error",
value="Time record must follow format MM:SS.ms, SS.ms, or SS.",
)
elif int_regex is None and not is_time_record:
embed = submission_message.embeds[0]
embed.add_field(name="Error", value="Number record must only be number.")
else:
score = message.content
if is_time_record:
# standardize time score format.
if re.search("^[1-9]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# score is already fine.
pass
elif re.search("^[0-9]{2}(\\.[0-9])?$", score):
# need to add leading colon and 0
score = "0:" + score
elif re.search("^[0-9](\\.[0-9])?$", score):
# need to add 0 in seconds and leading colon and 0
score = "0:0" + score
elif re.search("^[0]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# if there is a minute 10ths place 0, remove
score = score.lstrip("0")
submission_objects[message.author.id]["score"] = score
embed = get_submission_embed(message.author.id)
await submission_message.edit( await submission_message.edit(
embed=embed, embed=embed,
) )
@@ -41,7 +83,7 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
) )
elif state == SubmissionState.NAME and message.content: elif state == SubmissionState.NAME and message.content:
submission_objects[message.author.id]["username"] = message.content[ submission_objects[message.author.id]["username"] = message.content[
: highscores_data["username_length"] : highscores_data.get_guild_username_length(guild_id)
] ]
embed = get_submission_embed(message.author.id) embed = get_submission_embed(message.author.id)
await submission_message.edit( await submission_message.edit(
+6 -12
View File
@@ -6,9 +6,7 @@ from nextcord.ext import commands
import nextcord import nextcord
from cogs.submission.submission_views import SubmissionCreateButton # pylint: disable=import-error from cogs.submission.submission_views import SubmissionCreateButton # pylint: disable=import-error
from highscores import highscores_config # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error from highscores import highscores_data # pylint: disable=import-error
from utilities.data_storage import save_highscores_data # pylint: disable=import-error
from utilities.utils import send_highscore_message # pylint: disable=import-error from utilities.utils import send_highscore_message # pylint: disable=import-error
@@ -29,22 +27,18 @@ class PostHighscores(commands.Cog, name="Post Highscores"):
?post-scores ?post-scores
``` ```
""" """
highscore_channel_id = highscores_data["highscore_channel_id"] highscore_channel_id = highscores_data.get_guild_highscores_channel(ctx.guild.id)
print(f"Printing out highscores information in channel {highscore_channel_id}") print(f"Printing out highscores information in channel {highscore_channel_id}")
channel = self.bot.get_channel(highscore_channel_id) channel = ctx.guild.get_channel(highscore_channel_id)
error_response = 'Registered Highscores channel does not exist or was never registered. \n\ error_response = "Registered Highscores channel does not exist or was never registered. "
Register with "?register" command.' error_response += 'Register with "?register" command.'
assert channel is not None, await ctx.send(error_response) assert channel is not None, await ctx.send(error_response)
for boss, categories in highscores_config[ # pylint: disable=unused-variable for table in highscores_data.get_table_names(ctx.guild.id):
"highscore_table" message: nextcord.Message = await send_highscore_message(channel, table, ctx.guild.id)
].items():
message: nextcord.Message = await send_highscore_message(channel, boss)
await message.edit(view=SubmissionCreateButton(self.bot)) await message.edit(view=SubmissionCreateButton(self.bot))
save_highscores_data(highscores_data)
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
+53
View File
@@ -0,0 +1,53 @@
"""
Functions for controlling the bot's presence.
"""
import random
from nextcord.ext import commands
from nextcord.ext import tasks
import nextcord
class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self):
"""Listener for Guild Join. Updated presence on guild join."""
await self.change_presence()
@commands.Cog.listener()
async def on_ready(self):
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_change_presense is subclassed from tasks.loop.
self.periodic_change_presence.start() # pylint: disable=no-member
@tasks.loop(minutes=2)
async def periodic_change_presence(self):
"""Loops every 2 minutes and changes presence."""
await self.change_presence()
async def change_presence(self):
"""Randomly chooses a presence and updates bot's presence."""
statuses = [
f"?help | {len(self.bot.guilds)} servers",
f"Vuln Pls. | {len(self.bot.guilds)} servers",
f"All your base are belong to us. | {len(self.bot.guilds)} servers",
f"Double plus good. | {len(self.bot.guilds)} servers",
]
status = random.choice(statuses)
await self.bot.change_presence(activity=nextcord.Game(status))
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(BotPresence(bot))
+3 -6
View File
@@ -6,7 +6,6 @@ from nextcord import Embed
from nextcord.ext import commands from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error from highscores import highscores_data # pylint: disable=import-error
from utilities.data_storage import save_highscores_data # pylint: disable=import-error
class RegisterVerificationChannel(commands.Cog, name="Register"): class RegisterVerificationChannel(commands.Cog, name="Register"):
@@ -32,13 +31,13 @@ class RegisterVerificationChannel(commands.Cog, name="Register"):
if registration_type == "verify": if registration_type == "verify":
await ctx.send(f"Registered <#{channel_id}> as channel to post verifications in.") await ctx.send(f"Registered <#{channel_id}> as channel to post verifications in.")
highscores_data["verification_channel_id"] = channel_id highscores_data.set_guild_verification_channel(ctx.guild.id, channel_id)
elif registration_type == "highscore": elif registration_type == "highscore":
await ctx.send(f"Registered <#{channel_id}> as channel to post highscores in.") await ctx.send(f"Registered <#{channel_id}> as channel to post highscores in.")
highscores_data["highscore_channel_id"] = channel_id highscores_data.set_guild_highscore_channel(ctx.guild.id, channel_id)
elif registration_type == "submission": elif registration_type == "submission":
await ctx.send(f"Registered <#{channel_id}> as channel to post submissions in.") await ctx.send(f"Registered <#{channel_id}> as channel to post submissions in.")
highscores_data["submission_channel_id"] = channel_id highscores_data.set_guild_submission_channel(ctx.guild.id, channel_id)
else: else:
embed = Embed( embed = Embed(
title="Register command help", description="How to use the register command." title="Register command help", description="How to use the register command."
@@ -57,8 +56,6 @@ class RegisterVerificationChannel(commands.Cog, name="Register"):
embed.add_field(name="?register help", value="Use to show this message.") embed.add_field(name="?register help", value="Use to show this message.")
await ctx.reply(embed=embed) await ctx.reply(embed=embed)
save_highscores_data(highscores_data)
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
+5 -5
View File
@@ -5,7 +5,6 @@ Bot command to submit a score
from nextcord.ext import commands from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error from highscores import highscores_data # pylint: disable=import-error
from utilities.utils import submit_score # pylint: disable=import-error
from .verification_view import VerificationView from .verification_view import VerificationView
@@ -30,11 +29,12 @@ class Score(commands.Cog, name="Score"):
This command still works if you prefer to use it over the ui. This command still works if you prefer to use it over the ui.
``` ```
""" """
verification_channel_id = highscores_data["verification_channel_id"] guild_data = highscores_data[str(ctx.guild.id)]
channel = self.bot.get_channel(verification_channel_id) verification_channel_id = guild_data["verification_channel_id"]
channel = ctx.guild.get_channel(verification_channel_id)
error_response = "Registered Verification channel does not exist or was never registered. \ error_response = "Registered Verification channel does not exist or was never registered."
Contact server admins." error_response += "Contact server admins."
assert channel is not None, await ctx.send(error_response) assert channel is not None, await ctx.send(error_response)
verification_string = f"{ctx.author.display_name}:{boss_name}:{category}:{score}" verification_string = f"{ctx.author.display_name}:{boss_name}:{category}:{score}"
+25 -15
View File
@@ -7,8 +7,8 @@ import nextcord
from nextcord import Embed from nextcord import Embed
from nextcord.ext import commands from nextcord.ext import commands
from utilities.data_storage import open_message_map # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error from highscores import highscores_data # pylint: disable=import-error
from highscores import highscores_message_map # pylint: disable=import-error
from highscores import submission_objects # pylint: disable=import-error from highscores import submission_objects # pylint: disable=import-error
from .verification_view import VerificationView from .verification_view import VerificationView
@@ -52,12 +52,12 @@ def get_submission_embed(userid: int) -> nextcord.Embed:
return embed return embed
def category_select_options(boss_name: str) -> "list[nextcord.SelectOption]": def category_select_options(guild_id: int, boss_name: str) -> "list[nextcord.SelectOption]":
"""Returns list of categories for a given boss.""" """Returns list of categories for a given boss."""
options: "list[nextcord.SelectOption]" = [] options: "list[nextcord.SelectOption]" = []
boss_dict = highscores_data[boss_name] category_list = highscores_data.get_table_categories(guild_id, boss_name)
for category in boss_dict["categories"].items(): for category in category_list:
options.append(nextcord.SelectOption(label=category[0])) options.append(nextcord.SelectOption(label=category))
return options return options
@@ -148,8 +148,10 @@ class SubmitButton(nextcord.ui.Button):
self._bot = bot self._bot = bot
async def callback(self, interaction: nextcord.Interaction): async def callback(self, interaction: nextcord.Interaction):
verification_channel_id = highscores_data["verification_channel_id"] verification_channel_id = highscores_data.get_guild_verification_channel(
channel = self._bot.get_channel(verification_channel_id) interaction.guild_id
)
channel = interaction.guild.get_channel(verification_channel_id)
error_response = "Registered Verification channel does not exist or was never registered. \ error_response = "Registered Verification channel does not exist or was never registered. \
Contact server admins." Contact server admins."
@@ -186,23 +188,28 @@ class SubmissionCreateButton(nextcord.ui.View):
self, button: nextcord.ui.Button, interaction: nextcord.Interaction self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Button for score submission.""" """Button for score submission."""
highscores_message_map = open_message_map() # Don't allow a user to start a new submission if one already exists
boss_name = highscores_message_map[str(interaction.message.id)] if interaction.user.id in submission_objects:
# embed = nextcord.Embed(title="Highscores Submission") await interaction.send("Please finish in progress submission first.", ephemeral=True)
# embed.add_field(name="Boss", value=boss_name, inline=False) return
# embed.add_field(name="User", value=interaction.user.display_name, inline=False)
channel = self._bot.get_channel(highscores_data["submission_channel_id"]) boss_name = highscores_message_map[interaction.guild.id][interaction.message.id]
channel = interaction.guild.get_channel(
highscores_data.get_guild_submission_channel(interaction.guild_id)
)
# a user should only have 1 submission active at once. # a user should only have 1 submission active at once.
# submitting with dummy message_id until message created # submitting with dummy message_id until message created
# split username on pipe.
submission_objects[interaction.user.id] = { submission_objects[interaction.user.id] = {
"message_id": 0, "message_id": 0,
"guild_id": interaction.guild_id,
"submission_state": SubmissionState.CATEGORY, "submission_state": SubmissionState.CATEGORY,
"boss_name": boss_name, "boss_name": boss_name,
"score": "", "score": "",
"category": "", "category": "",
"username": interaction.user.display_name.split("|")[0][ "username": interaction.user.display_name.split("|")[0][
: highscores_data["username_length"] : highscores_data.get_guild_username_length(interaction.guild_id)
], ],
} }
@@ -236,10 +243,13 @@ class SubmissionView(nextcord.ui.View):
self._bot = bot self._bot = bot
self._state = submission_objects[self._submission_user]["submission_state"] self._state = submission_objects[self._submission_user]["submission_state"]
self._boss_name = submission_objects[self._submission_user]["boss_name"] self._boss_name = submission_objects[self._submission_user]["boss_name"]
self._guild_id = submission_objects[self._submission_user]["guild_id"]
# add items depending on given state # add items depending on given state
if self._state is SubmissionState.CATEGORY: if self._state is SubmissionState.CATEGORY:
self.add_item(SubmissionDropdown(bot, category_select_options(self._boss_name))) self.add_item(
SubmissionDropdown(bot, category_select_options(self._guild_id, self._boss_name))
)
self.add_item(CancelButton()) self.add_item(CancelButton())
self.add_item(NextButton(self._bot)) self.add_item(NextButton(self._bot))
elif ( elif (
+14 -6
View File
@@ -1,5 +1,5 @@
{ {
"highscore_table": { "tables": {
"The Ambassador": [ "The Ambassador": [
"Kill Count", "Kill Count",
"Kill Time", "Kill Time",
@@ -212,8 +212,8 @@
"Spooned Log" "Spooned Log"
], ],
"Taraket the Necromancer": [ "Taraket the Necromancer": [
"Kill Count", "Solo Kill Count",
"Kill Time", "Solo Kill Time",
"Group Kill Count", "Group Kill Count",
"Group Kill Time", "Group Kill Time",
"Spooned Log" "Spooned Log"
@@ -244,8 +244,8 @@
"Spooned Log" "Spooned Log"
], ],
"Verak Lith": [ "Verak Lith": [
"Kill Count", "Solo Kill Count",
"Kill Time", "Solo Kill Time",
"Group Kill Count", "Group Kill Count",
"Group Kill Time", "Group Kill Time",
"Spooned Log" "Spooned Log"
@@ -318,10 +318,18 @@
"ascending": true, "ascending": true,
"is_time_record": true "is_time_record": true
}, },
"Solo Kill Count": {
"ascending": false,
"is_time_record": false
},
"Solo Kill Time": {
"ascending": true,
"is_time_record": true
},
"Spooned Log": { "Spooned Log": {
"ascending": true, "ascending": true,
"is_time_record": false "is_time_record": false
} }
}, },
"highscore_size": 3 "highscore_size": 3
} }
+15 -13
View File
@@ -4,14 +4,13 @@ main file for pvm-highscores bot
import logging import logging
import os import os
import sys
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
import config import config
from utilities.data_storage import open_highscores_config from utilities.highscores_data import HighscoresData
from utilities.data_storage import open_highscores_data
from utilities.data_storage import open_message_map
logger = logging.getLogger("nextcord") logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@@ -20,22 +19,31 @@ handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(me
logger.addHandler(handler) logger.addHandler(handler)
highscores_config = open_highscores_config() highscores_data: HighscoresData = HighscoresData()
highscores_data = open_highscores_data() highscores_message_map = {}
highscores_message_map = open_message_map()
for guild in highscores_data.get_guilds():
highscores_message_map[guild] = {}
for table in highscores_data.get_table_names(guild):
message_id = highscores_data.get_table_message_id(guild, table)
highscores_message_map[guild][message_id] = table
submission_objects: dict = {} submission_objects: dict = {}
def main(): def main():
"""Main function for bot.""" """Main function for bot."""
min_python_version = (3, 7)
if sys.version_info < min_python_version:
print("ERROR: Python version must be >= 3.7")
sys.exit()
# Allows privledged intents for monitoring members joining, roles editing, and role assignments # Allows privledged intents for monitoring members joining, roles editing, and role assignments
# These need to be enabled in the developer portal as well # These need to be enabled in the developer portal as well
intents = nextcord.Intents.default() intents = nextcord.Intents.default()
# Required in order to read messages (eg. prefix commands) # Required in order to read messages (eg. prefix commands)
intents.message_content = True # pylint: disable=assigning-non-slot intents.message_content = True # pylint: disable=assigning-non-slot
intents.messages = True # pylint: disable=assigning-non-slot
# To enable the guilds priveleged intent: # To enable the guilds priveleged intent:
# intents.guilds = True # intents.guilds = True
@@ -43,15 +51,9 @@ def main():
# To enable the members priveleged intent: # To enable the members priveleged intent:
# intents.members = True # intents.members = True
# Set custom status to "Listening to ?help"
activity = nextcord.Activity(
type=nextcord.ActivityType.listening, name=f"{config.BOT_PREFIX}help"
)
bot = commands.Bot( bot = commands.Bot(
commands.when_mentioned_or(config.BOT_PREFIX), commands.when_mentioned_or(config.BOT_PREFIX),
intents=intents, intents=intents,
activity=activity,
) )
# Get the modules of all cogs whose directory structure is ./cogs/<module_name> # Get the modules of all cogs whose directory structure is ./cogs/<module_name>
-72
View File
@@ -1,72 +0,0 @@
"""Functions for retreiving and storing highscores data and configurations."""
import os
import json
HIGHSCORES_CONFIG_FILE = "highscores_config.json"
HIGHSCORES_DATA_FILE = "highscores_data.json"
HIGHSCORES_MESSAGE_MAP_FILE = "highscores_message_map.json"
def open_highscores_config():
"""
Opens the highscores config file and returns.
@return: dict of highscores config.
"""
# Load bot config
with open(HIGHSCORES_CONFIG_FILE, encoding="utf-8", mode="r") as file:
return json.load(file)
def open_highscores_data():
"""
Opens a highscores_data.json file if one exists, otherwise creates a skeleton of the data.
@return: dict of highscores data.
"""
if os.path.exists(HIGHSCORES_DATA_FILE):
with open(HIGHSCORES_DATA_FILE, encoding="utf-8", mode="r") as file:
data = json.load(file)
else:
# create the json object
data = {"highscore_channel_id": -1, "verification_channel_id": -1, "username_length": 12}
config: dict = open_highscores_config()
for boss, categories in config["highscore_table"].items():
data[boss] = {"message_id": 0}
boss_categories = {}
for category in categories: # pylint: disable=unused-variable
rank_list = []
boss_categories[category] = rank_list
data[boss]["categories"] = boss_categories
return data
def open_message_map():
"""
Opens a highscore_message_map.json file if one exists. Otherwise returns empty dict.
@return: dict message_map of message to boss.
"""
if os.path.exists(HIGHSCORES_MESSAGE_MAP_FILE):
with open(HIGHSCORES_MESSAGE_MAP_FILE, encoding="utf-8", mode="r") as file:
message_map = json.load(file)
else:
# create the json object
message_map = {}
return message_map
def save_highscores_data(data):
"""
Saves the highscores data dict into a json file.
@param data: dict of highscores data.
"""
with open(HIGHSCORES_DATA_FILE, encoding="utf-8", mode="w") as file:
file.write(json.dumps(data, indent=4))
def save_message_map(message_map):
"""
Saves the highscores message mao data dict into a json file.
@param map: dict map of message to boss.
"""
with open(HIGHSCORES_MESSAGE_MAP_FILE, encoding="utf-8", mode="w") as file:
file.write(json.dumps(message_map, indent=4))
+304
View File
@@ -0,0 +1,304 @@
"""Class and functions for storing and retrieving highscores data."""
import os
import json
HIGHSCORES_CONFIG_FILE = "highscores_config.json"
HIGHSCORES_DATA_FILE = "highscores_data.json"
class HighscoresData:
"""Class for storing and retrieving highscores data."""
def __init__(self) -> None:
self._config = open_highscores_config()
self._data = open_highscores_data()
def add_new_server(self, guild_id: int):
"""
Creates new skeleton data dict and adds to self for new guild.
@param guild_id: guild_id to add.
"""
# create the dict
skeleton = {
"highscore_channel_id": -1,
"verification_channel_id": -1,
"username_length": 12,
}
config: dict = open_highscores_config()
skeleton["tables"] = {}
for boss, categories in config["tables"].items():
skeleton["tables"][boss] = {"message_id": 0}
boss_categories = {}
for category in categories:
scores_list = {}
boss_categories[category] = scores_list
skeleton["tables"][boss]["categories"] = boss_categories
self._data[str(guild_id)] = skeleton
def add_score(
self, guild_id: int, boss_name: str, category: str, user: str, score: str
) -> None:
"""
Adds a new score to guilds highscore data.
Ensures that there is no user duplicate and then sorts the scores
@param guild_id: guild id to retrieve from.
@param boss_name: boss to add score to.
@param category: category to add score to.
@param user: user that submitted score.
@param score: score to add.
"""
scores: "list[tuple]" = self._data[str(guild_id)]["tables"][boss_name]["categories"][
category
]
ascending: bool = self._config["categories"][category]["ascending"]
time_record: bool = self._config["categories"][category]["is_time_record"]
# create score tuple for either time or int
# define sort index
if time_record:
if score.find(":") == -1:
score_seconds = float(score)
else:
minutes = score.split(":")[0]
seconds = score.split(":")[1]
score_seconds = int(minutes) * 60 + float(seconds)
score_list = [score, score_seconds]
sort_index = 1
else:
score_list = [int(score)]
sort_index = 0
# Will not ensure new score is better than old one. Allows for correcting mistakes.
scores[user] = score_list
scores = dict(sorted(scores.items(), key=lambda x: x[1][sort_index], reverse=not ascending))
self._data[str(guild_id)]["tables"][boss_name]["categories"][category] = scores
save_highscores_data(self._data)
def get_categories_data(self, guild_id: int, table: str) -> "list[tuple]":
"""
Returns categories data in a list of tuples. Calls dict function items().
@param guild_id: guild's id to retrieve from.
@param table: table to get category data from
@returns: list of tuples containing all category and relative score data.
"""
return self._data[str(guild_id)]["tables"][table]["categories"].items()
def get_config_highscore_size(self) -> int:
"""
Gets the defined size of highscore for config.
@returns: int highscore size
"""
return self._config["highscore_size"]
def get_config_time_record(self, category: str) -> bool:
"""
Tells if the category is a time record
@param category: category name to retrieve from.
@returns: bool indicating time record or not.
"""
return self._config["categories"][category]["is_time_record"]
def get_guilds(self) -> "list[int]":
"""
Gets a list of guilds tracked by data
@returns: list of ints representing guild ids
"""
guilds: "list[int]" = []
for guild, data in self._data.items(): # pylint: disable=unused-variable
guilds.append(int(guild))
return guilds
def get_guild_highscores_channel(self, guild_id: int) -> int:
"""
Returns the channel id to post highscore messages to.
@param guild_id: guild id to retrieve from.
@returns: channel id of highscores channel.
"""
return self._data[str(guild_id)]["highscore_channel_id"]
def get_guild_submission_channel(self, guild_id: int) -> int:
"""
Returns the channel id to post submission messages to.
@param guild_id: guild id to retrieve from.
@returns: channel id of submission channel.
"""
return self._data[str(guild_id)]["submission_channel_id"]
def get_guild_username_length(self, guild_id: int) -> int:
"""
Returns username length as set by each guild.
@param guild_id: guild id to retrieve from.
@returns: int of username length.
"""
return self._data[str(guild_id)]["username_length"]
def get_guild_verification_channel(self, guild_id: int) -> int:
"""
Returns the channel id to post verification messages to.
@param guild_id: guild id to retrieve from.
@returns: channel id of verification channel.
"""
return self._data[str(guild_id)]["verification_channel_id"]
def get_table_categories(self, guild_id: int, table: str) -> "list[str]":
"""
Returns list of categories for a guild's table
@param guild_id: guild id to retrieve from.
@param table: the table to retreive from.
@returns: string list of categories in given table.
"""
categories: "list[str]" = []
for category, data in self._data[str(guild_id)][ # pylint: disable=unused-variable
"tables"
][table]["categories"].items():
categories.append(category)
return categories
def get_table_message_id(self, guild_id: int, table: str) -> int:
"""
Returns the message id for given guild's table.
@param guild_id: guild id to retrieve from.
@param table: the table to retrieve from.
@returns: int id of message for table's highscores.
"""
return self._data[str(guild_id)]["tables"][table]["message_id"]
def get_table_names(self, guild_id) -> "list[str]":
"""
Returns a list of table names.
@param guild_id: guild id to retrieve from.
@returns: list of table names
"""
tables: "list[str]" = []
for table, data in self._data[str(guild_id)][ # pylint: disable=unused-variable
"tables"
].items():
tables.append(table)
return tables
def remove_guild(self, guild_id: int, guild_name: str) -> None:
"""
Removes a guild's data from object. Saves backup.
@param guild_id: guild id to remove.
@param guild_name: name of guild for backup.
"""
old_data = self._data.pop(str(guild_id))
save_highscores_data(self._data)
if not os.path.exists("deleted_backups"):
os.mkdir("deleted_backups")
with open(f"deleted_backups/{guild_name}_backup.json", encoding="utf-8", mode="w") as file:
file.write(json.dumps(old_data, indent=4))
def reset_guild_data(self, guild_id: int) -> None:
"""
Resets highscore data for a guild. Should only be used for development really...
@param guild_id: guild id to reset data for
"""
guild_data: dict = self._data[str(guild_id)]
for boss, boss_data in guild_data["tables"].items():
for category, category_data in boss_data["categories"].items():
category_data = []
boss_data["categories"][category] = category_data
guild_data["tables"][boss] = boss_data
self._data[str(guild_id)] = guild_data
save_highscores_data(self._data)
def set_guild_highscore_channel(self, guild_id: int, channel_id: int) -> None:
"""
Sets the guild's highscores channel.
@param guild_id: guild id to set to.
@param channel_id: channel id to set.
"""
self._data[str(guild_id)]["highscore_channel_id"] = channel_id
def set_guild_submission_channel(self, guild_id: int, channel_id: int) -> None:
"""
Sets the guild's submission channel.
@param guild_id: guild id to set to.
@param channel_id: channel id to set.
"""
self._data[str(guild_id)]["submission_channel_id"] = channel_id
def set_guild_verification_channel(self, guild_id: int, channel_id: int) -> None:
"""
Sets the guild's verification channel.
@param guild_id: guild id to set to.
@param channel_id: channel id to set.
"""
self._data[str(guild_id)]["verification_channel_id"] = channel_id
def set_table_message_id(self, guild_id: int, table: str, message_id: int) -> None:
"""
Sets the message id for given guild's table.
Saves highscores data.
@param guild_id: guild id to set to.
@param table: the table to set to.
@param message_id: message id to set.
"""
self._data[str(guild_id)]["tables"][table]["message_id"] = message_id
save_highscores_data(self._data)
def open_highscores_config():
"""
Opens the highscores config file and returns.
@return: dict of highscores config.
"""
# Load bot config
with open(HIGHSCORES_CONFIG_FILE, encoding="utf-8", mode="r") as file:
return json.load(file)
def open_highscores_data():
"""
Opens a highscores_data.json file if one exists, otherwise returns empty dict.
@return: dict of highscores data.
"""
if os.path.exists(HIGHSCORES_DATA_FILE):
with open(HIGHSCORES_DATA_FILE, encoding="utf-8", mode="r") as file:
data = json.load(file)
else:
data = {}
return data
def save_highscores_data(data: dict):
"""
Saves the highscores data dict into a json file.
@param data: dict of highscores data.
"""
with open(HIGHSCORES_DATA_FILE, encoding="utf-8", mode="w") as file:
file.write(json.dumps(data, indent=4))
+43 -98
View File
@@ -3,50 +3,49 @@ Contains various utlity functions for highscores bot
""" """
import nextcord import nextcord
from highscores import highscores_config # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error from highscores import highscores_data # pylint: disable=import-error
from highscores import highscores_message_map # pylint: disable=import-error from highscores import highscores_message_map # pylint: disable=import-error
from utilities.data_storage import save_highscores_data # pylint: disable=import-error
from utilities.data_storage import save_message_map # pylint: disable=import-error
def format_highscore_message(boss_name: str): def format_highscore_message(boss_name: str, guild_id: int):
""" """
returns a formatted string for a boss's highscore returns a formatted string for a boss's highscore
@param boss: the dictionary object for the boss @param boss: the dictionary object for the boss
@param guild_id: the guild id the message is for
@return the formatted string @return the formatted string
""" """
ret_string = f"**{boss_name}**\n```" ret_string = f"**{boss_name}**\n```"
boss_data = highscores_data[boss_name]
# Gather all categories and add to string # Gather all categories and add to string
for key, value in boss_data["categories"].items(): for category_name in highscores_data.get_table_categories(guild_id, boss_name):
ret_string += f"{key}".ljust(26, " ") ret_string += f"{category_name}".ljust(26, " ")
ret_string += "\n" ret_string += "\n"
highscore_size = highscores_config["highscore_size"] highscore_size = highscores_data.get_config_highscore_size()
# list rankings in a single line. Start with first place for each category and then continue # list rankings in a single line. Start with first place for each category and then continue
for i in range(highscore_size): for i in range(highscore_size):
for key, value in boss_data["categories"].items(): for category_name, category_data in highscores_data.get_categories_data(
if len(value) > i: guild_id, boss_name
score_pair = value[i] ):
if len(category_data) > i:
# pad username to max length. # pad username to max length.
username: str = score_pair[0].ljust(highscores_data["username_length"], " ") username: str = list(category_data.keys())[i].ljust(
score: str = str(score_pair[1]) highscores_data.get_guild_username_length(guild_id), " "
)
# turns category data value into a string. Very janky.
score: str = str(list(category_data.values())[i][0])
# standardize time score format. # pad score to correct length
if highscores_config["categories"][key]["is_time_record"]: # MM:SS.ms is 7 characters long
if score.find(":") == -1: if highscores_data.get_config_time_record(category_name):
score = "00:" + score # if there are single digit minutes, add a space for formatting.
elif len(score.split(":", maxsplit=1)[0]) < 2: if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2:
score = "0" + score score = " " + score
if score.find(".") == -1: else:
# insert 2 spaces to make up for period and ms score = score.rjust(7, " ")
score = score + " "
line_str = f"{i+1}: {username} - " ret_string += f"{i+1}: {username} - {score}".ljust(26, " ")
ret_string += line_str + score.rjust(26 - len(line_str) - 1) + " "
else: else:
ret_string += f"{i+1}: submit your score".ljust(26, " ") ret_string += f"{i+1}: submit your score".ljust(26, " ")
ret_string += "\n" ret_string += "\n"
@@ -54,16 +53,20 @@ def format_highscore_message(boss_name: str):
return ret_string + "```" return ret_string + "```"
async def send_highscore_message(channel, boss_name: str) -> nextcord.Message: async def send_highscore_message(
channel: nextcord.TextChannel, boss_name: str, guild_id: int
) -> nextcord.Message:
""" """
Function takes care of checking if a message exists for a boss, then Function takes care of checking if a message exists for a boss, then
creating and sending/editing the message creating and sending/editing the message
@param channel: the channel to send the message in/search for message id @param channel: the channel to send the message in/search for message id
@param boss: the boss to send the message about @param boss: the boss to send the message about
@param guild_id: the guild id to send message to
@return: The nextcord message
""" """
highscore_string = format_highscore_message(boss_name) highscore_string = format_highscore_message(boss_name, guild_id)
message_id = highscores_data[boss_name]["message_id"] message_id = highscores_data.get_table_message_id(guild_id, boss_name)
try: try:
message: nextcord.Message = await channel.fetch_message(message_id) message: nextcord.Message = await channel.fetch_message(message_id)
await message.edit(content=highscore_string) await message.edit(content=highscore_string)
@@ -71,10 +74,14 @@ async def send_highscore_message(channel, boss_name: str) -> nextcord.Message:
# message doesn't exist. # message doesn't exist.
message: nextcord.Message = await channel.send(highscore_string) message: nextcord.Message = await channel.send(highscore_string)
message_id = message.id message_id = message.id
highscores_data[boss_name]["message_id"] = message_id highscores_data.set_table_message_id(guild_id, boss_name, message_id)
# save message -> boss_name in message_map # save message -> boss_name in message_map
highscores_message_map[str(message_id)] = boss_name # get guild id from channel
save_message_map(highscores_message_map) # channel should come from the guild that is requesting this highscores message anyways.
# check if guild is in message map already
if not channel.guild.id in highscores_message_map:
highscores_message_map[channel.guild.id] = {}
highscores_message_map[channel.guild.id][message_id] = boss_name
return message return message
@@ -90,77 +97,15 @@ async def submit_score(
@param score: score to add @param score: score to add
""" """
boss_category_config = highscores_config["categories"][category] highscores_data.add_score(interaction.guild_id, boss_name, category, user, score)
# Splitting on pipe. My discord has multiple names on account split by pipe.
# cap length to data defined length.
user = user.split("|")[0][: highscores_data["username_length"]]
# strip leading zeros and colon from score
score = score.lstrip("0:")
# create score tuple for either time or int
# define sort index
if boss_category_config["is_time_record"]:
if score.find(":") == -1:
score_seconds = float(score)
else:
minutes = score.split(":")[0]
seconds = score.split(":")[1]
score_seconds = int(minutes) * 60 + float(seconds)
score_tuple = (user, score, score_seconds)
sort_index = 2
else:
score_tuple = (user, int(score))
sort_index = 1
scores: "list[tuple]" = highscores_data[boss_name]["categories"][category]
# if user is in an existing tuple, delete it.
users_scores: "list[tuple]" = [score_tuple]
for tup in scores:
if user == tup[0]:
# Add tup to users_scores, remove from scores
users_scores.append(tup)
# sort users_scores
users_scores.sort(
key=lambda x: x[sort_index],
reverse=not boss_category_config["ascending"],
)
for index, tup in enumerate(users_scores):
if index == 0:
# first score is the best score.
if tup not in scores:
scores.append(tup)
else:
# discard all other scores from scores list.
if tup in scores:
scores.remove(tup)
# sort scores.
scores.sort(
key=lambda x: x[sort_index],
reverse=not boss_category_config["ascending"],
)
# enforce highscore size
while len(scores) > highscores_config["highscore_size"]:
scores.pop()
highscores_data[boss_name]["categories"][category] = scores
# edit message # edit message
highscore_channel_id = highscores_data["highscore_channel_id"] highscore_channel_id = highscores_data.get_guild_highscores_channel(interaction.guild_id)
print(f"Editting message {highscore_channel_id}") print(f"Editting message {highscore_channel_id}")
channel = interaction.guild.get_channel(highscore_channel_id) channel = interaction.guild.get_channel(highscore_channel_id)
error_response = 'Registered Highscores channel does not exist or was never registered. \ error_response = "Registered Highscores channel does not exist or was never registered."
Register with "?register" command.' error_response += 'Register with "?register" command.'
assert channel is not None, await interaction.send(error_response, ephemeral=True) assert channel is not None, await interaction.send(error_response, ephemeral=True)
await send_highscore_message(channel, boss_name) await send_highscore_message(channel, boss_name, interaction.guild_id)
# save data
save_highscores_data(highscores_data)