Compare commits

...
Author SHA1 Message Date
alwayscompiling fe0a74cfc4 Extend submission timeout. Remove submission object on submission timeout. Also Let user know when it isn't their submission. 2022-07-30 16:19:44 -04:00
alwayscompiling 5bed587da9 Fixing configs for elite dungeon bosses. 2022-07-30 10:56:21 -04:00
alwayscompiling f7a40f83a9 Birthday troll. 2022-07-29 22:45:40 -04:00
alwayscompiling a66bc4982f on_guild_join needs 2 parameters. 2022-07-29 22:45:21 -04:00
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 ef17d3dc3e Made all scores line up properly. 2022-07-20 22:55:40 -04:00
alwayscompiling d711763c80 Standardize time score format. 2022-07-20 22:35:33 -04:00
alwayscompiling 8318bf741a Remove old TODO. 2022-07-20 22:32:59 -04:00
alwayscompiling 17c1915810 Change location of approve button. Should be on right. 2022-07-20 22:31:39 -04:00
alwayscompiling 674ea94588 Pad username out to max length. 2022-07-20 22:24:50 -04:00
alwayscompiling 090184c1e9 Tracking username length limit in data file. 2022-07-20 22:17:20 -04:00
alwayscompiling 31fa0b50a3 Added regex for time and int submissions. 2022-07-20 22:06:07 -04:00
alwayscompiling 374632bbc6 Removing TODOs. 2022-07-20 20:58:17 -04:00
alwayscompiling 8a36c9dac9 Users cannot move to next step without completing current step. 2022-07-20 20:50:29 -04:00
alwayscompiling 1900e21f72 Updating message listener to work with states. 2022-07-20 20:37:29 -04:00
alwayscompiling 2c85483db5 Buttons now change views, embed has proper steps. Added todos for what is next. 2022-07-19 22:29:06 -04:00
alwayscompiling 63db9fb7d4 Create buttons and enum for submission state navigation. 2022-07-19 21:05:37 -04:00
alwayscompiling 5155744ec5 Made config file more efficient. 2022-07-13 22:02:40 -04:00
alwayscompiling fedc8be778 Adding VerificationView to bot on cog load. 2022-07-13 20:01:21 -04:00
alwayscompiling 125939e5b3 Updating runescape_bosses.json example config to follow new config style. 2022-07-12 21:26:12 -04:00
alwayscompiling 3b410b5f90 Redid configs to allow for unique categories on each table. 2022-07-12 21:00:51 -04:00
alwayscompiling d7bac2048f Changed listener to allow for score or attachment by themselves. 2022-07-12 19:59:21 -04:00
alwayscompiling dd4bee73c6 Approved message now mentions person who approved. 2022-07-12 18:30:25 -04:00
alwayscompiling d21683fb59 Only add best score if its new, only delete worse scores if they exist. 2022-07-12 16:07:20 -04:00
alwayscompiling 44b3d7a80b Merge branch 'develop' into 4-user-score-update 2022-07-12 15:59:42 -04:00
alwayscompiling af5017c95d Changed id of verification views. Solved problem for long lasting buttons. 2022-07-12 15:59:17 -04:00
alwayscompiling 3087c6bc39 Worked out dup check method, just need to debug. 2022-07-12 15:58:39 -04:00
alwayscompiling cf0ef96edc Merge branch 'develop' into 4-user-score-update 2022-07-12 15:22:07 -04:00
alwayscompiling 9ba37c22bb Splitting username on pipe. 2022-07-12 15:18:59 -04:00
alwayscompiling 8eeda14119 Strip leading 0 and : from scores. 2022-07-12 15:15:37 -04:00
alwayscompiling 115e3dd3d0 Shortening the pad size for messages a little. 2022-07-10 21:59:11 -04:00
alwayscompiling 9ef2acf34c Capping user names to 12 characters because runescape names. Will need to revisit user name limit later. 2022-07-10 21:51:19 -04:00
alwayscompiling 82f47ac136 Temporary attempt. Makes assumption that a user will submit a better score. 2022-07-10 21:47:01 -04:00
alwayscompiling 4a15c63e8c Corrected some comments. 2022-07-10 21:11:51 -04:00
alwayscompiling 4c6279b444 Made post command admin only. 2022-07-10 21:08:30 -04:00
alwayscompiling 39f4a108ca Fixing hardmode category searching. 2022-07-10 20:33:39 -04:00
alwayscompiling 83f0bf6498 Slight time score bug fixes. 2022-07-10 20:27:56 -04:00
alwayscompiling 3f5ebdf175 Changed listener check to be more efficient. 2022-07-10 20:16:52 -04:00
alwayscompiling f98c80d200 Updated comments in message listener. 2022-07-10 20:09:27 -04:00
alwayscompiling b61008518c Moved message listener to a cog. Listener is set up on bot run. Listener listens to all messages. Pulls score and image from a message sent in submission channel if a user has an active submission. 2022-07-10 20:05:20 -04:00
alwayscompiling 198d9008b9 Fixed missing parameter. 2022-07-09 19:46:50 -04:00
alwayscompiling ae1d820297 Refactored register command to be one command that takes in parameter. 2022-07-09 14:21:45 -04:00
alwayscompiling a602347d09 Removing TODOs. 2022-07-09 13:57:48 -04:00
alwayscompiling 03ed723f5c Add input verification in verification_view.py. 2022-07-09 13:53:23 -04:00
alwayscompiling 18db48a55e Sending to verification view. 2022-07-09 11:43:49 -04:00
alwayscompiling 7854be890f Entire submission process works somehow. 2022-07-09 11:24:29 -04:00
alwayscompiling 6a7207d710 Slight organization changes. Decided to use new message for score input. 2022-07-07 22:57:38 -04:00
alwayscompiling f9800677cc Got response of dropdown. 2022-07-06 22:16:22 -04:00
alwayscompiling 3d16fe81e5 First dropdown works. 2022-07-06 21:37:46 -04:00
alwayscompiling c4261fc7f4 Changed where submission views live. 2022-07-06 21:05:04 -04:00
alwayscompiling d0428afb3f Add example config, ignore working config. 2022-07-06 20:39:59 -04:00
alwayscompiling a92f790a5f Adding new boss. 2022-07-04 10:15:01 -04:00
alwayscompiling 22d368f0a8 Adding submission button for all highscore messages. 2022-07-04 10:13:16 -04:00
alwayscompiling 84cf3781ee Adding map to track message id to boss. 2022-07-03 23:01:24 -04:00
21 changed files with 1403 additions and 409 deletions
+3
View File
@@ -132,3 +132,6 @@ nextcord.log
# data file for bot
highscores_data.json
highscores_message_map.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))
+1 -1
View File
@@ -22,5 +22,5 @@ class HelpCog(commands.Cog):
# setup functions for bot
def setup(bot: commands.Bot):
"""Adds functon to bot"""
"""Adds function to bot"""
bot.add_cog(HelpCog(bot))
+114
View File
@@ -0,0 +1,114 @@
"""
Defines cog for on message listener.
"""
from datetime import date
import random
import re
from nextcord.ext import commands
import nextcord
from cogs.submission.submission_views import SubmissionState # pylint: disable=import-error
from cogs.submission.submission_views import get_submission_embed # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error
from highscores import submission_objects # pylint: disable=import-error
class SubmissionListener(commands.Cog, name="Message Listener"):
"""Sets up message listener."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_message(self, message: nextcord.Message):
"""Listener for message creation. Adds score and attachments to submission."""
# don't want to process any messages from bot.
if message.author.id == self.bot.user.id:
return
guild_id = message.guild.id
# troll addition for friends birthday
if guild_id == 239758371358638080 and date.today() == date(2022, 7, 31):
# 5% chance to send the message
chance = random.randint(1, 20)
if chance == 1:
await message.channel.send("Happy Birthday Chris <@642330132958281750>")
if message.author.id in submission_objects:
message_id = submission_objects[message.author.id]["message_id"]
submission_channel_id = highscores_data.get_guild_submission_channel(guild_id)
submission_message: nextcord.Message = (
await self.bot.get_guild(guild_id)
.get_channel(submission_channel_id)
.fetch_message(message_id)
)
# need check if correct user and correct channel
if message.channel.id == submission_channel_id:
state = submission_objects[message.author.id]["submission_state"]
if state == SubmissionState.SCORE and message.content:
# 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(
embed=embed,
)
elif state == SubmissionState.PROOF and len(message.attachments) > 0:
await submission_message.edit(
files=[await attch.to_file() for attch in message.attachments],
)
elif state == SubmissionState.NAME and message.content:
submission_objects[message.author.id]["username"] = message.content[
: highscores_data.get_guild_username_length(guild_id)
]
embed = get_submission_embed(message.author.id)
await submission_message.edit(
embed=embed,
)
await message.delete()
# 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(SubmissionListener(bot))
+12 -11
View File
@@ -3,9 +3,10 @@ Bot command to post the highscores
"""
from nextcord.ext import commands
from highscores import highscores_config # pylint: disable=import-error
import nextcord
from cogs.submission.submission_views import SubmissionCreateButton # 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
@@ -16,6 +17,7 @@ class PostHighscores(commands.Cog, name="Post Highscores"):
self.bot = bot
@commands.command(name="post-scores")
@commands.has_permissions(administrator=True)
async def post_highscores(self, ctx: commands.Context):
"""
A command which posts the highscore in the designated channel.
@@ -25,22 +27,21 @@ class PostHighscores(commands.Cog, name="Post Highscores"):
?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}")
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. \
Register with "?register" command.'
error_response = "Registered Highscores channel does not exist or was never registered. "
error_response += 'Register with "?register" command.'
assert channel is not None, await ctx.send(error_response)
for boss in highscores_config["bosses"]:
await send_highscore_message(channel, boss["boss"])
save_highscores_data(highscores_data)
for table in highscores_data.get_table_names(ctx.guild.id):
message: nextcord.Message = await send_highscore_message(channel, table, ctx.guild.id)
await message.edit(view=SubmissionCreateButton(self.bot))
# 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 functon to bot"""
"""Adds function to bot"""
bot.add_cog(PostHighscores(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, guild: nextcord.Guild): # pylint: disable=unused-argument
"""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))
+64
View File
@@ -0,0 +1,64 @@
"""
Bot command for registering various channels to the bot.
"""
from nextcord import Embed
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
class RegisterVerificationChannel(commands.Cog, name="Register"):
"""Bot command for registering various channels to the bot."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="register")
@commands.has_permissions(administrator=True)
async def register_highscores(self, ctx: commands.Context, registration_type: str):
"""A command which registers current channel as for various uses.
Usage:
```
?register verify (Use in channel you want to verify submissions in.)
?register highscore (Use in channel you want to show highscore messages in.)
?register submission (Use in channel you want submission messages to be send it.)
?register help (Shows help message.)
```
"""
# respond to the message
channel_id = ctx.channel.id
if registration_type == "verify":
await ctx.send(f"Registered <#{channel_id}> as channel to post verifications in.")
highscores_data.set_guild_verification_channel(ctx.guild.id, channel_id)
elif registration_type == "highscore":
await ctx.send(f"Registered <#{channel_id}> as channel to post highscores in.")
highscores_data.set_guild_highscore_channel(ctx.guild.id, channel_id)
elif registration_type == "submission":
await ctx.send(f"Registered <#{channel_id}> as channel to post submissions in.")
highscores_data.set_guild_submission_channel(ctx.guild.id, channel_id)
else:
embed = Embed(
title="Register command help", description="How to use the register command."
)
embed.add_field(
name="?register verify", value="Use in channel you want to verify submissions in."
)
embed.add_field(
name="?register highscore",
value="Use in channel you want to show highscore messages in.",
)
embed.add_field(
name="?register submission",
value="Use in channel you want submission messages to be send it.",
)
embed.add_field(name="?register help", value="Use to show this message.")
await ctx.reply(embed=embed)
# 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(RegisterVerificationChannel(bot))
@@ -1,35 +0,0 @@
"""
Bot command to register the highscores channel
"""
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
from utilities.data_storage import save_highscores_data # pylint: disable=import-error
class RegisterHighscoreChannel(commands.Cog, name="Register HighScore Channel"):
"""Registers Highscore channel"""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="register-highscore")
async def register_highscores(self, ctx: commands.Context):
"""A command which registers current channel as channel to post highscores in.
Usage:
```
?register-highscore (in channel to post highscores in)
```
"""
print(f"Registering {ctx.channel.id} for highscores.")
# respond to the message
await ctx.send(f"Registered <#{ctx.channel.id}> as channel to post highscores in.")
highscores_data["highscore_channel_id"] = ctx.channel.id
save_highscores_data(highscores_data)
# 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 functon to bot"""
bot.add_cog(RegisterHighscoreChannel(bot))
@@ -1,35 +0,0 @@
"""
Bot command to register the verification channel
"""
from nextcord.ext import commands
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 Verify Channel"):
"""Registers Verification channel"""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="register-verify")
async def register_highscores(self, ctx: commands.Context):
"""A command which registers current channel as channel to post verifications in.
Usage:
```
?register-verify (in channel to post highscores in)
```
"""
print(f"Registering {ctx.channel.id} for verification.")
# respond to the message
await ctx.send(f"Registered <#{ctx.channel.id}> as channel to post verifications in.")
highscores_data["verification_channel_id"] = ctx.channel.id
save_highscores_data(highscores_data)
# 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 functon to bot"""
bot.add_cog(RegisterVerificationChannel(bot))
+8 -6
View File
@@ -3,8 +3,8 @@ Bot command to submit a score
"""
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
from utilities.utils import submit_score # pylint: disable=import-error
from .verification_view import VerificationView
@@ -26,13 +26,15 @@ class Score(commands.Cog, name="Score"):
Usage:
```
?score "Boss Name" "Category" "score"
This command still works if you prefer to use it over the ui.
```
"""
verification_channel_id = highscores_data["verification_channel_id"]
channel = self.bot.get_channel(verification_channel_id)
guild_data = highscores_data[str(ctx.guild.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. \
Contact server admins."
error_response = "Registered Verification channel does not exist or was never registered."
error_response += "Contact server admins."
assert channel is not None, await ctx.send(error_response)
verification_string = f"{ctx.author.display_name}:{boss_name}:{category}:{score}"
@@ -47,5 +49,5 @@ class Score(commands.Cog, name="Score"):
# 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 functon to bot"""
"""Adds function to bot"""
bot.add_cog(Score(bot))
+6 -2
View File
@@ -10,7 +10,9 @@ class VerificationView(nextcord.ui.View):
def __init__(self):
super().__init__(timeout=None)
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve")
@nextcord.ui.button(
label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-score-command"
)
async def approve_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
@@ -28,7 +30,9 @@ class VerificationView(nextcord.ui.View):
files=[await attch.to_file() for attch in interaction.message.attachments],
)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny")
@nextcord.ui.button(
label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-score-command"
)
async def deny_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
+28
View File
@@ -0,0 +1,28 @@
"""
Cog for creating submission process.
"""
from nextcord.ext import commands
from .submission_views import SubmissionCreateButton
from .verification_view import VerificationView
class SubmissionCog(commands.Cog, name="Submission Cog"):
"""Constructs submission method."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""Called when cog is loaded"""
self.bot.add_view(SubmissionCreateButton(self.bot))
self.bot.add_view(VerificationView())
# 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(SubmissionCog(bot))
+276
View File
@@ -0,0 +1,276 @@
"""Views used for submission process."""
from typing import Optional
from enum import Enum
import nextcord
from nextcord import Embed
from nextcord.ext import commands
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 .verification_view import VerificationView
class SubmissionState(Enum):
"""Enum for which state a submission is in"""
CATEGORY = 1
SCORE = 2
PROOF = 3
NAME = 4
SUBMIT = 5
VERIFY = 6
def get_submission_embed(userid: int) -> nextcord.Embed:
"""Creates an embed based off of the submission state and fields filled out."""
usersubmission: dict = submission_objects[userid]
state: SubmissionState = usersubmission["submission_state"]
if state is SubmissionState.CATEGORY:
description = "Step 1: Choose your category!"
elif state is SubmissionState.SCORE:
description = "Step 2: Input your score!"
elif state is SubmissionState.PROOF:
description = "Step 3: Submit your proof!"
elif state is SubmissionState.NAME:
description = "Step 4 (Optional): Change name on submission! (proceed to next if skipping)"
elif state is SubmissionState.SUBMIT:
description = "Step 5: Submit if everything looks correct!"
elif state is SubmissionState.VERIFY:
description = "Review fields and accept."
embed = Embed(title="Highscores Submission", description=description)
embed.add_field(name="User Name", value=usersubmission["username"], inline=False)
embed.add_field(name="Boss", value=usersubmission["boss_name"], inline=False)
if usersubmission["category"]:
embed.add_field(name="Category", value=usersubmission["category"], inline=False)
if usersubmission["score"]:
embed.add_field(name="Score", value=usersubmission["score"], inline=False)
return embed
def category_select_options(guild_id: int, boss_name: str) -> "list[nextcord.SelectOption]":
"""Returns list of categories for a given boss."""
options: "list[nextcord.SelectOption]" = []
category_list = highscores_data.get_table_categories(guild_id, boss_name)
for category in category_list:
options.append(nextcord.SelectOption(label=category))
return options
############ Items used in submission process ############
class SubmissionDropdown(nextcord.ui.Select):
"""Defines dropdown menu for submission"""
def __init__(
self,
bot: commands.Bot,
options: "list[nextcord.SelectOption]",
):
super().__init__(
placeholder="Choose submission category.", min_values=1, max_values=1, options=options
)
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
submission_objects[interaction.user.id]["category"] = self.values[0]
embed = get_submission_embed(interaction.user.id)
await interaction.message.edit(
embed=embed,
)
class BackButton(nextcord.ui.Button):
"""Defines view for back button."""
def __init__(self, bot: commands.Bot):
super().__init__(label="Back", style=nextcord.ButtonStyle.primary, custom_id="back")
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
state: SubmissionState = submission_objects[interaction.user.id]["submission_state"]
state = SubmissionState(state.value - 1)
submission_objects[interaction.user.id]["submission_state"] = state
embed = get_submission_embed(interaction.user.id)
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
class CancelButton(nextcord.ui.Button):
"""Deletes message to cancel interaction.."""
def __init__(self):
super().__init__(label="Cancel", style=nextcord.ButtonStyle.red, custom_id="cancel")
async def callback(self, interaction: nextcord.Interaction):
submission_objects.pop(interaction.user.id)
await interaction.message.delete()
class NextButton(nextcord.ui.Button):
"""Defines view for next button."""
def __init__(self, bot: commands.Bot):
super().__init__(label="Next", style=nextcord.ButtonStyle.primary, custom_id="next")
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
# check if fields have been filled out for each state
state: SubmissionState = submission_objects[interaction.user.id]["submission_state"]
if (
state == SubmissionState.CATEGORY
and not submission_objects[interaction.user.id]["category"]
):
await interaction.send("Please choose a category.", ephemeral=True)
elif (
state == SubmissionState.SCORE and not submission_objects[interaction.user.id]["score"]
):
await interaction.send("Please send a score.", ephemeral=True)
elif state == SubmissionState.PROOF and not len(interaction.message.attachments) > 0:
await interaction.send("Please attach picture for proof.", ephemeral=True)
else:
state = SubmissionState(state.value + 1)
submission_objects[interaction.user.id]["submission_state"] = state
embed = get_submission_embed(interaction.user.id)
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
class SubmitButton(nextcord.ui.Button):
"""Defines button to submit a user's score."""
def __init__(self, bot: commands.Bot):
super().__init__(label="Submit", style=nextcord.ButtonStyle.green, custom_id="submit")
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
verification_channel_id = highscores_data.get_guild_verification_channel(
interaction.guild_id
)
channel = interaction.guild.get_channel(verification_channel_id)
error_response = "Registered Verification channel does not exist or was never registered. \
Contact server admins."
assert channel is not None, await channel.send(error_response)
submission_objects[interaction.user.id]["submission_state"] = SubmissionState.VERIFY
embed = get_submission_embed(interaction.user.id)
await channel.send(
files=[await attch.to_file() for attch in interaction.message.attachments],
view=VerificationView(),
embed=embed,
content=f"Submission from {interaction.user.display_name}",
)
await interaction.message.edit(content=f"<@{interaction.user.id}> Submitted!", view=None)
submission_objects.pop(interaction.user.id)
############ Submission Views ############
class SubmissionCreateButton(nextcord.ui.View):
"""Defines view for score submission button."""
def __init__(self, bot: commands.Bot):
super().__init__(timeout=None)
self._bot = bot
@nextcord.ui.button(
label="Submit", style=nextcord.ButtonStyle.primary, custom_id="submitcreate"
)
async def submitcreate_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for score submission."""
# Don't allow a user to start a new submission if one already exists
if interaction.user.id in submission_objects:
await interaction.send("Please finish in progress submission first.", ephemeral=True)
return
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.
# submitting with dummy message_id until message created
# split username on pipe.
submission_objects[interaction.user.id] = {
"message_id": 0,
"guild_id": interaction.guild_id,
"submission_state": SubmissionState.CATEGORY,
"boss_name": boss_name,
"score": "",
"category": "",
"username": interaction.user.display_name.split("|")[0][
: highscores_data.get_guild_username_length(interaction.guild_id)
],
}
embed = get_submission_embed(interaction.user.id)
submission_message = await channel.send(
f"<@{interaction.user.id}>",
view=SubmissionView(
self._bot,
interaction.user.id,
),
embed=embed,
)
# update the message_id once its created
submission_objects[interaction.user.id]["message_id"] = submission_message.id
class SubmissionView(nextcord.ui.View):
"""Defines view submitting. Adjusts to all states."""
def __init__(
self,
bot: commands.Bot,
submission_user: int,
*,
timeout: Optional[float] = 300,
):
super().__init__(timeout=timeout)
self._submission_user = submission_user
self._bot = bot
self._state = submission_objects[self._submission_user]["submission_state"]
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
if self._state is SubmissionState.CATEGORY:
self.add_item(
SubmissionDropdown(bot, category_select_options(self._guild_id, self._boss_name))
)
self.add_item(CancelButton())
self.add_item(NextButton(self._bot))
elif (
self._state is SubmissionState.SCORE
or self._state is SubmissionState.PROOF
or self._state is SubmissionState.NAME
):
self.add_item(BackButton(self._bot))
self.add_item(NextButton(self._bot))
elif self._state is SubmissionState.SUBMIT:
self.add_item(BackButton(self._bot))
self.add_item(SubmitButton(self._bot))
async def interaction_check(self, interaction: nextcord.Interaction) -> bool:
check = self._submission_user == interaction.user.id
if not check:
await interaction.send("This is not your submission.", ephemeral=True)
return check
async def on_timeout(self) -> None:
# remove user from submission objects after time out. Allows user to start new submission.
submission_objects.pop(self._submission_user)
+46
View File
@@ -0,0 +1,46 @@
"""View for verification buttons"""
import nextcord
from utilities.utils import submit_score # pylint: disable=import-error
class VerificationView(nextcord.ui.View):
"""Defines views for verifying score submissions"""
def __init__(self):
super().__init__(timeout=None)
@nextcord.ui.button(
label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-button-submission"
)
async def deny_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for denying score submission."""
await interaction.message.delete()
@nextcord.ui.button(
label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission"
)
async def approve_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for approving score submission."""
content = interaction.message.embeds[0].to_dict()
for field in content["fields"]:
if field["name"] == "Boss":
boss_name = field["value"]
elif field["name"] == "Category":
category = field["value"]
elif field["name"] == "Score":
score = field["value"]
elif field["name"] == "User Name":
user = field["value"]
try:
await submit_score(interaction, user, boss_name, category, score)
await interaction.message.edit(f"Approved by <@{interaction.user.id}>.", view=None)
except UnboundLocalError:
await interaction.send("Instruct user to submit with all fields.")
await interaction.message.edit(view=None)
+335
View File
@@ -0,0 +1,335 @@
{
"tables": {
"The Ambassador": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Araxxi": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Arch-Glacor": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Enrage",
"Spooned Log"
],
"Astellarn": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"The Barrows Brothers": [
"Kill Count",
"Spooned Log"
],
"The Barrows: Rise of the Six": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Beastmaster Durzag": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Black Stone Dragon": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Chaos Elemental": [
"Kill Count",
"Spooned Log"
],
"Commander Zilyana": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Corporeal Beast": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Crassian Leviathan": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Croesus": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Dagannoth Kings": [
"Kill Count",
"Spooned Log"
],
"General Graardor": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Giant Mole": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Gregorovic": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Har-Aken": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Helwyr": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Kalphite King": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Kalphite Queen": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Kerapac": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"King Black Dragon": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Kree'arra": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"K'ril Tsutsaroth": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Legiones": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"The Magister": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Masuta the Ascended": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Nex": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Nex - Angel of Death": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Queen Black Dragon": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Raksha": [
"Solo Kill Count",
"Solo Kill Time",
"Duo Kill Count",
"Duo Kill Time",
"Spooned Log"
],
"Rex Matriarchs": [
"Kill Count",
"Spooned Log"
],
"The Sanctum Guardian": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Seiryu the Azure Serpent": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Solak": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Taraket the Necromancer": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Telos": [
"Kill Count",
"Kill Time",
"Enrage",
"Spooned Log"
],
"The Twin Furies": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"TzKal-Zuk": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"TzTok-Jad": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Verak Lith": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Vindicta & Gorvek": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Vorago": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Yakamaru": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Zamorak, Lord of Chaos (solo)": [
"Kill Count",
"Kill Time",
"Enrage",
"Spooned Log"
],
"Zamorak, Lord of Chaos (group)": [
"Kill Count",
"Kill Time",
"Enrage"
]
},
"categories": {
"Duo Kill Count": {
"ascending": false,
"is_time_record": false
},
"Duo Kill Time": {
"ascending": true,
"is_time_record": true
},
"Enrage": {
"ascending": false,
"is_time_record": false
},
"Group Kill Count": {
"ascending": false,
"is_time_record": false
},
"Group Kill Time": {
"ascending": true,
"is_time_record": true
},
"Hardmode Kill Count": {
"ascending": false,
"is_time_record": false
},
"Hardmode Kill Time": {
"ascending": true,
"is_time_record": true
},
"Kill Count": {
"ascending": false,
"is_time_record": false
},
"Kill Time": {
"ascending": 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": {
"ascending": true,
"is_time_record": false
}
},
"highscore_size": 3
}
+18 -11
View File
@@ -4,13 +4,13 @@ main file for pvm-highscores bot
import logging
import os
import sys
import nextcord
from nextcord.ext import commands
import config
from utilities.data_storage import open_highscores_config
from utilities.data_storage import open_highscores_data
from utilities.highscores_data import HighscoresData
logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG)
@@ -19,18 +19,31 @@ handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(me
logger.addHandler(handler)
highscores_config = open_highscores_config()
highscores_data = open_highscores_data()
highscores_data: HighscoresData = HighscoresData()
highscores_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 = {}
def main():
"""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
# These need to be enabled in the developer portal as well
intents = nextcord.Intents.default()
# Required in order to read messages (eg. prefix commands)
intents.message_content = True
intents.message_content = True # pylint: disable=assigning-non-slot
# To enable the guilds priveleged intent:
# intents.guilds = True
@@ -38,15 +51,9 @@ def main():
# To enable the members priveleged intent:
# 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(
commands.when_mentioned_or(config.BOT_PREFIX),
intents=intents,
activity=activity,
)
# Get the modules of all cogs whose directory structure is ./cogs/<module_name>
-202
View File
@@ -1,202 +0,0 @@
{
"bosses": [
{
"boss": "The Ambassador",
"hardmode": true
},
{
"boss": "Araxxi",
"hardmode": false
},
{
"boss": "Arch-Glacor",
"hardmode": true
},
{
"boss": "Astellarn",
"hardmode": true
},
{
"boss": "The Barrows Brothers",
"hardmode": false
},
{
"boss": "The Barrows: Rise of the Six",
"hardmode": false
},
{
"boss": "Beastmaster Durzag",
"hardmode": false
},
{
"boss": "Black Stone Dragon",
"hardmode": true
},
{
"boss": "Chaos Elemental",
"hardmode": false
},
{
"boss": "Commander Zilyana",
"hardmode": true
},
{
"boss": "Corporeal Beast",
"hardmode": false
},
{
"boss": "Crassian Leviathan",
"hardmode": true
},
{
"boss": "Croesus",
"hardmode": false
},
{
"boss": "Dagannoth Kings",
"hardmode": false
},
{
"boss": "General Graardor",
"hardmode": true
},
{
"boss": "Giant Mole",
"hardmode": true
},
{
"boss": "Gregorovic",
"hardmode": true
},
{
"boss": "Har-Aken",
"hardmode": false
},
{
"boss": "Helwyr",
"hardmode": true
},
{
"boss": "Kalphite King",
"hardmode": false
},
{
"boss": "Kalphite Queen",
"hardmode": false
},
{
"boss": "Kerapac",
"hardmode": true
},
{
"boss": "King Black Dragon",
"hardmode": false
},
{
"boss": "Kree'arra",
"hardmode": true
},
{
"boss": "K'ril Tsutsaroth",
"hardmode": true
},
{
"boss": "Legiones",
"hardmode": false
},
{
"boss": "The Magister",
"hardmode": false
},
{
"boss": "Masuta the Ascended",
"hardmode": true
},
{
"boss": "Nex",
"hardmode": false
},
{
"boss": "Nex - Angel of Death",
"hardmode": false
},
{
"boss": "Queen Black Dragon",
"hardmode": false
},
{
"boss": "Raksha",
"hardmode": true
},
{
"boss": "Rex Matriarchs",
"hardmode": false
},
{
"boss": "The Sanctum Guardian",
"hardmode": true
},
{
"boss": "Seiryu the Azure Serpent",
"hardmode": true
},
{
"boss": "Solak",
"hardmode": false
},
{
"boss": "Taraket the Necromancer",
"hardmode": true
},
{
"boss": "Telos",
"hardmode": false
},
{
"boss": "The Twin Furies",
"hardmode": true
},
{
"boss": "TzKal-Zuk",
"hardmode": true
},
{
"boss": "TzTok-Jad",
"hardmode": false
},
{
"boss": "Verak Lith",
"hardmode": true
},
{
"boss": "Vindicta & Gorvek",
"hardmode": true
},
{
"boss": "Vorago",
"hardmode": true
},
{
"boss": "Yakamaru",
"hardmode": false
}
],
"categories": {
"Kill Count": {
"hardmode": true,
"ascending": false,
"is_time_record": false
},
"Kill Time": {
"hardmode": true,
"ascending": true,
"is_time_record": true
},
"Spooned Log": {
"hardmode": false,
"ascending": true,
"is_time_record": false
}
},
"highscore_size": 3
}
-51
View File
@@ -1,51 +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"
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}
config = open_highscores_config()
for boss in config["bosses"]:
data[boss["boss"]] = {"message_id": 0}
boss_categories = {}
for key, value in config["categories"].items():
rank_list = []
boss_categories[key] = rank_list
if boss["hardmode"] and value["hardmode"]:
rank_list = []
boss_categories["Hardmode " + key] = rank_list
data[boss["boss"]]["categories"] = boss_categories
return data
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))
+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))
+54 -54
View File
@@ -3,58 +3,87 @@ Contains various utlity functions for highscores bot
"""
import nextcord
from highscores import highscores_config # 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 highscores import highscores_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
@param boss: the dictionary object for the boss
@param guild_id: the guild id the message is for
@return the formatted string
"""
ret_string = f"**{boss_name}**\n```"
boss_data = highscores_data[boss_name]
# Gather all categories and add to string
for key, value in boss_data["categories"].items():
ret_string += f"{key}".ljust(30, " ")
for category_name in highscores_data.get_table_categories(guild_id, boss_name):
ret_string += f"{category_name}".ljust(26, " ")
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
for i in range(highscore_size):
for key, value in boss_data["categories"].items():
if len(value) > i:
score = value[i]
ret_string += f"{i+1}: {score[0]} - {score[1]}".ljust(30, " ")
for category_name, category_data in highscores_data.get_categories_data(
guild_id, boss_name
):
if len(category_data) > i:
# pad username to max length.
username: str = list(category_data.keys())[i].ljust(
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])
# pad score to correct length
# MM:SS.ms is 7 characters long
if highscores_data.get_config_time_record(category_name):
# if there are single digit minutes, add a space for formatting.
if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2:
score = " " + score
else:
score = score.rjust(7, " ")
ret_string += f"{i+1}: {username} - {score}".ljust(26, " ")
else:
ret_string += f"{i+1}: submit your score".ljust(30, " ")
ret_string += f"{i+1}: submit your score".ljust(26, " ")
ret_string += "\n"
return ret_string + "```"
async def send_highscore_message(channel, boss_name: str):
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
creating and sending/editing the message
@param channel: the channel to send the message in/search for message id
@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:
message = await channel.fetch_message(message_id)
message: nextcord.Message = await channel.fetch_message(message_id)
await message.edit(content=highscore_string)
except nextcord.NotFound:
# message doesn't exist.
message = await channel.send(highscore_string)
message: nextcord.Message = await channel.send(highscore_string)
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
# get guild id from channel
# 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
async def submit_score(
@@ -65,47 +94,18 @@ async def submit_score(
@param boss: the boss to add score to
@param category: the category to add score to
@param user: user who submitted score
@param score: score (int) to add
@param score: score to add
"""
# TODO update user submission to not have dupe users
# create score tuple for either time or int
# define sort index
if highscores_config["categories"][category]["is_time_record"]:
minutes = score.split(":")[0]
seconds = score.split(":")[1]
score_seconds = int(minutes) * 60 + int(seconds)
score_tuple = (user, score, score_seconds)
sort_index = 2
else:
score_tuple = (user, int(score))
sort_index = 1
scores = highscores_data[boss_name]["categories"][category]
scores.append(score_tuple)
# sort scores.
scores.sort(
key=lambda x: x[sort_index],
reverse=not highscores_config["categories"][category]["ascending"],
)
# enforce highscore size
while len(scores) > highscores_config["highscore_size"]:
scores.pop()
highscores_data[boss_name]["categories"][category] = scores
highscores_data.add_score(interaction.guild_id, boss_name, category, user, score)
# 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}")
channel = interaction.guild.get_channel(highscore_channel_id)
error_response = 'Registered Highscores channel does not exist or was never registered. \
Register with "?register" command.'
assert channel is not None, await interaction.send(error_response)
error_response = "Registered Highscores channel does not exist or was never registered."
error_response += 'Register with "?register" command.'
assert channel is not None, await interaction.send(error_response, ephemeral=True)
await send_highscore_message(channel, boss_name)
# save data
save_highscores_data(highscores_data)
await send_highscore_message(channel, boss_name, interaction.guild_id)