diff --git a/BlueSkyUtils.py b/BlueSkyUtils.py index ad1a79e..f030e3d 100644 --- a/BlueSkyUtils.py +++ b/BlueSkyUtils.py @@ -35,7 +35,7 @@ def get_api_key(user, password): body=bytes(json.dumps(post_data), encoding="utf-8"), ) api_key = json.loads(api_key.data) - return api_key["accessJwt"] + return api_key["accessJwt"] # type: ignore def upload_blob(img_data, jwt, mime_type): http = urllib3.PoolManager() @@ -46,7 +46,7 @@ def upload_blob(img_data, jwt, mime_type): headers={"Content-Type": mime_type, "Authorization": f"Bearer {jwt}"}, ) blob_request = json.loads(blob_request.data) - return blob_request["blob"] + return blob_request["blob"] # type: ignore def send_post(user, jwt, text, blob, image_alt): http = urllib3.PoolManager() diff --git a/Constants.py b/Constants.py index dbde40c..7347bbd 100644 --- a/Constants.py +++ b/Constants.py @@ -1,3 +1,5 @@ +from enum import Enum +from typing import Dict, List PORTRAIT_SIZE = 0 PORTRAIT_TILE_X = 0 @@ -8,18 +10,18 @@ CROP_PORTRAITS = True -COMPLETION_EMOTIONS = [] +COMPLETION_EMOTIONS: List[List[int]] = [] -EMOTIONS = [] +EMOTIONS: List[str] = [] -ACTION_MAP = { } +ACTION_MAP: Dict[int, str] = { } -COMPLETION_ACTIONS = [] +COMPLETION_ACTIONS: List[List[int]] = [] -ACTIONS = [] -DUNGEON_ACTIONS = [] -STARTER_ACTIONS = [] +ACTIONS: List[str] = [] +DUNGEON_ACTIONS: List[str] = [] +STARTER_ACTIONS: List[str] = [] DIRECTIONS = [ "Down", "DownRight", @@ -33,4 +35,24 @@ MULTI_SHEET_XML = "AnimData.xml" CREDIT_TXT = "credits.txt" -PHASES = [ "\u26AA incomplete", "\u2705 available", "\u2B50 fully featured" ] \ No newline at end of file +PHASES = [ "\u26AA incomplete", "\u2705 available", "\u2B50 fully featured" ] + +MESSAGE_BOUNTIES_DISABLED = "Bounties are disabled for this instance of SpriteBot" + +class PermissionLevel(Enum): + EVERYONE = 0 + STAFF = 1 + ADMIN = 2 + + def canPerformAction(self, required_level) -> bool: + return required_level.value <= self.value + + def displayname(self) -> str: + if self == self.EVERYONE: + return "everyone" + elif self == self.STAFF: + return "staff" + elif self == self.ADMIN: + return "admin" + else: + return "unknown" \ No newline at end of file diff --git a/SpriteBot.py b/SpriteBot.py index cfa8993..195b842 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Dict, Any, Optional, Tuple import os @@ -25,9 +25,33 @@ from commands.ListRessource import ListRessource from commands.QueryRessourceCredit import QueryRessourceCredit from commands.DeleteRessourceCredit import DeleteRessourceCredit +from commands.ListBounties import ListBounties +from commands.ClearCache import ClearCache from commands.GetProfile import GetProfile - -from Constants import PHASES +from commands.SetProfile import SetProfile +from commands.GetAbsenteeProfiles import GetAbsenteeProfiles +from commands.RenameNode import RenameNode +from commands.ReplaceRessource import ReplaceRessource +from commands.MoveNode import MoveNode +from commands.MoveRessource import MoveRessource +from commands.SetRessourceCredit import SetRessourceCredit +from commands.AddRessourceCredit import AddRessourceCredit +from commands.AddNode import AddNode +from commands.AddGender import AddGender +from commands.DeleteGender import DeleteGender +from commands.DeleteNode import DeleteNode +from commands.TransferProfile import TransferProfile +from commands.SetRessourceCompletion import SetRessourceCompletion +from commands.SetRessourceLock import SetRessourceLock +from commands.SetNodeCanon import SetNodeCanon +from commands.SetNeedNode import SetNeedNode +from commands.ForcePush import ForcePush +from commands.Rescan import Rescan +from commands.Update import Update +from commands.Shutdown import Shutdown + +from Constants import PHASES, PermissionLevel, MESSAGE_BOUNTIES_DISABLED +from utils import unpack_optional import psutil # Housekeeping for login information @@ -39,8 +63,6 @@ SPRITE_CONFIG_FILE_PATH = 'sprite_config.json' TRACKER_FILE_PATH = 'tracker.json' -MESSAGE_BOUNTIES_DISABLED = "Bounties are disabled for this instance of SpriteBot" - scdir = os.path.dirname(os.path.abspath(__file__)) parser = argparse.ArgumentParser() @@ -99,18 +121,17 @@ def __init__(self, main_dict=None): self.update_ch = 0 self.update_msg = 0 self.use_bounties = False - self.servers = {} + self.servers: Dict[str, BotServer] = {} if main_dict is None: return for key in main_dict: - self.__dict__[key] = main_dict[key] - - sub_dict = {} - for key in self.servers: - sub_dict[key] = BotServer(self.servers[key]) - self.servers = sub_dict + if key == "servers": + for server_key in main_dict[key]: + self.servers[server_key] = BotServer(main_dict[key][server_key]) + else: + self.__dict__[key] = main_dict[key] def getDict(self): node_dict = { } @@ -175,7 +196,7 @@ def __init__(self, in_path, client): # tracking data from the content folder with open(os.path.join(self.config.path, TRACKER_FILE_PATH)) as f: new_tracker = json.load(f) - self.tracker = { } + self.tracker: Dict[str, TrackerUtils.TrackerNode] = { } for species_idx in new_tracker: self.tracker[species_idx] = TrackerUtils.TrackerNode(new_tracker[species_idx]) self.names = TrackerUtils.loadNameFile(os.path.join(self.path, NAME_FILE_PATH)) @@ -204,6 +225,7 @@ def __init__(self, in_path, client): # register commands self.commands = [ + # everyone QueryRessourceStatus(self, "portrait", False), QueryRessourceStatus(self, "portrait", True), QueryRessourceStatus(self, "sprite", False), @@ -218,7 +240,49 @@ def __init__(self, in_path, client): QueryRessourceCredit(self, "sprite", True), DeleteRessourceCredit(self, "portrait"), DeleteRessourceCredit(self, "sprite"), - GetProfile(self) + GetProfile(self), + SetProfile(self, False), + GetAbsenteeProfiles(self), + ListBounties(self), + + # staff + AddNode(self), + AddGender(self), + DeleteGender(self), + DeleteNode(self), + ClearCache(self), + SetProfile(self, True), + TransferProfile(self), + RenameNode(self), + ReplaceRessource(self, "portrait"), + ReplaceRessource(self, "sprite"), + MoveNode(self), + MoveRessource(self, "portrait"), + MoveRessource(self, "sprite"), + SetRessourceCredit(self, "portrait"), + SetRessourceCredit(self, "sprite"), + AddRessourceCredit(self, "portrait"), + AddRessourceCredit(self, "sprite"), + SetNeedNode(self, True), + SetNeedNode(self, False), + SetRessourceCompletion(self, "portrait", TrackerUtils.PHASE_INCOMPLETE), + SetRessourceCompletion(self, "portrait", TrackerUtils.PHASE_EXISTS), + SetRessourceCompletion(self, "portrait", TrackerUtils.PHASE_FULL), + SetRessourceCompletion(self, "sprite", TrackerUtils.PHASE_INCOMPLETE), + SetRessourceCompletion(self, "sprite", TrackerUtils.PHASE_EXISTS), + SetRessourceCompletion(self, "sprite", TrackerUtils.PHASE_FULL), + + # admin + SetRessourceLock(self, "portrait", True), + SetRessourceLock(self, "portrait", False), + SetRessourceLock(self, "sprite", True), + SetRessourceLock(self, "sprite", False), + SetNodeCanon(self, True), + SetNodeCanon(self, False), + ForcePush(self), + Rescan(self), + Update(self), + Shutdown(self) ] self.writeLog("Startup Memory: {0}".format(psutil.Process().memory_info().rss)) @@ -226,7 +290,7 @@ def __init__(self, in_path, client): def generateCreditCompilation(self): - credit_dict = {} + credit_dict: Dict[str, TrackerUtils.CreditCompileEntry] = {} over_dict = TrackerUtils.initSubNode("", True) over_dict.subgroups = self.tracker TrackerUtils.updateCompilationStats(self.names, over_dict, os.path.join(self.config.path, "sprite"), "sprite", [], credit_dict) @@ -270,26 +334,6 @@ async def gitPush(self): origin.push() self.commits = 0 - async def updateBot(self, msg): - resp_ch = self.getChatChannel(msg.guild.id) - resp = await resp_ch.send("Pulling from repo...") - # update self - bot_repo = git.Repo(scdir) - origin = bot_repo.remotes.origin - origin.pull() - await resp.edit(content="Update complete! Bot will restart.") - self.need_restart = True - self.config.update_ch = resp_ch.id - self.config.update_msg = resp.id - self.saveConfig() - await self.client.close() - - async def shutdown(self, msg): - resp_ch = self.getChatChannel(msg.guild.id) - await resp_ch.send("Shutting down.") - self.saveConfig() - await self.client.close() - async def checkRestarted(self): if self.config.update_ch != 0 and self.config.update_msg != 0: msg = await self.client.get_channel(self.config.update_ch).fetch_message(self.config.update_msg) @@ -387,7 +431,7 @@ def getPostsFromDict(self, include_sprite, include_portrait, include_credit, tra - def getBountiesFromDict(self, asset_type, tracker_dict, entries, indices): + def getBountiesFromDict(self, asset_type, tracker_dict, entries: List[Tuple[int, str, str, int]], indices): if tracker_dict.name != "": new_titles = TrackerUtils.getIdxName(self.tracker, indices) dexnum = int(indices[0]) @@ -411,16 +455,16 @@ def getBountiesFromDict(self, asset_type, tracker_dict, entries, indices): self.getBountiesFromDict(asset_type, tracker_dict.subgroups[sub_dict], entries, indices + [sub_dict]) - async def isAuthorized(self, user, guild): - + async def getUserPermission(self, user, guild): + """Get a user permission level""" if user.id == self.client.user.id: - return False + return PermissionLevel.EVERYONE if user.id == self.config.root: - return True + return PermissionLevel.ADMIN guild_id_str = str(guild.id) if self.config.servers[guild_id_str].approval == 0: - return False + return PermissionLevel.EVERYONE approve_role = guild.get_role(self.config.servers[guild_id_str].approval) @@ -430,10 +474,10 @@ async def isAuthorized(self, user, guild): user_member = None if user_member is None: - return False + return PermissionLevel.EVERYONE if approve_role in user_member.roles: - return True - return False + return PermissionLevel.STAFF + return PermissionLevel.EVERYONE async def generateLink(self, file_data, filename): # file_data is a file-like object to post with @@ -659,7 +703,7 @@ async def postStagedSubmission(self, channel, cmd_str, formatted_content, full_i reduced_img = SpriteUtils.simple_quant_portraits(overcolor_img, overpalette) reduced_file = io.BytesIO() - reduced_img.save(reduced_file, format='PNG') + reduced_img.save(reduced_file, format='PNG') # type: ignore reduced_file.seek(0) send_files.append(discord.File(reduced_file, return_name.replace('.png', '_reduced.png'))) add_msg += "\nReduced Color Preview included." @@ -697,7 +741,12 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): await msg.delete() return + assert full_idx is not None + assert asset_type is not None + assert recolor is not None + chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) + assert chosen_node is not None chosen_path = TrackerUtils.getDirFromIdx(self.config.path, asset_type, full_idx) review_thread = await self.retrieveDiscussion(full_idx, chosen_node, asset_type, msg.guild.id) @@ -803,7 +852,7 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): orig_node = chosen_node if is_shiny: orig_idx = TrackerUtils.createShinyIdx(full_idx, False) - orig_node = TrackerUtils.getNodeFromIdx(self.tracker, orig_idx, 0) + orig_node = unpack_optional(TrackerUtils.getNodeFromIdx(self.tracker, orig_idx, 0)) prev_completion_file = TrackerUtils.getCurrentCompletion(orig_node, chosen_node, asset_type) @@ -1017,8 +1066,8 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): auto_diffs = [] try: if asset_type == "sprite": - orig_idx = TrackerUtils.createShinyIdx(full_idx, False) - orig_node = TrackerUtils.getNodeFromIdx(self.tracker, orig_idx, 0) + orig_idx = unpack_optional(TrackerUtils.createShinyIdx(full_idx, False)) + orig_node = unpack_optional(TrackerUtils.getNodeFromIdx(self.tracker, orig_idx, 0)) orig_group_link = await self.retrieveLinkMsg(orig_idx, orig_node, asset_type, False) orig_zip_group = SpriteUtils.getLinkZipGroup(orig_group_link) @@ -1030,7 +1079,7 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): return # post it as a staged submission - return_name = "{0}-{1}{2}".format(asset_type + "_recolor", "-".join(shiny_idx), ".png") + return_name = "{0}-{1}{2}".format(asset_type + "_recolor", "-".join(shiny_idx), ".png") # type: ignore auto_recolor_file = io.BytesIO() auto_recolor_img.save(auto_recolor_file, format='PNG') auto_recolor_file.seek(0) @@ -1077,8 +1126,11 @@ async def submissionDeclined(self, msg, orig_sender, declines): await self.getChatChannel(msg.guild.id).send(orig_sender + " " + "Removed unknown file: {0}".format(file_name)) await msg.delete() return + assert full_idx is not None + assert asset_type is not None + assert recolor is not None - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) + chosen_node = unpack_optional(TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0)) review_thread = await self.retrieveDiscussion(full_idx, chosen_node, asset_type, msg.guild.id) # change the status of the sprite @@ -1144,7 +1196,7 @@ async def pollSubmission(self, msg): ss = reaction else: async for user in reaction.users(): - if await self.isAuthorized(user, msg.guild): + if await self.getUserPermission(user, msg.guild).canPerformAction(PermissionLevel.STAFF): pass else: remove_users.append((reaction, user)) @@ -1179,14 +1231,14 @@ async def pollSubmission(self, msg): if deleting and user_author_id == orig_author: approve.append(user.id) consent = True - elif await self.isAuthorized(user, msg.guild): + elif (await self.getUserPermission(user, msg.guild)).canPerformAction(PermissionLevel.STAFF): approve.append(user.id) elif user.id != self.client.user.id: remove_users.append((cks, user)) if ws: async for user in ws.users(): - if await self.isAuthorized(user, msg.guild): + if (await self.getUserPermission(user, msg.guild)).canPerformAction(PermissionLevel.STAFF): warn = True else: remove_users.append((ws, user)) @@ -1194,7 +1246,7 @@ async def pollSubmission(self, msg): if xs: async for user in xs.users(): user_author_id = "<@!{0}>".format(user.id) - if await self.isAuthorized(user, msg.guild) or user.id == orig_sender_id: + if (await self.getUserPermission(user, msg.guild)).canPerformAction(PermissionLevel.STAFF) or user.id == orig_sender_id: decline.append(user.id) elif deleting and user_author_id == orig_author: decline.append(user.id) @@ -1203,6 +1255,9 @@ async def pollSubmission(self, msg): file_name = msg.attachments[0].filename name_valid, full_idx, asset_type, recolor = TrackerUtils.getStatsFromFilename(file_name) + assert full_idx is not None + assert asset_type is not None + assert recolor is not None if len(decline) > 0: await self.submissionDeclined(msg, orig_sender, decline) @@ -1242,14 +1297,20 @@ async def pollSubmission(self, msg): # if the node cant be found, the filepath is invalid if chosen_node is None: name_valid = False - elif not chosen_node.__dict__[asset_type + "_required"]: - # if the node can be found, but it's not required, it's also invalid - name_valid = False + else: + assert asset_type is not None + if not chosen_node.__dict__[asset_type + "_required"]: + # if the node can be found, but it's not required, it's also invalid + name_valid = False if not name_valid: await msg.delete() await self.getChatChannel(msg.guild.id).send(msg.author.mention + " Invalid filename {0}. Do not change the filename from the original name given by !portrait or !sprite .".format(file_name)) return False + + assert full_idx is not None + assert asset_type is not None + assert recolor is not None try: msg_args = parser.parse_args(msg.content.split()) @@ -1511,34 +1572,6 @@ async def retrieveLinkMsg(self, full_idx, chosen_node, asset_type, recolor): self.saveTracker() return new_link - - async def completeSlot(self, msg, name_args, asset_type, phase): - name_seq = [TrackerUtils.sanitizeName(i) for i in name_args] - full_idx = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq, 0) - if full_idx is None: - await msg.channel.send(msg.author.mention + " No such Pokemon.") - return - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) - - phase_str = PHASES[phase] - - # if the node has no credit, fail - if chosen_node.__dict__[asset_type + "_credit"].primary == "" and phase > TrackerUtils.PHASE_INCOMPLETE: - status = TrackerUtils.getStatusEmoji(chosen_node, asset_type) - await msg.channel.send(msg.author.mention + - " {0} #{1:03d}: {2} has no data and cannot be marked {3}.".format(status, int(full_idx[0]), " ".join(name_seq), phase_str)) - return - - # set to complete - chosen_node.__dict__[asset_type + "_complete"] = phase - - status = TrackerUtils.getStatusEmoji(chosen_node, asset_type) - await msg.channel.send(msg.author.mention + " {0} #{1:03d}: {2} marked as {3}.".format(status, int(full_idx[0]), " ".join(name_seq), phase_str)) - - self.saveTracker() - self.changed = True - - async def checkMoveLock(self, full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, asset_type): chosen_path_from = TrackerUtils.getDirFromIdx(self.config.path, asset_type, full_idx_from) @@ -1549,235 +1582,7 @@ async def checkMoveLock(self, full_idx_from, chosen_node_from, full_idx_to, chos elif asset_type == "portrait": chosen_img_to = SpriteUtils.getLinkImg(chosen_img_to_link) SpriteUtils.verifyPortraitLock(chosen_node_from, chosen_path_from, chosen_img_to, False) - - async def moveSlotRecursive(self, msg, name_args): - try: - delim_idx = name_args.index("->") - except: - await msg.channel.send(msg.author.mention + " Command needs to separate the source and destination with `->`.") - return - - name_args_from = name_args[:delim_idx] - name_args_to = name_args[delim_idx+1:] - - name_seq_from = [TrackerUtils.sanitizeName(i) for i in name_args_from] - full_idx_from = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq_from, 0) - if full_idx_from is None: - await msg.channel.send(msg.author.mention + " No such Pokemon specified as source.") - return - if len(full_idx_from) > 2: - await msg.channel.send(msg.author.mention + " Can move only species or form. Source specified more than that.") - return - - name_seq_to = [TrackerUtils.sanitizeName(i) for i in name_args_to] - full_idx_to = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq_to, 0) - if full_idx_to is None: - await msg.channel.send(msg.author.mention + " No such Pokemon specified as destination.") - return - if len(full_idx_to) > 2: - await msg.channel.send(msg.author.mention + " Can move only species or form. Destination specified more than that.") - return - - chosen_node_from = TrackerUtils.getNodeFromIdx(self.tracker, full_idx_from, 0) - chosen_node_to = TrackerUtils.getNodeFromIdx(self.tracker, full_idx_to, 0) - - if chosen_node_from == chosen_node_to: - await msg.channel.send(msg.author.mention + " Cannot move to the same location.") - return - - explicit_idx_from = full_idx_from.copy() - if len(explicit_idx_from) < 2: - explicit_idx_from.append("0000") - explicit_idx_to = full_idx_to.copy() - if len(explicit_idx_to) < 2: - explicit_idx_to.append("0000") - - explicit_node_from = TrackerUtils.getNodeFromIdx(self.tracker, full_idx_from, 0) - explicit_node_to = TrackerUtils.getNodeFromIdx(self.tracker, full_idx_to, 0) - - # check the main nodes - try: - await self.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, "sprite") - await self.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, "portrait") - except SpriteUtils.SpriteVerifyError as e: - await msg.channel.send(msg.author.mention + " Cannot move the locked Pokemon specified as source:\n{0}".format(e.message)) - return - - try: - await self.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, "sprite") - await self.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, "portrait") - except SpriteUtils.SpriteVerifyError as e: - await msg.channel.send(msg.author.mention + " Cannot move the locked Pokemon specified as destination:\n{0}".format(e.message)) - return - - # check the subnodes - for sub_idx in explicit_node_from.subgroups: - sub_node = explicit_node_from.subgroups[sub_idx] - if TrackerUtils.hasLock(sub_node, "sprite", True) or TrackerUtils.hasLock(sub_node, "portrait", True): - await msg.channel.send(msg.author.mention + " Cannot move the locked subgroup specified as source.") - return - for sub_idx in explicit_node_to.subgroups: - sub_node = explicit_node_to.subgroups[sub_idx] - if TrackerUtils.hasLock(sub_node, "sprite", True) or TrackerUtils.hasLock(sub_node, "portrait", True): - await msg.channel.send(msg.author.mention + " Cannot move the locked subgroup specified as destination.") - return - - # clear caches - TrackerUtils.clearCache(chosen_node_from, True) - TrackerUtils.clearCache(chosen_node_to, True) - - # perform the swap - TrackerUtils.swapFolderPaths(self.config.path, self.tracker, "sprite", full_idx_from, full_idx_to) - TrackerUtils.swapFolderPaths(self.config.path, self.tracker, "portrait", full_idx_from, full_idx_to) - TrackerUtils.swapNodeMiscFeatures(chosen_node_from, chosen_node_to) - - # then, swap the subnodes - TrackerUtils.swapAllSubNodes(self.config.path, self.tracker, explicit_idx_from, explicit_idx_to) - - await msg.channel.send(msg.author.mention + " Swapped {0} with {1}.".format(" ".join(name_seq_from), " ".join(name_seq_to))) - # if the source is empty in sprite and portrait, and its subunits are empty in sprite and portrait - # remind to delete - if not TrackerUtils.isDataPopulated(chosen_node_from): - await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_to))) - if not TrackerUtils.isDataPopulated(chosen_node_to): - await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_from))) - - self.saveTracker() - self.changed = True - - await self.gitCommit("Swapped {0} with {1} recursively".format(" ".join(name_seq_from), " ".join(name_seq_to))) - - - async def replaceSlot(self, msg, name_args, asset_type): - try: - delim_idx = name_args.index("->") - except: - await msg.channel.send(msg.author.mention + " Command needs to separate the source and destination with `->`.") - return - - name_args_from = name_args[:delim_idx] - name_args_to = name_args[delim_idx+1:] - - name_seq_from = [TrackerUtils.sanitizeName(i) for i in name_args_from] - full_idx_from = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq_from, 0) - if full_idx_from is None: - await msg.channel.send(msg.author.mention + " No such Pokemon specified as source.") - return - - name_seq_to = [TrackerUtils.sanitizeName(i) for i in name_args_to] - full_idx_to = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq_to, 0) - if full_idx_to is None: - await msg.channel.send(msg.author.mention + " No such Pokemon specified as destination.") - return - - chosen_node_from = TrackerUtils.getNodeFromIdx(self.tracker, full_idx_from, 0) - chosen_node_to = TrackerUtils.getNodeFromIdx(self.tracker, full_idx_to, 0) - - if chosen_node_from == chosen_node_to: - await msg.channel.send(msg.author.mention + " Cannot move to the same location.") - return - - if not chosen_node_from.__dict__[asset_type + "_required"]: - await msg.channel.send(msg.author.mention + " Cannot move when source {0} is unneeded.".format(asset_type)) - return - if not chosen_node_to.__dict__[asset_type + "_required"]: - await msg.channel.send(msg.author.mention + " Cannot move when destination {0} is unneeded.".format(asset_type)) - return - - try: - await self.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, asset_type) - except SpriteUtils.SpriteVerifyError as e: - await msg.channel.send(msg.author.mention + " Cannot move out the locked Pokemon specified as source:\n{0}".format(e.message)) - return - - try: - await self.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, asset_type) - except SpriteUtils.SpriteVerifyError as e: - await msg.channel.send(msg.author.mention + " Cannot replace the locked Pokemon specified as destination:\n{0}".format(e.message)) - return - - # clear caches - TrackerUtils.clearCache(chosen_node_from, True) - TrackerUtils.clearCache(chosen_node_to, True) - - TrackerUtils.replaceFolderPaths(self.config.path, self.tracker, asset_type, full_idx_from, full_idx_to) - - await msg.channel.send(msg.author.mention + " Replaced {0} with {1}.".format(" ".join(name_seq_to), " ".join(name_seq_from))) - # if the source is empty in sprite and portrait, and its subunits are empty in sprite and portrait - # remind to delete - await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_from))) - - self.saveTracker() - self.changed = True - - await self.gitCommit("Replaced {0} with {1}".format(" ".join(name_seq_to), " ".join(name_seq_from))) - - async def moveSlot(self, msg, name_args, asset_type): - try: - delim_idx = name_args.index("->") - except: - await msg.channel.send(msg.author.mention + " Command needs to separate the source and destination with `->`.") - return - - name_args_from = name_args[:delim_idx] - name_args_to = name_args[delim_idx+1:] - - name_seq_from = [TrackerUtils.sanitizeName(i) for i in name_args_from] - full_idx_from = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq_from, 0) - if full_idx_from is None: - await msg.channel.send(msg.author.mention + " No such Pokemon specified as source.") - return - - name_seq_to = [TrackerUtils.sanitizeName(i) for i in name_args_to] - full_idx_to = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq_to, 0) - if full_idx_to is None: - await msg.channel.send(msg.author.mention + " No such Pokemon specified as destination.") - return - - chosen_node_from = TrackerUtils.getNodeFromIdx(self.tracker, full_idx_from, 0) - chosen_node_to = TrackerUtils.getNodeFromIdx(self.tracker, full_idx_to, 0) - - if chosen_node_from == chosen_node_to: - await msg.channel.send(msg.author.mention + " Cannot move to the same location.") - return - - if not chosen_node_from.__dict__[asset_type + "_required"]: - await msg.channel.send(msg.author.mention + " Cannot move when source {0} is unneeded.".format(asset_type)) - return - if not chosen_node_to.__dict__[asset_type + "_required"]: - await msg.channel.send(msg.author.mention + " Cannot move when destination {0} is unneeded.".format(asset_type)) - return - - try: - await self.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, asset_type) - except SpriteUtils.SpriteVerifyError as e: - await msg.channel.send(msg.author.mention + " Cannot move the locked Pokemon specified as source:\n{0}".format(e.message)) - return - - try: - await self.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, asset_type) - except SpriteUtils.SpriteVerifyError as e: - await msg.channel.send(msg.author.mention + " Cannot move the locked Pokemon specified as destination:\n{0}".format(e.message)) - return - - # clear caches - TrackerUtils.clearCache(chosen_node_from, True) - TrackerUtils.clearCache(chosen_node_to, True) - - TrackerUtils.swapFolderPaths(self.config.path, self.tracker, asset_type, full_idx_from, full_idx_to) - - await msg.channel.send(msg.author.mention + " Swapped {0} with {1}.".format(" ".join(name_seq_from), " ".join(name_seq_to))) - # if the source is empty in sprite and portrait, and its subunits are empty in sprite and portrait - # remind to delete - if not TrackerUtils.isDataPopulated(chosen_node_from): - await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_from))) - if not TrackerUtils.isDataPopulated(chosen_node_to): - await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_to))) - - self.saveTracker() - self.changed = True - - await self.gitCommit("Swapped {0} with {1}".format(" ".join(name_seq_from), " ".join(name_seq_to))) + async def placeBounty(self, msg, name_args, asset_type): if not self.config.use_bounties: @@ -1807,7 +1612,7 @@ async def placeBounty(self, msg, name_args, asset_type): return if self.config.points == 0: - if not await self.isAuthorized(msg.author, msg.guild): + if not (await self.getUserPermission(msg.author, msg.guild)).canPerformAction(PermissionLevel.STAFF): await msg.channel.send(msg.author.mention + " Not authorized.") return else: @@ -1854,27 +1659,6 @@ def check(m): self.saveTracker() self.changed = True - async def setCanon(self, msg, name_args, canon_state): - - name_seq = [TrackerUtils.sanitizeName(i) for i in name_args] - full_idx = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq, 0) - if full_idx is None: - await msg.channel.send(msg.author.mention + " No such Pokemon.") - return - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) - - TrackerUtils.setCanon(chosen_node, canon_state) - - lock_str = "non-" - if canon_state: - lock_str = "" - # set to complete - await msg.channel.send(msg.author.mention + " {0} is now {1}canon.".format(" ".join(name_seq), lock_str)) - - self.saveTracker() - self.changed = True - - async def promote(self, msg, name_args): if not self.config.mastodon and not self.config.bluesky: @@ -1940,96 +1724,6 @@ async def promote(self, msg, name_args): await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) - - async def setLock(self, msg, name_args, asset_type, lock_state): - - name_seq = [TrackerUtils.sanitizeName(i) for i in name_args[:-1]] - full_idx = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq, 0) - if full_idx is None: - await msg.channel.send(msg.author.mention + " No such Pokemon.") - return - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) - - file_name = name_args[-1] - for k in chosen_node.__dict__[asset_type + "_files"]: - if file_name.lower() == k.lower(): - file_name = k - break - - if file_name not in chosen_node.__dict__[asset_type + "_files"]: - await msg.channel.send(msg.author.mention + " Specify a Pokemon and an existing emotion/animation.") - return - chosen_node.__dict__[asset_type + "_files"][file_name] = lock_state - - status = TrackerUtils.getStatusEmoji(chosen_node, asset_type) - - lock_str = "unlocked" - if lock_state: - lock_str = "locked" - # set to complete - await msg.channel.send(msg.author.mention + " {0} #{1:03d}: {2} {3} is now {4}.".format(status, int(full_idx[0]), " ".join(name_seq), file_name, lock_str)) - - self.saveTracker() - self.changed = True - - async def listBounties(self, msg, name_args): - if not self.config.use_bounties: - await msg.channel.send(msg.author.mention + " " + MESSAGE_BOUNTIES_DISABLED) - return - - include_sprite = True - include_portrait = True - - if len(name_args) > 0: - if name_args[0].lower() == "sprite": - include_portrait = False - elif name_args[0].lower() == "portrait": - include_sprite = False - else: - await msg.channel.send(msg.author.mention + " Use 'sprite' or 'portrait' as argument.") - return - - entries = [] - over_dict = TrackerUtils.initSubNode("", True) - over_dict.subgroups = self.tracker - - if include_sprite: - self.getBountiesFromDict("sprite", over_dict, entries, []) - if include_portrait: - self.getBountiesFromDict("portrait", over_dict, entries, []) - - entries = sorted(entries, reverse=True) - entries = entries[:10] - - posts = [] - if include_sprite and include_portrait: - posts.append("**Top Bounties**") - elif include_sprite: - posts.append("**Top Bounties for Sprites**") - else: - posts.append("**Top Bounties for Portraits**") - for entry in entries: - posts.append("#{0:02d}. {2} for **{1}GP**, paid when the {3} becomes {4}.".format(len(posts), entry[0], entry[1], entry[2], PHASES[entry[3]].title())) - - if len(posts) == 1: - posts.append("[None]") - - msgs_used, changed = await self.sendInfoPosts(msg.channel, posts, [], 0) - - async def clearCache(self, msg, name_args): - name_seq = [TrackerUtils.sanitizeName(i) for i in name_args] - full_idx = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq, 0) - if full_idx is None: - await msg.channel.send(msg.author.mention + " No such Pokemon.") - return - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) - - TrackerUtils.clearCache(chosen_node, True) - - self.saveTracker() - - await msg.channel.send(msg.author.mention + " Cleared links for #{0:03d}: {1}.".format(int(full_idx[0]), " ".join(name_seq))) - def createCreditAttribution(self, mention, plainName=False): if plainName: # "plainName" actually refers to "social-media-ready name" @@ -2074,183 +1768,31 @@ def createCreditBlock(self, credit, base_credit, plainName=False): block += " +{0} more".format(credit_diff) return block - async def resetCredit(self, msg, name_args, asset_type): - # compute answer from current status - if len(name_args) < 2: - await msg.channel.send(msg.author.mention + " Specify a user ID and Pokemon.") - return - - wanted_author = self.getFormattedCredit(name_args[0]) - name_seq = [TrackerUtils.sanitizeName(i) for i in name_args[1:]] - full_idx = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq, 0) - if full_idx is None: - await msg.channel.send(msg.author.mention + " No such Pokemon.") - return - - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) - - if chosen_node.__dict__[asset_type + "_credit"].primary == "": - await msg.channel.send(msg.author.mention + " No credit found.") - return - gen_path = TrackerUtils.getDirFromIdx(self.config.path, asset_type, full_idx) - - credit_entries = TrackerUtils.getCreditEntries(gen_path) - - if wanted_author not in credit_entries: - await msg.channel.send(msg.author.mention + " Could not find ID `{0}` in credits for {1}.".format(wanted_author, asset_type)) - return - - # make the credit array into the most current author by itself - credit_data = chosen_node.__dict__[asset_type + "_credit"] - if credit_data.primary == "CHUNSOFT": - await msg.channel.send(msg.author.mention + " Cannot reset credit for a CHUNSOFT {0}.".format(asset_type)) - return - - credit_data.primary = wanted_author - TrackerUtils.updateCreditFromEntries(credit_data, credit_entries) - - await msg.channel.send(msg.author.mention + " Credit display has been reset for {0} {1}:\n{2}".format(asset_type, " ".join(name_seq), self.createCreditBlock(credit_data, None))) - - self.saveTracker() - self.changed = True - - async def addCredit(self, msg, name_args, asset_type): - # compute answer from current status - if len(name_args) < 2: - await msg.channel.send(msg.author.mention + " Specify a user ID and Pokemon.") - return - - wanted_author = self.getFormattedCredit(name_args[0]) - name_seq = [TrackerUtils.sanitizeName(i) for i in name_args[1:]] - full_idx = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq, 0) - if full_idx is None: - await msg.channel.send(msg.author.mention + " No such Pokemon.") - return - - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) - - if chosen_node.__dict__[asset_type + "_credit"].primary == "": - await msg.channel.send(msg.author.mention + " This command only works on filled {0}.".format(asset_type)) - return - - if wanted_author not in self.names: - await msg.channel.send(msg.author.mention + " No such profile ID.") - return - - chat_id = self.config.servers[str(msg.guild.id)].submit - if chat_id == 0: - await msg.channel.send(msg.author.mention + " This server does not support submissions.") - return - - submit_channel = self.client.get_channel(chat_id) - author = "<@!{0}>".format(msg.author.id) - - base_link = await self.retrieveLinkMsg(full_idx, chosen_node, asset_type, False) - base_file, base_name = SpriteUtils.getLinkData(base_link) - - # stage a post in submissions - await self.postStagedSubmission(submit_channel, "--addauthor", "", full_idx, chosen_node, asset_type, author + "/" + wanted_author, - False, None, base_file, base_name, None) - - async def getAbsentProfiles(self, msg): - total_names = ["Absentee profiles:"] - msg_ids = [] - for name in self.names: - if not name.startswith("<@!"): - total_names.append(name + "\nName: \"{0}\" Contact: \"{1}\"".format(self.names[name].name, self.names[name].contact)) - await self.sendInfoPosts(msg.channel, total_names, msg_ids, 0) - - async def setProfile(self, msg, args): + async def deleteProfile(self, msg, args): msg_mention = "<@!{0}>".format(msg.author.id) if len(args) == 0: - new_credit = TrackerUtils.CreditEntry("", "") + await msg.channel.send(msg.author.mention + " WARNING: This command will move your credits into an anonymous profile and be separated from your account. This cannot be undone.\n" \ + "If you wish to proceed, rerun the command with your discord ID and username (with discriminator) as arguments.") + return elif len(args) == 1: - new_credit = TrackerUtils.CreditEntry(args[0], "") - elif len(args) == 2: - new_credit = TrackerUtils.CreditEntry(args[0], args[1]) - elif len(args) == 3: - if not await self.isAuthorized(msg.author, msg.guild): - await msg.channel.send(msg.author.mention + " Not authorized to create absent registration.") + if not (await self.getUserPermission(msg.author, msg.guild)).canPerformAction(PermissionLevel.STAFF): + await msg.channel.send(msg.author.mention + " Not authorized to delete registration.") return msg_mention = self.getFormattedCredit(args[0]) - new_credit = TrackerUtils.CreditEntry(args[1], args[2]) + elif len(args) == 2: + did = str(msg.author.id) + dname = msg.author.name + "#" + msg.author.discriminator + if args[0] != did or args[1] != dname: + await msg.channel.send(msg.author.mention + " Discord ID/Username did not match.") + return else: await msg.channel.send(msg.author.mention + " Invalid args") return - if msg_mention in self.names: - new_credit.sprites = self.names[msg_mention].sprites - new_credit.portraits = self.names[msg_mention].portraits - self.names[msg_mention] = new_credit - self.saveNames() - - await msg.channel.send(msg_mention + " registered profile:\nName: \"{0}\" Contact: \"{1}\"".format(self.names[msg_mention].name, self.names[msg_mention].contact)) - - async def transferProfile(self, msg, args): - if len(args) != 2: - await msg.channel.send(msg.author.mention + " Invalid args") - return - - from_name = self.getFormattedCredit(args[0]) - to_name = self.getFormattedCredit(args[1]) - if from_name.startswith("<@!") or from_name == "CHUNSOFT": - await msg.channel.send(msg.author.mention + " Only transfers from absent registrations are allowed.") - return - if from_name not in self.names: - await msg.channel.send(msg.author.mention + " Entry {0} doesn't exist!".format(from_name)) - return - if to_name not in self.names: - await msg.channel.send(msg.author.mention + " Entry {0} doesn't exist!".format(to_name)) - return - - new_credit = TrackerUtils.CreditEntry(self.names[to_name].name, self.names[to_name].contact) - new_credit.sprites = self.names[from_name].sprites or self.names[to_name].sprites - new_credit.portraits = self.names[from_name].portraits or self.names[to_name].portraits - del self.names[from_name] - self.names[to_name] = new_credit - - # update tracker based on last-modify - over_dict = TrackerUtils.initSubNode("", True) - over_dict.subgroups = self.tracker - - TrackerUtils.renameFileCredits(os.path.join(self.config.path, "sprite"), from_name, to_name) - TrackerUtils.renameFileCredits(os.path.join(self.config.path, "portrait"), from_name, to_name) - TrackerUtils.renameJsonCredits(over_dict, from_name, to_name) - - await msg.channel.send(msg.author.mention + " account {0} deleted and credits moved to {1}.".format(from_name, to_name)) - - self.saveTracker() - self.saveNames() - self.changed = True - - await self.gitCommit("Moved account {0} to {1}".format(from_name, to_name)) - - async def deleteProfile(self, msg, args): - msg_mention = "<@!{0}>".format(msg.author.id) - - if len(args) == 0: - await msg.channel.send(msg.author.mention + " WARNING: This command will move your credits into an anonymous profile and be separated from your account. This cannot be undone.\n" \ - "If you wish to proceed, rerun the command with your discord ID and username (with discriminator) as arguments.") - return - elif len(args) == 1: - if not await self.isAuthorized(msg.author, msg.guild): - await msg.channel.send(msg.author.mention + " Not authorized to delete registration.") - return - msg_mention = self.getFormattedCredit(args[0]) - elif len(args) == 2: - did = str(msg.author.id) - dname = msg.author.name + "#" + msg.author.discriminator - if args[0] != did or args[1] != dname: - await msg.channel.send(msg.author.mention + " Discord ID/Username did not match.") - return - else: - await msg.channel.send(msg.author.mention + " Invalid args") - return - - if msg_mention not in self.names: - await msg.channel.send(msg.author.mention + " Entry {0} doesn't exist!".format(msg_mention)) - return + if msg_mention not in self.names: + await msg.channel.send(msg.author.mention + " Entry {0} doesn't exist!".format(msg_mention)) + return @@ -2375,8 +1917,8 @@ async def initServer(self, msg, args): new_server.chat = bot_ch.id if submit_ch is not None: new_server.submit = submit_ch.id - new_server.approval_chat = reviewer_ch.id - new_server.approval = reviewer_role.id + new_server.approval_chat = reviewer_ch.id # type: ignore + new_server.approval = reviewer_role.id # type: ignore else: new_server.submit = 0 new_server.approval_chat = 0 @@ -2386,112 +1928,6 @@ async def initServer(self, msg, args): self.saveConfig() await msg.channel.send(msg.author.mention + " Initialized bot to this server!") - async def rescan(self, msg): - #SpriteUtils.iterateTracker(self.tracker, self.markPortraitFull, []) - #self.changed = True - #self.saveTracker() - await msg.channel.send(msg.author.mention + " Rescan complete.") - - async def addSpeciesForm(self, msg, args): - if len(args) < 1 or len(args) > 2: - await msg.channel.send(msg.author.mention + " Invalid number of args!") - return - - species_name = TrackerUtils.sanitizeName(args[0]) - species_idx = TrackerUtils.findSlotIdx(self.tracker, species_name) - if len(args) == 1: - if species_idx is not None: - await msg.channel.send(msg.author.mention + " {0} already exists!".format(species_name)) - return - - count = len(self.tracker) - new_idx = "{:04d}".format(count) - self.tracker[new_idx] = TrackerUtils.createSpeciesNode(species_name) - - await msg.channel.send(msg.author.mention + " Added #{0:03d}: {1}!".format(count, species_name)) - else: - if species_idx is None: - await msg.channel.send(msg.author.mention + " {0} doesn't exist! Create it first!".format(species_name)) - return - - form_name = TrackerUtils.sanitizeName(args[1]) - species_dict = self.tracker[species_idx] - form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) - if form_idx is not None: - await msg.channel.send(msg.author.mention + - " {2} already exists within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) - return - - if form_name == "Shiny" or form_name == "Male" or form_name == "Female": - await msg.channel.send(msg.author.mention + " Invalid form name!") - return - - canon = True - if re.search(r"_?Alternate\d*$", form_name): - canon = False - if re.search(r"_?Starter\d*$", form_name): - canon = False - if re.search(r"_?Altcolor\d*$", form_name): - canon = False - if re.search(r"_?Beta\d*$", form_name): - canon = False - if species_name == "Missingno_": - canon = False - - count = len(species_dict.subgroups) - new_count = "{:04d}".format(count) - species_dict.subgroups[new_count] = TrackerUtils.createFormNode(form_name, canon) - - await msg.channel.send(msg.author.mention + - " Added #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) - - self.saveTracker() - self.changed = True - - async def renameSpeciesForm(self, msg, args): - if len(args) < 2 or len(args) > 3: - await msg.channel.send(msg.author.mention + " Invalid number of args!") - return - - species_name = TrackerUtils.sanitizeName(args[0]) - new_name = TrackerUtils.sanitizeName(args[-1]) - species_idx = TrackerUtils.findSlotIdx(self.tracker, species_name) - if species_idx is None: - await msg.channel.send(msg.author.mention + " {0} does not exist!".format(species_name)) - return - - species_dict = self.tracker[species_idx] - - if len(args) == 2: - new_species_idx = TrackerUtils.findSlotIdx(self.tracker, new_name) - if new_species_idx is not None: - await msg.channel.send(msg.author.mention + " #{0:03d}: {1} already exists!".format(int(new_species_idx), new_name)) - return - - species_dict.name = new_name - await msg.channel.send(msg.author.mention + " Changed #{0:03d}: {1} to {2}!".format(int(species_idx), species_name, new_name)) - else: - - form_name = TrackerUtils.sanitizeName(args[1]) - form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) - if form_idx is None: - await msg.channel.send(msg.author.mention + " {2} doesn't exist within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) - return - - new_form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, new_name) - if new_form_idx is not None: - await msg.channel.send(msg.author.mention + " {2} already exists within #{0:03d}: {1}!".format(int(species_idx), species_name, new_name)) - return - - form_dict = species_dict.subgroups[form_idx] - form_dict.name = new_name - - await msg.channel.send(msg.author.mention + " Changed {2} to {3} in #{0:03d}: {1}!".format(int(species_idx), species_name, form_name, new_name)) - - self.saveTracker() - self.changed = True - - async def modSpeciesForm(self, msg, args): if len(args) < 1 or len(args) > 2: await msg.channel.send(msg.author.mention + " Invalid number of args!") @@ -2531,210 +1967,45 @@ async def modSpeciesForm(self, msg, args): self.saveTracker() self.changed = True + async def help(self, msg, args, permission_level: Optional[PermissionLevel]): + list_commands = len(args) == 0 + if permission_level == None: + if len(args) > 0: + if args[0] == "staff": + permission_level = PermissionLevel.STAFF + list_commands = True + elif args[0] == "admin": + permission_level = PermissionLevel.ADMIN + list_commands = True + else: + permission_level = PermissionLevel.EVERYONE + else: + permission_level = PermissionLevel.EVERYONE - async def removeSpeciesForm(self, msg, args): - if len(args) < 1 or len(args) > 2: - await msg.channel.send(msg.author.mention + " Invalid number of args!") - return - - species_name = TrackerUtils.sanitizeName(args[0]) - species_idx = TrackerUtils.findSlotIdx(self.tracker, species_name) - if species_idx is None: - await msg.channel.send(msg.author.mention + " {0} does not exist!".format(species_name)) - return - - species_dict = self.tracker[species_idx] - if len(args) == 1: - - # check against data population - if TrackerUtils.isDataPopulated(species_dict) and msg.author.id != self.config.root: - await msg.channel.send(msg.author.mention + " Can only delete empty slots!") - return - - TrackerUtils.deleteData(self.tracker, os.path.join(self.config.path, 'sprite'), - os.path.join(self.config.path, 'portrait'), species_idx) - - await msg.channel.send(msg.author.mention + " Deleted #{0:03d}: {1}!".format(int(species_idx), species_name)) - else: - - form_name = TrackerUtils.sanitizeName(args[1]) - form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) - if form_idx is None: - await msg.channel.send(msg.author.mention + " {2} doesn't exist within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) - return - - # check against data population - form_dict = species_dict.subgroups[form_idx] - if TrackerUtils.isDataPopulated(form_dict) and msg.author.id != self.config.root: - await msg.channel.send(msg.author.mention + " Can only delete empty slots!") - return - - TrackerUtils.deleteData(species_dict.subgroups, os.path.join(self.config.path, 'sprite', species_idx), - os.path.join(self.config.path, 'portrait', species_idx), form_idx) - - await msg.channel.send(msg.author.mention + " Deleted #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) - - self.saveTracker() - self.changed = True - - await self.gitCommit("Removed {0}".format(" ".join(args))) - - async def setNeed(self, msg, args, needed): - if len(args) < 2 or len(args) > 5: - await msg.channel.send(msg.author.mention + " Invalid number of args!") - return - - asset_type = args[0].lower() - if asset_type != "sprite" and asset_type != "portrait": - await msg.channel.send(msg.author.mention + " Must specify sprite or portrait!") - return - - name_seq = [TrackerUtils.sanitizeName(i) for i in args[1:]] - full_idx = TrackerUtils.findFullTrackerIdx(self.tracker, name_seq, 0) - if full_idx is None: - await msg.channel.send(msg.author.mention + " No such Pokemon.") - return - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) - chosen_node.__dict__[asset_type + "_required"] = needed - - if needed: - await msg.channel.send(msg.author.mention + " {0} {1} is now needed.".format(asset_type, " ".join(name_seq))) - else: - await msg.channel.send(msg.author.mention + " {0} {1} is no longer needed.".format(asset_type, " ".join(name_seq))) - - self.saveTracker() - self.changed = True - - async def addGender(self, msg, args): - if len(args) < 3 or len(args) > 4: - await msg.channel.send(msg.author.mention + " Invalid number of args!") - return - - asset_type = args[0].lower() - if asset_type != "sprite" and asset_type != "portrait": - await msg.channel.send(msg.author.mention + " Must specify sprite or portrait!") - return - - gender_name = args[-1].title() - if gender_name != "Male" and gender_name != "Female": - await msg.channel.send(msg.author.mention + " Must specify male or female!") - return - other_gender = "Male" - if gender_name == "Male": - other_gender = "Female" - - species_name = TrackerUtils.sanitizeName(args[1]) - species_idx = TrackerUtils.findSlotIdx(self.tracker, species_name) - if species_idx is None: - await msg.channel.send(msg.author.mention + " {0} does not exist!".format(species_name)) - return - - species_dict = self.tracker[species_idx] - if len(args) == 3: - # check against already existing - if TrackerUtils.genderDiffExists(species_dict.subgroups["0000"], asset_type, gender_name): - await msg.channel.send(msg.author.mention + " Gender difference already exists for #{0:03d}: {1}!".format(int(species_idx), species_name)) - return - - TrackerUtils.createGenderDiff(species_dict.subgroups["0000"], asset_type, gender_name) - await msg.channel.send(msg.author.mention + " Added gender difference to #{0:03d}: {1}! ({2})".format(int(species_idx), species_name, asset_type)) - else: - - form_name = TrackerUtils.sanitizeName(args[2]) - form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) - if form_idx is None: - await msg.channel.send(msg.author.mention + " {2} doesn't exist within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) - return - - # check against data population - form_dict = species_dict.subgroups[form_idx] - if TrackerUtils.genderDiffExists(form_dict, asset_type, gender_name): - await msg.channel.send(msg.author.mention + - " Gender difference already exists for #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) - return - - TrackerUtils.createGenderDiff(form_dict, asset_type, gender_name) - await msg.channel.send(msg.author.mention + - " Added gender difference to #{0:03d}: {1} {2}! ({3})".format(int(species_idx), species_name, form_name, asset_type)) - - self.saveTracker() - self.changed = True - - - async def removeGender(self, msg, args): - if len(args) < 2 or len(args) > 3: - await msg.channel.send(msg.author.mention + " Invalid number of args!") - return - - asset_type = args[0].lower() - if asset_type != "sprite" and asset_type != "portrait": - await msg.channel.send(msg.author.mention + " Must specify sprite or portrait!") - return - - species_name = TrackerUtils.sanitizeName(args[1]) - species_idx = TrackerUtils.findSlotIdx(self.tracker, species_name) - if species_idx is None: - await msg.channel.send(msg.author.mention + " {0} does not exist!".format(species_name)) - return - - species_dict = self.tracker[species_idx] - if len(args) == 2: - # check against not existing - if not TrackerUtils.genderDiffExists(species_dict.subgroups["0000"], asset_type, "Male") and \ - not TrackerUtils.genderDiffExists(species_dict.subgroups["0000"], asset_type, "Female"): - await msg.channel.send(msg.author.mention + " Gender difference doesnt exist for #{0:03d}: {1}!".format(int(species_idx), species_name)) - return - - # check against data population - if TrackerUtils.genderDiffPopulated(species_dict.subgroups["0000"], asset_type): - await msg.channel.send(msg.author.mention + " Gender difference isn't empty for #{0:03d}: {1}!".format(int(species_idx), species_name)) - return - - TrackerUtils.removeGenderDiff(species_dict.subgroups["0000"], asset_type) - await msg.channel.send(msg.author.mention + - " Removed gender difference to #{0:03d}: {1}! ({2})".format(int(species_idx), species_name, asset_type)) - else: - form_name = TrackerUtils.sanitizeName(args[2]) - form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) - if form_idx is None: - await msg.channel.send(msg.author.mention + " {2} doesn't exist within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) - return - - # check against not existing - form_dict = species_dict.subgroups[form_idx] - if not TrackerUtils.genderDiffExists(form_dict, asset_type, "Male") and \ - not TrackerUtils.genderDiffExists(form_dict, asset_type, "Female"): - await msg.channel.send(msg.author.mention + - " Gender difference doesn't exist for #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) - return - - # check against data population - if TrackerUtils.genderDiffPopulated(form_dict, asset_type): - await msg.channel.send(msg.author.mention + " Gender difference isn't empty for #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) - return - - TrackerUtils.removeGenderDiff(form_dict, asset_type) - await msg.channel.send(msg.author.mention + - " Removed gender difference to #{0:03d}: {1} {2}! ({3})".format(int(species_idx), species_name, form_name, asset_type)) - - self.saveTracker() - self.changed = True - - async def help(self, msg, args): server_config = self.config.servers[str(msg.guild.id)] prefix = server_config.prefix use_bounties = self.config.use_bounties - if len(args) == 0: + if list_commands: return_msg = "**Commands**\n" + + if permission_level == PermissionLevel.EVERYONE: + if use_bounties: + return_msg += f"`{prefix}spritebounty` - Place a bounty on a sprite\n" \ + f"`{prefix}portraitbounty` - Place a bounty on a portrait\n" + elif permission_level == PermissionLevel.STAFF: + return_msg = "**Approver Commands**\n" \ + f"`{prefix}modreward` - Toggles whether a sprite/portrait will have a custom reward\n" + for command in self.commands: - return_msg += f"`{prefix}{command.getCommand()}` - {command.getSingleLineHelp(server_config)}\n" - if use_bounties: - return_msg += f"`{prefix}spritebounty` - Place a bounty on a sprite\n" \ - f"`{prefix}portraitbounty` - Place a bounty on a portrait\n" \ - f"`{prefix}bounties` - View top bounties\n" - return_msg += f"`{prefix}register` - Register your profile\n" \ - f"Type `{prefix}help` with the name of a command to learn more about it." + if permission_level == command.getRequiredPermission() and command.shouldListInHelp(): + return_msg += f"`{prefix}{command.getCommand()}` - {command.getSingleLineHelp(server_config)}\n" + + if permission_level == PermissionLevel.EVERYONE: + return_msg += f"`{prefix}help staff` - List staff commands\n" \ + f"`{prefix}help admin` - List admin commands\n" + + return_msg += f"Type `{prefix}help` with the name of a command to learn more about it." else: base_arg = args[0] @@ -2785,366 +2056,6 @@ async def help(self, msg, args): f"`{prefix}portraitbounty Diancie Mega Shiny 1`" else: return_msg = MESSAGE_BOUNTIES_DISABLED - elif base_arg == "bounties": - if use_bounties: - return_msg = "**Command Help**\n" \ - f"`{prefix}bounties [Type]`\n" \ - "View the top sprites/portraits that have bounties placed on them. " \ - "You will claim a bounty when you successfully submit that sprite/portrait.\n" \ - "`Type` - [Optional] Can be `sprite` or `portrait`\n" \ - "**Examples**\n" \ - f"`{prefix}bounties`\n" \ - f"`{prefix}bounties sprite`" - else: - return_msg = MESSAGE_BOUNTIES_DISABLED - elif base_arg == "register": - return_msg = "**Command Help**\n" \ - f"`{prefix}register `\n" \ - "Registers your name and contact info for crediting purposes. " \ - "If you do not register, credits will be given to your discord ID instead.\n" \ - "`Name` - Your preferred name\n" \ - "`Contact` - Your preferred contact info; can be email, url, etc.\n" \ - "**Examples**\n" \ - f"`{prefix}register Audino https://github.com/audinowho`" - else: - return_msg = "Unknown Command." - await msg.channel.send(msg.author.mention + " {0}".format(return_msg)) - - - async def staffhelp(self, msg, args): - prefix = self.config.servers[str(msg.guild.id)].prefix - if len(args) == 0: - return_msg = "**Approver Commands**\n" \ - f"`{prefix}add` - Adds a Pokemon or forme to the current list\n" \ - f"`{prefix}delete` - Deletes an empty Pokemon or forme\n" \ - f"`{prefix}rename` - Renames a Pokemon or forme\n" \ - f"`{prefix}addgender` - Adds the female sprite/portrait to the Pokemon\n" \ - f"`{prefix}deletegender` - Removes the female sprite/portrait from the Pokemon\n" \ - f"`{prefix}need` - Marks a sprite/portrait as needed\n" \ - f"`{prefix}dontneed` - Marks a sprite/portrait as unneeded\n" \ - f"`{prefix}movesprite` - Swaps the sprites for two Pokemon/formes\n" \ - f"`{prefix}moveportrait` - Swaps the portraits for two Pokemon/formes\n" \ - f"`{prefix}move` - Swaps the sprites, portraits, and names for two Pokemon/formes\n" \ - f"`{prefix}spritewip` - Sets the sprite status as Incomplete\n" \ - f"`{prefix}portraitwip` - Sets the portrait status as Incomplete\n" \ - f"`{prefix}spriteexists` - Sets the sprite status as Exists\n" \ - f"`{prefix}portraitexists` - Sets the portrait status as Exists\n" \ - f"`{prefix}spritefilled` - Sets the sprite status as Fully Featured\n" \ - f"`{prefix}portraitfilled` - Sets the portrait status as Fully Featured\n" \ - f"`{prefix}setspritecredit` - Sets the primary author of the sprite\n" \ - f"`{prefix}setportraitcredit` - Sets the primary author of the portrait\n" \ - f"`{prefix}addspritecredit` - Adds a new author to the credits of the sprite\n" \ - f"`{prefix}addportraitcredit` - Adds a new author to the credits of the portrait\n" \ - f"`{prefix}modreward` - Toggles whether a sprite/portrait will have a custom reward\n" \ - f"`{prefix}register` - Use with arguments to make absentee profiles\n" \ - f"`{prefix}transferprofile` - Transfers the credit from absentee profile to a real one\n" \ - f"`{prefix}clearcache` - Clears the image/zip links for a Pokemon/forme/shiny/gender\n" \ - f"Type `{prefix}staffhelp` with the name of a command to learn more about it." - - else: - base_arg = args[0] - if base_arg == "add": - return_msg = "**Command Help**\n" \ - f"`{prefix}add [Form Name]`\n" \ - "Adds a Pokemon to the dex, or a form to the existing Pokemon.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}add Calyrex`\n" \ - f"`{prefix}add Mr_Mime Galar`\n" \ - f"`{prefix}add Missingno_ Kotora`" - elif base_arg == "delete": - return_msg = "**Command Help**\n" \ - f"`{prefix}delete [Form Name]`\n" \ - "Deletes a Pokemon or form of an existing Pokemon. " \ - "Only works if the slot + its children are empty.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}delete Pikablu`\n" \ - f"`{prefix}delete Arceus Mega`" - elif base_arg == "rename": - return_msg = "**Command Help**\n" \ - f"`{prefix}rename [Form Name] `\n" \ - "Changes the existing species or form to the new name.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`New Name` - New Pokemon of Form name\n" \ - "**Examples**\n" \ - f"`{prefix}rename Calrex Calyrex`\n" \ - f"`{prefix}rename Vulpix Aloha Alola`" - elif base_arg == "addgender": - return_msg = "**Command Help**\n" \ - f"`{prefix}addgender [Pokemon Form] `\n" \ - "Adds a slot for the male/female version of the species, or form of the species.\n" \ - "`Asset Type` - \"sprite\" or \"portrait\"\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}addgender Sprite Venusaur Female`\n" \ - f"`{prefix}addgender Portrait Steelix Female`\n" \ - f"`{prefix}addgender Sprite Raichu Alola Male`" - elif base_arg == "deletegender": - return_msg = "**Command Help**\n" \ - f"`{prefix}deletegender [Pokemon Form]`\n" \ - "Removes the slot for the male/female version of the species, or form of the species. " \ - "Only works if empty.\n" \ - "`Asset Type` - \"sprite\" or \"portrait\"\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}deletegender Sprite Venusaur`\n" \ - f"`{prefix}deletegender Portrait Steelix`\n" \ - f"`{prefix}deletegender Sprite Raichu Alola`" - elif base_arg == "need": - return_msg = "**Command Help**\n" \ - f"`{prefix}need [Pokemon Form] [Shiny]`\n" \ - "Marks a sprite/portrait as Needed. This is the default for all sprites/portraits.\n" \ - "`Asset Type` - \"sprite\" or \"portrait\"\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "**Examples**\n" \ - f"`{prefix}need Sprite Venusaur`\n" \ - f"`{prefix}need Portrait Steelix`\n" \ - f"`{prefix}need Portrait Minior Red`\n" \ - f"`{prefix}need Portrait Minior Shiny`\n" \ - f"`{prefix}need Sprite Castform Sunny Shiny`" - elif base_arg == "dontneed": - return_msg = "**Command Help**\n" \ - f"`{prefix}dontneed [Pokemon Form] [Shiny]`\n" \ - "Marks a sprite/portrait as Unneeded. " \ - "Unneeded sprites/portraits are marked with \u26AB and do not need submissions.\n" \ - "`Asset Type` - \"sprite\" or \"portrait\"\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "**Examples**\n" \ - f"`{prefix}dontneed Sprite Venusaur`\n" \ - f"`{prefix}dontneed Portrait Steelix`\n" \ - f"`{prefix}dontneed Portrait Minior Red`\n" \ - f"`{prefix}dontneed Portrait Minior Shiny`\n" \ - f"`{prefix}dontneed Sprite Alcremie Shiny`" - elif base_arg == "movesprite": - return_msg = "**Command Help**\n" \ - f"`{prefix}movesprite [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ - "Swaps the contents of one sprite with another. " \ - "Good for promoting alternates to main, temp Pokemon to newly revealed dex numbers, " \ - "or just fixing mistakes.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}movesprite Escavalier -> Accelgor`\n" \ - f"`{prefix}movesprite Zoroark Alternate -> Zoroark`\n" \ - f"`{prefix}movesprite Missingno_ Kleavor -> Kleavor`\n" \ - f"`{prefix}movesprite Minior Blue -> Minior Indigo`" - elif base_arg == "moveportrait": - return_msg = "**Command Help**\n" \ - f"`{prefix}moveportrait [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ - "Swaps the contents of one portrait with another. " \ - "Good for promoting alternates to main, temp Pokemon to newly revealed dex numbers, " \ - "or just fixing mistakes.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}moveportrait Escavalier -> Accelgor`\n" \ - f"`{prefix}moveportrait Zoroark Alternate -> Zoroark`\n" \ - f"`{prefix}moveportrait Missingno_ Kleavor -> Kleavor`\n" \ - f"`{prefix}moveportrait Minior Blue -> Minior Indigo`" - elif base_arg == "move": - return_msg = "**Command Help**\n" \ - f"`{prefix}move [Pokemon Form] -> [Pokemon Form 2]`\n" \ - "Swaps the name, sprites, and portraits of one slot with another. " \ - "This can only be done with Pokemon or formes, and the swap is recursive to shiny/genders. " \ - "Good for promoting alternate forms to base form, temp Pokemon to newly revealed dex numbers, " \ - "or just fixing mistakes.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}move Escavalier -> Accelgor`\n" \ - f"`{prefix}move Zoroark Alternate -> Zoroark`\n" \ - f"`{prefix}move Missingno_ Kleavor -> Kleavor`\n" \ - f"`{prefix}move Minior Blue -> Minior Indigo`" - elif base_arg == "replacesprite": - return_msg = "**Command Help**\n" \ - f"`{prefix}replacesprite [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ - "Replaces the contents of one sprite with another. " \ - "Good for promoting scratch-made alternates to main.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}replacesprite Zoroark Alternate -> Zoroark`" - elif base_arg == "replaceportrait": - return_msg = "**Command Help**\n" \ - f"`{prefix}replaceportrait [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ - "Replaces the contents of one portrait with another. " \ - "Good for promoting scratch-made alternates to main.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}replaceportrait Zoroark Alternate -> Zoroark`" - elif base_arg == "spritewip": - return_msg = "**Command Help**\n" \ - f"`{prefix}spritewip [Form Name] [Shiny] [Gender]`\n" \ - "Manually sets the sprite status as \u26AA Incomplete.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}spritewip Pikachu`\n" \ - f"`{prefix}spritewip Pikachu Shiny`\n" \ - f"`{prefix}spritewip Pikachu Female`\n" \ - f"`{prefix}spritewip Pikachu Shiny Female`\n" \ - f"`{prefix}spritewip Shaymin Sky`\n" \ - f"`{prefix}spritewip Shaymin Sky Shiny`" - elif base_arg == "portraitwip": - return_msg = "**Command Help**\n" \ - f"`{prefix}portraitwip [Form Name] [Shiny] [Gender]`\n" \ - "Manually sets the portrait status as \u26AA Incomplete.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}portraitwip Pikachu`\n" \ - f"`{prefix}portraitwip Pikachu Shiny`\n" \ - f"`{prefix}portraitwip Pikachu Female`\n" \ - f"`{prefix}portraitwip Pikachu Shiny Female`\n" \ - f"`{prefix}portraitwip Shaymin Sky`\n" \ - f"`{prefix}portraitwip Shaymin Sky Shiny`" - elif base_arg == "spriteexists": - return_msg = "**Command Help**\n" \ - f"`{prefix}spriteexists [Form Name] [Shiny] [Gender]`\n" \ - "Manually sets the sprite status as \u2705 Available.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}spriteexists Pikachu`\n" \ - f"`{prefix}spriteexists Pikachu Shiny`\n" \ - f"`{prefix}spriteexists Pikachu Female`\n" \ - f"`{prefix}spriteexists Pikachu Shiny Female`\n" \ - f"`{prefix}spriteexists Shaymin Sky`\n" \ - f"`{prefix}spriteexists Shaymin Sky Shiny`" - elif base_arg == "portraitexists": - return_msg = "**Command Help**\n" \ - f"`{prefix}portraitexists [Form Name] [Shiny] [Gender]`\n" \ - "Manually sets the portrait status as \u2705 Available.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}portraitexists Pikachu`\n" \ - f"`{prefix}portraitexists Pikachu Shiny`\n" \ - f"`{prefix}portraitexists Pikachu Female`\n" \ - f"`{prefix}portraitexists Pikachu Shiny Female`\n" \ - f"`{prefix}portraitexists Shaymin Sky`\n" \ - f"`{prefix}portraitexists Shaymin Sky Shiny`" - elif base_arg == "spritefilled": - return_msg = "**Command Help**\n" \ - f"`{prefix}spritefilled [Form Name] [Shiny] [Gender]`\n" \ - "Manually sets the sprite status as \u2B50 Fully Featured.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}spritefilled Pikachu`\n" \ - f"`{prefix}spritefilled Pikachu Shiny`\n" \ - f"`{prefix}spritefilled Pikachu Female`\n" \ - f"`{prefix}spritefilled Pikachu Shiny Female`\n" \ - f"`{prefix}spritefilled Shaymin Sky`\n" \ - f"`{prefix}spritefilled Shaymin Sky Shiny`" - elif base_arg == "portraitfilled": - return_msg = "**Command Help**\n" \ - f"`{prefix}portraitfilled [Form Name] [Shiny] [Gender]`\n" \ - "Manually sets the portrait status as \u2B50 Fully Featured.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}portraitfilled Pikachu`\n" \ - f"`{prefix}portraitfilled Pikachu Shiny`\n" \ - f"`{prefix}portraitfilled Pikachu Female`\n" \ - f"`{prefix}portraitfilled Pikachu Shiny Female`\n" \ - f"`{prefix}portraitfilled Shaymin Sky`\n" \ - f"`{prefix}portraitfilled Shaymin Sky Shiny`" - elif base_arg == "setspritecredit": - return_msg = "**Command Help**\n" \ - f"`{prefix}setspritecredit [Form Name] [Shiny] [Gender]`\n" \ - "Manually sets the primary author of a sprite to the specified author. " \ - "The specified author must already exist in the credits for the sprite.\n" \ - "`Author ID` - The discord ID of the author to set as primary\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}setspritecredit @Audino Unown Shiny`\n" \ - f"`{prefix}setspritecredit <@!117780585635643396> Unown Shiny`\n" \ - f"`{prefix}setspritecredit POWERCRISTAL Calyrex`\n" \ - f"`{prefix}setspritecredit POWERCRISTAL Calyrex Shiny`\n" \ - f"`{prefix}setspritecredit POWERCRISTAL Jellicent Shiny Female`" - elif base_arg == "setportraitcredit": - return_msg = "**Command Help**\n" \ - f"`{prefix}setportraitcredit [Form Name] [Shiny] [Gender]`\n" \ - "Manually sets the primary author of a portrait to the specified author. " \ - "The specified author must already exist in the credits for the portrait.\n" \ - "`Author ID` - The discord ID of the author to set as primary\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}setportraitcredit @Audino Unown Shiny`\n" \ - f"`{prefix}setportraitcredit <@!117780585635643396> Unown Shiny`\n" \ - f"`{prefix}setportraitcredit POWERCRISTAL Calyrex`\n" \ - f"`{prefix}setportraitcredit POWERCRISTAL Calyrex Shiny`\n" \ - f"`{prefix}setportraitcredit POWERCRISTAL Jellicent Shiny Female`" - elif base_arg == "addspritecredit": - return_msg = "**Command Help**\n" \ - f"`{prefix}addspritecredit [Form Name] [Shiny] [Gender]`\n" \ - "Adds the specified author to the credits of the sprite. " \ - "This makes a post in the submissions channel, asking other approvers to sign off.\n" \ - "`Author ID` - The discord ID of the author to set as primary\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}addspritecredit @Audino Unown Shiny`\n" \ - f"`{prefix}addspritecredit <@!117780585635643396> Unown Shiny`\n" \ - f"`{prefix}addspritecredit POWERCRISTAL Calyrex`\n" \ - f"`{prefix}addspritecredit POWERCRISTAL Calyrex Shiny`\n" \ - f"`{prefix}addspritecredit POWERCRISTAL Jellicent Shiny Female`" - elif base_arg == "addportraitcredit": - return_msg = "**Command Help**\n" \ - f"`{prefix}addportraitcredit [Form Name] [Shiny] [Gender]`\n" \ - "Adds the specified author to the credits of the portrait. " \ - "This makes a post in the submissions channel, asking other approvers to sign off.\n" \ - "`Author ID` - The discord ID of the author to set as primary\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}addportraitcredit @Audino Unown Shiny`\n" \ - f"`{prefix}addportraitcredit <@!117780585635643396> Unown Shiny`\n" \ - f"`{prefix}addportraitcredit POWERCRISTAL Calyrex`\n" \ - f"`{prefix}addportraitcredit POWERCRISTAL Calyrex Shiny`\n" \ - f"`{prefix}addportraitcredit POWERCRISTAL Jellicent Shiny Female`" elif base_arg == "modreward": return_msg = "**Command Help**\n" \ f"`{prefix}modreward [Form Name]`\n" \ @@ -3155,61 +2066,15 @@ async def staffhelp(self, msg, args): "**Examples**\n" \ f"`{prefix}modreward Unown`\n" \ f"`{prefix}modreward Minior Red`" - elif base_arg == "register": - return_msg = "**Command Help**\n" \ - f"`{prefix}register `\n" \ - "Registers an absentee profile with name and contact info for crediting purposes. " \ - "If a discord ID is provided, the profile is force-edited " \ - "(can be used to remove inappropriate content)." \ - "This command is also available for self-registration. " \ - f"Check the `{prefix}help` version for more.\n" \ - "`Author ID` - The desired ID of the absentee profile\n" \ - "`Name` - The person's preferred name\n" \ - "`Contact` - The person's preferred contact info\n" \ - "**Examples**\n" \ - f"`{prefix}register SUGIMORI Sugimori https://twitter.com/SUPER_32X`\n" \ - f"`{prefix}register @Audino Audino https://github.com/audinowho`\n" \ - f"`{prefix}register <@!117780585635643396> Audino https://github.com/audinowho`" - elif base_arg == "transferprofile": - return_msg = "**Command Help**\n" \ - f"`{prefix}transferprofile `\n" \ - "Transfers the credit from absentee profile to a real one. " \ - "Used for when an absentee's discord account is confirmed " \ - "and credit needs te be moved to the new name." \ - "This command is also available for self-registration. " \ - f"Check the `{prefix}help` version for more.\n" \ - "`Author ID` - The desired ID of the absentee profile\n" \ - "`New Author ID` - The real discord ID of the author\n" \ - "**Examples**\n" \ - f"`{prefix}transferprofile AUDINO_WHO <@!117780585635643396>`\n" \ - f"`{prefix}transferprofile AUDINO_WHO @Audino`" - elif base_arg == "clearcache": - return_msg = "**Command Help**\n" \ - f"`{prefix}clearcache [Form Name] [Shiny] [Gender]`\n" \ - "Clears the all uploaded images related to a Pokemon, allowing them to be regenerated. " \ - "This includes all portrait image and sprite zip links, " \ - "meant to be used whenever those links somehow become stale.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}clearcache Pikachu`\n" \ - f"`{prefix}clearcache Pikachu Shiny`\n" \ - f"`{prefix}clearcache Pikachu Female`\n" \ - f"`{prefix}clearcache Pikachu Shiny Female`\n" \ - f"`{prefix}clearcache Shaymin Sky`\n" \ - f"`{prefix}clearcache Shaymin Sky Shiny`" else: return_msg = "Unknown Command." await msg.channel.send(msg.author.mention + " {0}".format(return_msg)) - @client.event async def on_ready(): print('Logged in as') - print(client.user.name) - print(client.user.id) + print(client.user.name) # type: ignore + print(client.user.id) # type: ignore global sprite_bot await sprite_bot.checkAllSubmissions() await sprite_bot.checkRestarted() @@ -3246,108 +2111,37 @@ async def on_message(msg: discord.Message): return args = content[len(prefix):].split() - authorized = await sprite_bot.isAuthorized(msg.author, msg.guild) + user_permission = await sprite_bot.getUserPermission(msg.author, msg.guild) + authorized = user_permission.canPerformAction(PermissionLevel.STAFF) base_arg = args[0].lower() for command in sprite_bot.commands: if base_arg == command.getCommand(): - await command.executeCommand(msg, args[1:]) + #TODO: a way to overwrite this value for certain command via config is needed is needed for NotSpriteCollab, but just compare to default for now + if user_permission.canPerformAction(command.getRequiredPermission()): + await command.executeCommand(msg, args[1:]) + else: + await msg.channel.send("{} Not authorized (you need the permission level of at least “{}” to run this command)".format(msg.author.mention, command.getRequiredPermission().displayname())) return if base_arg == "help": - await sprite_bot.help(msg, args[1:]) + await sprite_bot.help(msg, args[1:], None) + # legacy link to help staff elif base_arg == "staffhelp": - await sprite_bot.staffhelp(msg, args[1:]) + await sprite_bot.help(msg, args[1:], PermissionLevel.STAFF) # primary commands elif base_arg == "spritebounty": await sprite_bot.placeBounty(msg, args[1:], "sprite") elif base_arg == "portraitbounty": await sprite_bot.placeBounty(msg, args[1:], "portrait") - elif base_arg == "bounties": - await sprite_bot.listBounties(msg, args[1:]) - elif base_arg == "register": - await sprite_bot.setProfile(msg, args[1:]) - elif base_arg == "absentprofiles": - await sprite_bot.getAbsentProfiles(msg) elif base_arg == "unregister": await sprite_bot.deleteProfile(msg, args[1:]) # authorized commands - elif base_arg == "add" and authorized: - await sprite_bot.addSpeciesForm(msg, args[1:]) - elif base_arg == "delete" and authorized: - await sprite_bot.removeSpeciesForm(msg, args[1:]) - elif base_arg == "rename" and authorized: - await sprite_bot.renameSpeciesForm(msg, args[1:]) - elif base_arg == "addgender" and authorized: - await sprite_bot.addGender(msg, args[1:]) - elif base_arg == "deletegender" and authorized: - await sprite_bot.removeGender(msg, args[1:]) - elif base_arg == "need" and authorized: - await sprite_bot.setNeed(msg, args[1:], True) - elif base_arg == "dontneed" and authorized: - await sprite_bot.setNeed(msg, args[1:], False) - elif base_arg == "movesprite" and authorized: - await sprite_bot.moveSlot(msg, args[1:], "sprite") - elif base_arg == "moveportrait" and authorized: - await sprite_bot.moveSlot(msg, args[1:], "portrait") - elif base_arg == "move" and authorized: - await sprite_bot.moveSlotRecursive(msg, args[1:]) - elif base_arg == "replacesprite" and authorized: - await sprite_bot.replaceSlot(msg, args[1:], "sprite") - elif base_arg == "replaceportrait" and authorized: - await sprite_bot.replaceSlot(msg, args[1:], "portrait") - elif base_arg == "spritewip" and authorized: - await sprite_bot.completeSlot(msg, args[1:], "sprite", TrackerUtils.PHASE_INCOMPLETE) - elif base_arg == "portraitwip" and authorized: - await sprite_bot.completeSlot(msg, args[1:], "portrait", TrackerUtils.PHASE_INCOMPLETE) - elif base_arg == "spriteexists" and authorized: - await sprite_bot.completeSlot(msg, args[1:], "sprite", TrackerUtils.PHASE_EXISTS) - elif base_arg == "portraitexists" and authorized: - await sprite_bot.completeSlot(msg, args[1:], "portrait", TrackerUtils.PHASE_EXISTS) - elif base_arg == "spritefilled" and authorized: - await sprite_bot.completeSlot(msg, args[1:], "sprite", TrackerUtils.PHASE_FULL) - elif base_arg == "portraitfilled" and authorized: - await sprite_bot.completeSlot(msg, args[1:], "portrait", TrackerUtils.PHASE_FULL) - elif base_arg == "setspritecredit" and authorized: - await sprite_bot.resetCredit(msg, args[1:], "sprite") - elif base_arg == "setportraitcredit" and authorized: - await sprite_bot.resetCredit(msg, args[1:], "portrait") - elif base_arg == "addspritecredit" and authorized: - await sprite_bot.addCredit(msg, args[1:], "sprite") - elif base_arg == "addportraitcredit" and authorized: - await sprite_bot.addCredit(msg, args[1:], "portrait") elif base_arg == "modreward" and authorized: await sprite_bot.modSpeciesForm(msg, args[1:]) - elif base_arg == "transferprofile" and authorized: - await sprite_bot.transferProfile(msg, args[1:]) - elif base_arg == "clearcache" and authorized: - await sprite_bot.clearCache(msg, args[1:]) # root commands elif base_arg == "promote" and msg.author.id == sprite_bot.config.root: await sprite_bot.promote(msg, args[1:]) - elif base_arg == "rescan" and msg.author.id == sprite_bot.config.root: - await sprite_bot.rescan(msg) - elif base_arg == "unlockportrait" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setLock(msg, args[1:], "portrait", False) - elif base_arg == "unlocksprite" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setLock(msg, args[1:], "sprite", False) - elif base_arg == "lockportrait" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setLock(msg, args[1:], "portrait", True) - elif base_arg == "locksprite" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setLock(msg, args[1:], "sprite", True) - elif base_arg == "canon" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setCanon(msg, args[1:], True) - elif base_arg == "noncanon" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setCanon(msg, args[1:], False) - elif base_arg == "update" and msg.author.id == sprite_bot.config.root: - await sprite_bot.updateBot(msg) - elif base_arg == "shutdown" and msg.author.id == sprite_bot.config.root: - await sprite_bot.shutdown(msg) - elif base_arg == "forcepush" and msg.author.id == sprite_bot.config.root: - sprite_bot.generateCreditCompilation() - await sprite_bot.gitCommit("Tracker update from forced push.") - await sprite_bot.gitPush() - await msg.channel.send(msg.author.mention + " Changes pushed.") elif base_arg in ["gr", "tr", "checkr"]: pass else: @@ -3365,11 +2159,11 @@ async def on_message(msg: discord.Message): async def on_raw_reaction_add(payload): await client.wait_until_ready() try: - if payload.user_id == client.user.id: + if payload.user_id == client.user.id: # type: ignore return guild_id_str = str(payload.guild_id) if payload.channel_id == sprite_bot.config.servers[guild_id_str].submit: - msg = await client.get_channel(payload.channel_id).fetch_message(payload.message_id) + msg = await client.get_channel(payload.channel_id).fetch_message(payload.message_id) # type: ignore changed_tracker = await sprite_bot.pollSubmission(msg) if changed_tracker: sprite_bot.saveTracker() diff --git a/SpriteUtils.py b/SpriteUtils.py index 9fd04d4..4279d9e 100644 --- a/SpriteUtils.py +++ b/SpriteUtils.py @@ -136,7 +136,7 @@ def thumbnailFileImg(inFile): factor = 400 // length new_size = (img.size[0] * factor, img.size[1] * factor) # expand to 400px wide at most - img = img.resize(new_size, resample=Image.NEAREST) + img = img.resize(new_size, resample=Image.NEAREST) # type: ignore file_data = BytesIO() img.save(file_data, format='PNG') @@ -207,8 +207,8 @@ def animateFileZip(inFile, anim): tile_tex = anim_img.crop(tile_bounds) shadow_tex = shadow_img.crop(tile_bounds) - new_tile_tex = tile_tex.resize(newTileSize, resample=Image.NEAREST) - new_shadow_tex = shadow_tex.resize(newTileSize, resample=Image.NEAREST) + new_tile_tex = tile_tex.resize(newTileSize, resample=Image.NEAREST) # type: ignore + new_shadow_tex = shadow_tex.resize(newTileSize, resample=Image.NEAREST) # type: ignore total_durations.append(durations[jj] * 20) @@ -269,7 +269,7 @@ def verifyZipFile(zip, file_name): if info.file_size > ZIP_SIZE_LIMIT: raise SpriteVerifyError("Zipped file {0} is too large, at {1} bytes.".format(file_name, info.file_size)) -def readZipImg(zip, file_name) -> Image.Image: +def readZipImg(zip, file_name: str) -> Image.Image: verifyZipFile(zip, file_name) file_data = BytesIO() @@ -401,6 +401,8 @@ def getStatsFromTree(file_data): if durations_node is None: raise SpriteVerifyError("Durations missing in {}".format(name)) for dur_node in durations_node.iter('Duration'): + if dur_node.text is None: + raise SpriteVerifyError("Duration text missing in a Duration entry in {}".format(name)) duration = int(dur_node.text) anim_stat.durations.append(duration) @@ -476,10 +478,10 @@ def compareSpriteRecolorDiff(orig_anim_img, shiny_anim_img, anim_name, shiny_palette[shiny_color] += 1 def verifySpriteRecolor(msg_args, precolor_zip, wan_zip, recolor, checkSilhouette): - orig_palette = {} - shiny_palette = {} - trans_diff = {} - black_diff = {} + orig_palette = {} # type: ignore + shiny_palette = {} # type: ignore + trans_diff = {} # type: ignore + black_diff = {} # type: ignore if recolor: if precolor_zip.size != wan_zip.size: @@ -518,8 +520,8 @@ def verifySpriteRecolor(msg_args, precolor_zip, wan_zip, recolor, checkSilhouett if orig_anim_data != shiny_anim_data: bin_diff.append(shiny_name) elif not shiny_name.endswith("-Anim.png"): - orig_anim_data = readZipImg(zip, shiny_name) - shiny_anim_data = readZipImg(shiny_zip, shiny_name) + orig_anim_data = readZipImg(zip, shiny_name) # type: ignore + shiny_anim_data = readZipImg(shiny_zip, shiny_name) # type: ignore if not exUtils.imgsEqual(orig_anim_data, shiny_anim_data): bin_diff.append(shiny_name) @@ -683,7 +685,7 @@ def getLRSwappedOffset(offset): return swapped_offset def mapDuplicateImportImgs(imgs, final_imgs, img_map, offset_diffs): - map_back = {} + map_back = {} # type: ignore for idx, img in enumerate(imgs): dupe = False flip = -1 @@ -873,9 +875,9 @@ def verifySprite(msg_args, wan_zip): if len(rogue_pixels) > 0: raise SpriteVerifyError("Semi-transparent pixels found at: {0}".format(str(rogue_pixels)[:1900])) - offset_diffs = {} + offset_diffs = {} # type: ignore frame_map = [None] * len(frames) - final_frames = [] + final_frames = [] # type: ignore mapDuplicateImportImgs(frames, final_frames, frame_map, offset_diffs) if len(offset_diffs) > 0: if not msg_args.multioffset: @@ -919,7 +921,7 @@ def verifySpriteLock(dict, chosen_path, precolor_zip, wan_zip, recolor): frame_size = getFrameSizeFromFrames(frames) # obtain a mapping from the color image of the shiny path - shiny_frames = [] + shiny_frames = [] # type: ignore for yy in range(0, wan_zip.size[1], frame_size[1]): for xx in range(0, wan_zip.size[0], frame_size[0]): tile_bounds = (xx, yy, xx + frame_size[0], yy + frame_size[1]) @@ -1084,7 +1086,7 @@ def verifyPortrait(msg_args, img): raise SpriteVerifyError("Portrait has an invalid size of {0}, exceeding max of {1}".format(str(img.size), str(max_size))) in_data = img.getdata() - occupied = [[]] * Constants.PORTRAIT_TILE_X + occupied: List[List[bool]] = [[]] * Constants.PORTRAIT_TILE_X for ii in range(Constants.PORTRAIT_TILE_X): occupied[ii] = [False] * Constants.PORTRAIT_TILE_Y @@ -1241,8 +1243,8 @@ def isCopyOf(species_path, anim): tree = ET.parse(os.path.join(species_path, Constants.MULTI_SHEET_XML)) root = tree.getroot() anims_node = root.find('Anims') - for anim_node in anims_node.iter('Anim'): - name = anim_node.find('Name').text + for anim_node in anims_node.iter('Anim'): # type: ignore + name = anim_node.find('Name').text # type: ignore if name == anim: backref_node = anim_node.find('CopyOf') return backref_node is not None @@ -1272,7 +1274,7 @@ def placeSpriteRecolorToPath(orig_path, outImg, dest_path): frame_size = getFrameSizeFromFrames(frames) # obtain a mapping from the color image of the shiny path - shiny_frames = [] + shiny_frames = [] # type: ignore for yy in range(0, outImg.size[1], frame_size[1]): for xx in range(0, outImg.size[0], frame_size[0]): tile_bounds = (xx, yy, xx + frame_size[0], yy + frame_size[1]) @@ -1304,7 +1306,7 @@ def createRecolorAnim(template_img, anim_map, shiny_frames): frame_idx, flip = anim_map[abs_bounds] imgPiece = shiny_frames[frame_idx] if flip: - imgPiece = imgPiece.transpose(Image.FLIP_LEFT_RIGHT) + imgPiece = imgPiece.transpose(Image.FLIP_LEFT_RIGHT) # type: ignore anim_img.paste(imgPiece, (abs_bounds[0], abs_bounds[1]), imgPiece) return anim_img @@ -1501,7 +1503,7 @@ def getSpriteRecolorMap(frames, shiny_frames): img_tbl.append((frame_tex, shiny_tex)) break - color_lookup = {} + color_lookup = {} # type: ignore # only do a color mapping for frames that have been known to fit for frame_tex, shiny_tex in img_tbl: @@ -1546,7 +1548,7 @@ def getPortraitRecolorMap(img, shinyImg, frame_size): shiny_tex = shinyImg.crop(abs_bounds) img_tbl.append((frame_tex, shiny_tex)) - color_lookup = {} + color_lookup = {} # type: ignore datas = img.getdata() shinyDatas = shinyImg.getdata() for idx in range(len(datas)): @@ -1575,11 +1577,11 @@ def getRecoloredTex(color_tbl, img_tbl, frame_tex): if exUtils.imgsEqual(frame, frame_tex): return shiny_frame, { } if exUtils.imgsEqual(frame, frame_tex, True): - return shiny_frame.transpose(Image.FLIP_LEFT_RIGHT), { } + return shiny_frame.transpose(Image.FLIP_LEFT_RIGHT), { } # type: ignore # attempt to recolor the image datas = frame_tex.getdata() shiny_datas = [(0,0,0,0)] * len(datas) - off_color_tbl = { } + off_color_tbl = { } # type: ignore for idx in range(len(datas)): color = datas[idx] if color[3] != 255: @@ -1611,7 +1613,7 @@ def updateOffColorTable(total_off_color, off_color_tbl): def autoRecolor(prev_base_file, cur_base_path, shiny_path, asset_type): cur_shiny_img = None - total_off_color = {} + total_off_color = {} # type: ignore if asset_type == "sprite": with zipfile.ZipFile(prev_base_file, 'r') as prev_base_zip: prev_frames, _ = getFramesAndMappings(prev_base_zip, True) @@ -1799,9 +1801,10 @@ def simple_quant(img: Image.Image, colors) -> Image.Image: if img.mode != 'RGBA': img = img.convert('RGBA') transparency_map = [px[3] == 0 for px in img.getdata()] - qimg = img.quantize(colors, dither=0).convert('RGBA') + qimg = img.quantize(colors, dither=0).convert('RGBA') # type: ignore # Shift up all pixel values by 1 and add the transparent pixels pixels = qimg.load() + assert(pixels is not None) k = 0 for j in range(img.size[1]): for i in range(img.size[0]): diff --git a/TrackerUtils.py b/TrackerUtils.py index 1617944..1530f05 100644 --- a/TrackerUtils.py +++ b/TrackerUtils.py @@ -1,4 +1,6 @@ +from typing import Dict, List, Any, Optional, Tuple +import sys import os import re import shutil @@ -91,13 +93,13 @@ def mergeCredits(path_from, path_to): id_list = [] with open(path_to, 'r', encoding='utf-8') as txt: for line in txt: - credit = line.strip().split('\t') - id_list.append(CreditEvent(credit[0], credit[1], credit[2], credit[3], credit[4])) + splited = line.strip().split('\t') + id_list.append(CreditEvent(splited[0], splited[1], splited[2], splited[3], splited[4])) with open(path_from, 'r', encoding='utf-8') as txt: for line in txt: - credit = line.strip().split('\t') - id_list.append(CreditEvent(credit[0], credit[1], credit[2], credit[3], credit[4])) + splited = line.strip().split('\t') + id_list.append(CreditEvent(splited[0], splited[1], splited[2], splited[3], splited[4])) id_list = sorted(id_list, key=lambda x: x.datetime) @@ -158,11 +160,14 @@ def __init__(self, node_dict): temp_list = [i for i in node_dict] temp_list = sorted(temp_list) - main_dict = { } - for key in temp_list: - main_dict[key] = node_dict[key] + self.subgroups = { } - self.__dict__ = main_dict + for key in temp_list: + if key == "subgroups": + for sub_key, sub in node_dict[key].items(): + self.subgroups[sub_key] = TrackerNode(sub) + else: + self.__dict__[key] = node_dict[key] if "sprite_talk" not in self.__dict__: self.sprite_talk = {} @@ -171,11 +176,6 @@ def __init__(self, node_dict): self.sprite_credit = CreditNode(node_dict["sprite_credit"]) self.portrait_credit = CreditNode(node_dict["portrait_credit"]) - sub_dict = { } - for key in self.subgroups: - sub_dict[key] = TrackerNode(self.subgroups[key]) - self.subgroups = sub_dict - def getDict(self): node_dict = { } for k in self.__dict__: @@ -221,7 +221,7 @@ def loadNameFile(name_path): return name_dict def initCreditDict(): - credit_dict = { } + credit_dict: Dict[str, Any] = { } credit_dict["primary"] = "" credit_dict["secondary"] = [] credit_dict["total"] = 0 @@ -307,8 +307,8 @@ def updateFiles(dict, species_path, prefix): tree = ET.parse(os.path.join(species_path, Constants.MULTI_SHEET_XML)) root = tree.getroot() anims_node = root.find('Anims') - for anim_node in anims_node.iter('Anim'): - name = anim_node.find('Name').text + for anim_node in anims_node.iter('Anim'): # type: ignore + name = anim_node.find('Name').text # type: ignore file_list.append(name) else: for inFile in os.listdir(species_path): @@ -505,7 +505,7 @@ def createShinyIdx(full_idx, shiny): new_idx.pop() return new_idx -def getNodeFromIdx(tracker_dict, full_idx, depth): +def getNodeFromIdx(tracker_dict: Dict[str, TrackerNode], full_idx: Optional[List[str]], depth: int) -> Optional[TrackerNode]: if full_idx is None: return None if len(full_idx) == 0: @@ -521,7 +521,7 @@ def getNodeFromIdx(tracker_dict, full_idx, depth): # recursive case, kind of weird return getNodeFromIdx(node.subgroups, full_idx, depth+1) -def getStatsFromFilename(filename): +def getStatsFromFilename(filename: str) -> Tuple[bool, Optional[List[str]], Optional[str], Optional[bool]]: # attempt to parse the filename to a destination file, ext = os.path.splitext(filename) name_idx = file.split("-") @@ -690,7 +690,7 @@ def updateCreditCompilation(name_path, credit_dict): txt.write("\t\t{0}: {1}\n".format(id_key, ",".join(all_parts))) txt.write("\n") -def updateCompilationStats(name_dict, dict, species_path, prefix, form_name_list, credit_dict): +def updateCompilationStats(name_dict, dict, species_path, prefix, form_name_list, credit_dict: Dict[str, CreditCompileEntry]): # generate the form name form_name = " ".join([i for i in form_name_list if i != ""]) # is there a credits txt? read it @@ -847,6 +847,9 @@ def swapFolderPaths(base_path, tracker, asset_type, full_idx_from, full_idx_to): chosen_node_from = getNodeFromIdx(tracker, full_idx_from, 0) chosen_node_to = getNodeFromIdx(tracker, full_idx_to, 0) + if chosen_node_from is None or chosen_node_to is None: + raise KeyError("Source {} or destination {} node not found in the tracker".format(str(full_idx_from), str(full_idx_to))) + swapNodeAssetFeatures(chosen_node_from, chosen_node_to, asset_type) # prepare to swap textures @@ -932,6 +935,9 @@ def swapAllSubNodes(base_path, tracker, full_idx_from, full_idx_to): chosen_node_from = getNodeFromIdx(tracker, full_idx_from, 0) chosen_node_to = getNodeFromIdx(tracker, full_idx_to, 0) + if chosen_node_from is None or chosen_node_to is None: + raise KeyError("Source {} or destination {} node not found in the tracker".format(str(full_idx_from), str(full_idx_to))) + tmp = chosen_node_from.subgroups chosen_node_from.subgroups = chosen_node_to.subgroups chosen_node_to.subgroups = tmp diff --git a/commands/AddGender.py b/commands/AddGender.py new file mode 100644 index 0000000..a5ee03f --- /dev/null +++ b/commands/AddGender.py @@ -0,0 +1,86 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import SpriteUtils +import TrackerUtils +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class AddGender(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "addgender" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Adds the female sprite/portrait to the Pokemon" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}addgender [Pokemon Form] `\n" \ + "Adds a slot for the male/female version of the species, or form of the species.\n" \ + "`Asset Type` - \"sprite\" or \"portrait\"\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "Sprite Venusaur Female", + "Portrait Steelix Female", + "Sprite Raichu Alola Male" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + if len(args) < 3 or len(args) > 4: + await msg.channel.send(msg.author.mention + " Invalid number of args!") + return + + asset_type = args[0].lower() + if asset_type != "sprite" and asset_type != "portrait": + await msg.channel.send(msg.author.mention + " Must specify sprite or portrait!") + return + + gender_name = args[-1].title() + if gender_name != "Male" and gender_name != "Female": + await msg.channel.send(msg.author.mention + " Must specify male or female!") + return + other_gender = "Male" + if gender_name == "Male": + other_gender = "Female" + + species_name = TrackerUtils.sanitizeName(args[1]) + species_idx = TrackerUtils.findSlotIdx(self.spritebot.tracker, species_name) + if species_idx is None: + await msg.channel.send(msg.author.mention + " {0} does not exist!".format(species_name)) + return + + species_dict = self.spritebot.tracker[species_idx] + if len(args) == 3: + # check against already existing + if TrackerUtils.genderDiffExists(species_dict.subgroups["0000"], asset_type, gender_name): + await msg.channel.send(msg.author.mention + " Gender difference already exists for #{0:03d}: {1}!".format(int(species_idx), species_name)) + return + + TrackerUtils.createGenderDiff(species_dict.subgroups["0000"], asset_type, gender_name) + await msg.channel.send(msg.author.mention + " Added gender difference to #{0:03d}: {1}! ({2})".format(int(species_idx), species_name, asset_type)) + else: + + form_name = TrackerUtils.sanitizeName(args[2]) + form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) + if form_idx is None: + await msg.channel.send(msg.author.mention + " {2} doesn't exist within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) + return + + # check against data population + form_dict = species_dict.subgroups[form_idx] + if TrackerUtils.genderDiffExists(form_dict, asset_type, gender_name): + await msg.channel.send(msg.author.mention + + " Gender difference already exists for #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) + return + + TrackerUtils.createGenderDiff(form_dict, asset_type, gender_name) + await msg.channel.send(msg.author.mention + + " Added gender difference to #{0:03d}: {1} {2}! ({3})".format(int(species_idx), species_name, form_name, asset_type)) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file diff --git a/commands/AddNode.py b/commands/AddNode.py new file mode 100644 index 0000000..aafbb8b --- /dev/null +++ b/commands/AddNode.py @@ -0,0 +1,86 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord +import re + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class AddNode(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "add" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Adds a Pokemon or forme to the current list" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}add [Pokemon Form]`\n" \ + "Adds a Pokemon to the dex, or a form to the existing Pokemon.\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "Calyrex", + "Mr_Mime Galar", + "Missingno_ Kotora" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + if len(args) < 1 or len(args) > 2: + await msg.channel.send(msg.author.mention + " Invalid number of args!") + return + + species_name = TrackerUtils.sanitizeName(args[0]) + species_idx = TrackerUtils.findSlotIdx(self.spritebot.tracker, species_name) + if len(args) == 1: + if species_idx is not None: + await msg.channel.send(msg.author.mention + " {0} already exists!".format(species_name)) + return + + count = len(self.spritebot.tracker) + new_idx = "{:04d}".format(count) + self.spritebot.tracker[new_idx] = TrackerUtils.createSpeciesNode(species_name) + + await msg.channel.send(msg.author.mention + " Added #{0:03d}: {1}!".format(count, species_name)) + else: + if species_idx is None: + await msg.channel.send(msg.author.mention + " {0} doesn't exist! Create it first!".format(species_name)) + return + + form_name = TrackerUtils.sanitizeName(args[1]) + species_dict = self.spritebot.tracker[species_idx] + form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) + if form_idx is not None: + await msg.channel.send(msg.author.mention + + " {2} already exists within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) + return + + if form_name == "Shiny" or form_name == "Male" or form_name == "Female": + await msg.channel.send(msg.author.mention + " Invalid form name!") + return + + canon = True + if re.search(r"_?Alternate\d*$", form_name): + canon = False + if re.search(r"_?Starter\d*$", form_name): + canon = False + if re.search(r"_?Altcolor\d*$", form_name): + canon = False + if re.search(r"_?Beta\d*$", form_name): + canon = False + if species_name == "Missingno_": + canon = False + + count = len(species_dict.subgroups) + new_count = "{:04d}".format(count) + species_dict.subgroups[new_count] = TrackerUtils.createFormNode(form_name, canon) + + await msg.channel.send(msg.author.mention + + " Added #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file diff --git a/commands/AddRessourceCredit.py b/commands/AddRessourceCredit.py new file mode 100644 index 0000000..05821f3 --- /dev/null +++ b/commands/AddRessourceCredit.py @@ -0,0 +1,79 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import discord +import TrackerUtils +import SpriteUtils + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class AddRessourceCredit(BaseCommand): + def __init__(self, spritebot: "SpriteBot", ressource_type: str): + super().__init__(spritebot) + self.ressource_type = ressource_type + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return f"add{self.ressource_type}credit" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return f"Adds a new author to the credits of the {self.ressource_type}" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}{self.getCommand()} [Form Name] [Shiny] [Gender]`\n" \ + f"Adds the specified author to the credits of the {self.ressource_type}. " \ + "This makes a post in the submissions channel, asking other approvers to sign off.\n" \ + "`Author ID` - The discord ID of the author to set as primary\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ + "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "@Audino Unown Shiny", + "<@!117780585635643396> Unown Shiny", + "POWERCRISTAL Calyrex", + "POWERCRISTAL Calyrex Shiny", + "POWERCRISTAL Jellicent Shiny Female" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + # compute answer from current status + if len(args) < 2: + await msg.channel.send(msg.author.mention + " Specify a user ID and Pokemon.") + return + + wanted_author = self.spritebot.getFormattedCredit(args[0]) + name_seq = [TrackerUtils.sanitizeName(i) for i in args[1:]] + full_idx = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq, 0) + if full_idx is None: + await msg.channel.send(msg.author.mention + " No such Pokemon.") + return + + chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) + + if chosen_node.__dict__[self.ressource_type + "_credit"].primary == "": + await msg.channel.send(msg.author.mention + " This command only works on filled {0}.".format(self.ressource_type)) + return + + if wanted_author not in self.spritebot.names: + await msg.channel.send(msg.author.mention + " No such profile ID.") + return + + assert(msg.guild is not None) + chat_id = self.spritebot.config.servers[str(msg.guild.id)].submit + if chat_id == 0: + await msg.channel.send(msg.author.mention + " This server does not support submissions.") + return + + submit_channel = self.spritebot.client.get_channel(chat_id) + author = "<@!{0}>".format(msg.author.id) + + base_link = await self.spritebot.retrieveLinkMsg(full_idx, chosen_node, self.ressource_type, False) + base_file, base_name = SpriteUtils.getLinkData(base_link) + + # stage a post in submissions + await self.spritebot.postStagedSubmission(submit_channel, "--addauthor", "", full_idx, chosen_node, self.ressource_type, author + "/" + wanted_author, + False, None, base_file, base_name, None) \ No newline at end of file diff --git a/commands/AutoRecolorRessource.py b/commands/AutoRecolorRessource.py index 50b1a75..7508f4a 100644 --- a/commands/AutoRecolorRessource.py +++ b/commands/AutoRecolorRessource.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, List from .BaseCommand import BaseCommand +from Constants import PermissionLevel import discord import TrackerUtils import SpriteUtils @@ -13,6 +14,9 @@ def __init__(self, spritebot: "SpriteBot", ressource_type: str): super().__init__(spritebot) self.ressource_type = ressource_type + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: return f"autocolor{self.ressource_type}" diff --git a/commands/BaseCommand.py b/commands/BaseCommand.py index abf6a63..09dbe1f 100644 --- a/commands/BaseCommand.py +++ b/commands/BaseCommand.py @@ -1,5 +1,6 @@ from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, List +from Constants import PermissionLevel import discord if TYPE_CHECKING: @@ -9,6 +10,11 @@ class BaseCommand: def __init__(self, spritebot: "SpriteBot") -> None: self.spritebot = spritebot + @abstractmethod + def getRequiredPermission(self) -> PermissionLevel: + """return the permission level required to execute this command""" + raise NotImplementedError() + @abstractmethod def getCommand(self) -> str: """return the command associated with this Class, like "recolorsprite" """ @@ -24,6 +30,9 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: """return a multi-line help for this command""" raise NotImplementedError() + def shouldListInHelp(self) -> bool: + return True + @abstractmethod async def executeCommand(self, msg: discord.Message, args: List[str]): """perform the action of this command following a user’s command""" @@ -31,10 +40,9 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): def generateMultiLineExample(self, prefix: str, examples_args: List[str]) -> str: """ Generate the Examples: section of the multi-line documentation, with each entry in examples_args as a command argument list""" + result = "**Examples**\n" if len(examples_args) == 0: - return "" - else: - result = "**Examples**\n" - for example in examples_args: - result += f"`{prefix}{self.getCommand()} {example}`\n" - return result + examples_args = [""] + for example in examples_args: + result += f"`{prefix}{self.getCommand()} {example}`\n" + return result diff --git a/commands/ClearCache.py b/commands/ClearCache.py new file mode 100644 index 0000000..6c6f9b3 --- /dev/null +++ b/commands/ClearCache.py @@ -0,0 +1,48 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +import discord +import TrackerUtils +from Constants import PermissionLevel + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class ClearCache(BaseCommand): + def __init__(self, spritebot: "SpriteBot") -> None: + self.spritebot = spritebot + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "clearcache" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Clears the image/zip links for a Pokemon/forme/shiny/gender" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}clearcache [Form Name] [Shiny] [Gender]`\n" \ + "Clears the all uploaded images related to a Pokemon, allowing them to be regenerated. " \ + "This includes all portrait image and sprite zip links, " \ + "meant to be used whenever those links somehow become stale.\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ + "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, ["Pikachu", "Pikachu Shiny", "Pikachu Female", "Pikachu Shiny Female", "Shaymin Sky", "Shaymin Sky Shiny"]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + name_seq = [TrackerUtils.sanitizeName(i) for i in args] + full_idx = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq, 0) + + if full_idx is None: + await msg.channel.send(msg.author.mention + " No such Pokemon.") + return + + chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) + + TrackerUtils.clearCache(chosen_node, True) + + self.spritebot.saveTracker() + + await msg.channel.send(msg.author.mention + " Cleared links for #{0:03d}: {1}.".format(int(full_idx[0]), " ".join(name_seq))) \ No newline at end of file diff --git a/commands/DeleteGender.py b/commands/DeleteGender.py new file mode 100644 index 0000000..9bba3b7 --- /dev/null +++ b/commands/DeleteGender.py @@ -0,0 +1,90 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class DeleteGender(BaseCommand): + def getRequiredPermission(self): + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "deletegender" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Removes the female sprite/portrait from the Pokemon" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}deletegender [Pokemon Form]`\n" \ + "Removes the slot for the male/female version of the species, or form of the species. " \ + "Only works if empty.\n" \ + "`Asset Type` - \"sprite\" or \"portrait\"\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "Sprite Venusaur", + "Portrait Steelix", + "Sprite Raichu Alola" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + if len(args) < 2 or len(args) > 3: + await msg.channel.send(msg.author.mention + " Invalid number of args!") + return + + asset_type = args[0].lower() + if asset_type != "sprite" and asset_type != "portrait": + await msg.channel.send(msg.author.mention + " Must specify sprite or portrait!") + return + + species_name = TrackerUtils.sanitizeName(args[1]) + species_idx = TrackerUtils.findSlotIdx(self.spritebot.tracker, species_name) + if species_idx is None: + await msg.channel.send(msg.author.mention + " {0} does not exist!".format(species_name)) + return + + species_dict = self.spritebot.tracker[species_idx] + if len(args) == 2: + # check against not existing + if not TrackerUtils.genderDiffExists(species_dict.subgroups["0000"], asset_type, "Male") and \ + not TrackerUtils.genderDiffExists(species_dict.subgroups["0000"], asset_type, "Female"): + await msg.channel.send(msg.author.mention + " Gender difference doesnt exist for #{0:03d}: {1}!".format(int(species_idx), species_name)) + return + + # check against data population + if TrackerUtils.genderDiffPopulated(species_dict.subgroups["0000"], asset_type): + await msg.channel.send(msg.author.mention + " Gender difference isn't empty for #{0:03d}: {1}!".format(int(species_idx), species_name)) + return + + TrackerUtils.removeGenderDiff(species_dict.subgroups["0000"], asset_type) + await msg.channel.send(msg.author.mention + + " Removed gender difference to #{0:03d}: {1}! ({2})".format(int(species_idx), species_name, asset_type)) + else: + form_name = TrackerUtils.sanitizeName(args[2]) + form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) + if form_idx is None: + await msg.channel.send(msg.author.mention + " {2} doesn't exist within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) + return + + # check against not existing + form_dict = species_dict.subgroups[form_idx] + if not TrackerUtils.genderDiffExists(form_dict, asset_type, "Male") and \ + not TrackerUtils.genderDiffExists(form_dict, asset_type, "Female"): + await msg.channel.send(msg.author.mention + + " Gender difference doesn't exist for #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) + return + + # check against data population + if TrackerUtils.genderDiffPopulated(form_dict, asset_type): + await msg.channel.send(msg.author.mention + " Gender difference isn't empty for #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) + return + + TrackerUtils.removeGenderDiff(form_dict, asset_type) + await msg.channel.send(msg.author.mention + + " Removed gender difference to #{0:03d}: {1} {2}! ({3})".format(int(species_idx), species_name, form_name, asset_type)) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file diff --git a/commands/DeleteNode.py b/commands/DeleteNode.py new file mode 100644 index 0000000..23c238d --- /dev/null +++ b/commands/DeleteNode.py @@ -0,0 +1,77 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord +import os + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class DeleteNode(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "delete" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Deletes an empty Pokemon or forme" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}delete [Form Name]`\n" \ + "Deletes a Pokemon or form of an existing Pokemon. " \ + "Only works if the slot + its children are empty.\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "Pikablu", + "Arceus Mega" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + if len(args) < 1 or len(args) > 2: + await msg.channel.send(msg.author.mention + " Invalid number of args!") + return + + species_name = TrackerUtils.sanitizeName(args[0]) + species_idx = TrackerUtils.findSlotIdx(self.spritebot.tracker, species_name) + if species_idx is None: + await msg.channel.send(msg.author.mention + " {0} does not exist!".format(species_name)) + return + + species_dict = self.spritebot.tracker[species_idx] + if len(args) == 1: + + # check against data population + if TrackerUtils.isDataPopulated(species_dict) and msg.author.id != self.spritebot.config.root: + await msg.channel.send(msg.author.mention + " Can only delete empty slots!") + return + + TrackerUtils.deleteData(self.spritebot.tracker, os.path.join(self.spritebot.config.path, 'sprite'), + os.path.join(self.spritebot.config.path, 'portrait'), species_idx) + + await msg.channel.send(msg.author.mention + " Deleted #{0:03d}: {1}!".format(int(species_idx), species_name)) + else: + + form_name = TrackerUtils.sanitizeName(args[1]) + form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) + if form_idx is None: + await msg.channel.send(msg.author.mention + " {2} doesn't exist within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) + return + + # check against data population + form_dict = species_dict.subgroups[form_idx] + if TrackerUtils.isDataPopulated(form_dict) and msg.author.id != self.spritebot.config.root: + await msg.channel.send(msg.author.mention + " Can only delete empty slots!") + return + + TrackerUtils.deleteData(species_dict.subgroups, os.path.join(self.spritebot.config.path, 'sprite', species_idx), + os.path.join(self.spritebot.config.path, 'portrait', species_idx), form_idx) + + await msg.channel.send(msg.author.mention + " Deleted #{0:03d}: {1} {2}!".format(int(species_idx), species_name, form_name)) + + self.spritebot.saveTracker() + self.spritebot.changed = True + + await self.spritebot.gitCommit("Removed {0}".format(" ".join(args))) \ No newline at end of file diff --git a/commands/DeleteRessourceCredit.py b/commands/DeleteRessourceCredit.py index a013c19..fbea3b1 100644 --- a/commands/DeleteRessourceCredit.py +++ b/commands/DeleteRessourceCredit.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, List from .BaseCommand import BaseCommand +from Constants import PermissionLevel import TrackerUtils import discord import SpriteUtils @@ -12,6 +13,9 @@ def __init__(self, spritebot: "SpriteBot", ressource_type: str): super().__init__(spritebot) self.ressource_type = ressource_type + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: return f"delete{self.ressource_type}credit" @@ -62,7 +66,7 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): await msg.channel.send(msg.author.mention + " No such profile ID.") return - authorized = await self.spritebot.isAuthorized(msg.author, msg.guild) + authorized = (await self.spritebot.getUserPermission(msg.author, msg.guild)).canPerformAction(PermissionLevel.STAFF) author = "<@!{0}>".format(msg.author.id) if not authorized and author != wanted_author: await msg.channel.send(msg.author.mention + " You must specify your own user ID.") @@ -102,10 +106,11 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): await msg.channel.send(msg.author.mention + " The author cannot be the latest contributor.") return - if msg.guild == None: + guild = msg.guild + if guild is None: raise BaseException("The message has not been posted to a guild!") - chat_id = self.spritebot.config.servers[str(msg.guild.id)].submit + chat_id = self.spritebot.config.servers[str(guild.id)].submit if chat_id == 0: await msg.channel.send(msg.author.mention + " This server does not support submissions.") return diff --git a/commands/ForcePush.py b/commands/ForcePush.py new file mode 100644 index 0000000..c1f8282 --- /dev/null +++ b/commands/ForcePush.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class ForcePush(BaseCommand): + def getRequiredPermission(self): + return PermissionLevel.ADMIN + + def getCommand(self) -> str: + return "forcepush" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Commit and push the underlying git repository" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}{self.getCommand()}`\n" \ + f"{self.getSingleLineHelp(server_config)}" + + async def executeCommand(self, msg: discord.Message, args: List[str]): + self.spritebot.generateCreditCompilation() + await self.spritebot.gitCommit("Tracker update from forced push.") + await self.spritebot.gitPush() + await msg.channel.send(msg.author.mention + " Changes pushed.") diff --git a/commands/GetAbsenteeProfiles.py b/commands/GetAbsenteeProfiles.py new file mode 100644 index 0000000..7212a45 --- /dev/null +++ b/commands/GetAbsenteeProfiles.py @@ -0,0 +1,33 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class GetAbsenteeProfiles(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + + def getCommand(self) -> str: + return "absentprofiles" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "List the absentees profiles (those not linked to a Discord account)" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}{self.getCommand()}`\n" \ + f"{self.getSingleLineHelp(server_config)}\n" \ + + self.generateMultiLineExample( + server_config.prefix, + [] + ) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + total_names = ["Absentee profiles:"] + msg_ids = [] # type: ignore + for name in self.spritebot.names: + if not name.startswith("<@!"): + total_names.append(name + "\nName: \"{0}\" Contact: \"{1}\"".format(self.spritebot.names[name].name, self.spritebot.names[name].contact)) + await self.spritebot.sendInfoPosts(msg.channel, total_names, msg_ids, 0) \ No newline at end of file diff --git a/commands/GetProfile.py b/commands/GetProfile.py index f8344d6..9fa1bfe 100644 --- a/commands/GetProfile.py +++ b/commands/GetProfile.py @@ -1,5 +1,6 @@ from abc import ABCMeta, abstractmethod from .BaseCommand import BaseCommand +from Constants import PermissionLevel from typing import TYPE_CHECKING, List import discord @@ -7,6 +8,9 @@ from SpriteBot import SpriteBot, BotServer class GetProfile(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: return "profile" diff --git a/commands/ListBounties.py b/commands/ListBounties.py new file mode 100644 index 0000000..26dd110 --- /dev/null +++ b/commands/ListBounties.py @@ -0,0 +1,81 @@ +from typing import TYPE_CHECKING, List, Tuple +from .BaseCommand import BaseCommand +import TrackerUtils +from Constants import PermissionLevel, MESSAGE_BOUNTIES_DISABLED, PHASES +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class ListBounties(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + + def getCommand(self) -> str: + return "bounties" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "View top bounties" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + if self.spritebot.config.use_bounties: + return f"`{server_config.prefix}{self.getCommand()} [Type]`\n" \ + "View the top sprites/portraits that have bounties placed on them. " \ + "You will claim a bounty when you successfully submit that sprite/portrait.\n" \ + "`Type` - [Optional] Can be `sprite` or `portrait`\n" \ + + self.generateMultiLineExample( + server_config.prefix, + [ + "", + "sprite" + ] + ) + else: + return MESSAGE_BOUNTIES_DISABLED + + def shouldListInHelp(self) -> bool: + return self.spritebot.config.use_bounties + + async def executeCommand(self, msg: discord.Message, args: List[str]): + if not self.spritebot.config.use_bounties: + await msg.channel.send(msg.author.mention + " " + MESSAGE_BOUNTIES_DISABLED) + return + + include_sprite = True + include_portrait = True + + if len(args) > 0: + if args[0].lower() == "sprite": + include_portrait = False + elif args[0].lower() == "portrait": + include_sprite = False + else: + await msg.channel.send(msg.author.mention + " Use 'sprite' or 'portrait' as argument.") + return + + entries: List[Tuple[int, str, str, int]] = [] + over_dict = TrackerUtils.initSubNode("", True) + over_dict.subgroups = self.spritebot.tracker + + if include_sprite: + self.spritebot.getBountiesFromDict("sprite", over_dict, entries, []) + if include_portrait: + self.spritebot.getBountiesFromDict("portrait", over_dict, entries, []) + + entries = sorted(entries, reverse=True) + entries = entries[:10] + + posts = [] + if include_sprite and include_portrait: + posts.append("**Top Bounties**") + elif include_sprite: + posts.append("**Top Bounties for Sprites**") + else: + posts.append("**Top Bounties for Portraits**") + for entry in entries: + posts.append("#{0:02d}. {2} for **{1}GP**, paid when the {3} becomes {4}.".format(len(posts), entry[0], entry[1], entry[2], PHASES[entry[3]].title())) + + if len(posts) == 1: + posts.append("[None]") + + msgs_used, changed = await self.spritebot.sendInfoPosts(msg.channel, posts, [], 0) \ No newline at end of file diff --git a/commands/ListRessource.py b/commands/ListRessource.py index 0909a67..fff5ec6 100644 --- a/commands/ListRessource.py +++ b/commands/ListRessource.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, List from .BaseCommand import BaseCommand +from Constants import PermissionLevel import discord import TrackerUtils @@ -11,6 +12,9 @@ def __init__(self, spritebot: "SpriteBot", ressource_type: str): super().__init__(spritebot) self.ressource_type = ressource_type + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: return f"list{self.ressource_type}" diff --git a/commands/MoveNode.py b/commands/MoveNode.py new file mode 100644 index 0000000..adedee4 --- /dev/null +++ b/commands/MoveNode.py @@ -0,0 +1,134 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import SpriteUtils +import TrackerUtils +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + + +class MoveNode(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "move" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Swaps the sprites, portraits, and names for two Pokemon/formes" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}move [Pokemon Form] -> [Pokemon Form 2]`\n" \ + "Swaps the name, sprites, and portraits of one slot with another. " \ + "This can only be done with Pokemon or formes, and the swap is recursive to shiny/genders. " \ + "Good for promoting alternate forms to base form, temp Pokemon to newly revealed dex numbers, " \ + "or just fixing mistakes.\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "Escavalier -> Accelgor", + "Zoroark Alternate -> Zoroark", + "Missingno_ Kleavor -> Kleavor", + "Minior Blue -> Minior Indigo" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + try: + delim_idx = args.index("->") + except: + await msg.channel.send(msg.author.mention + " Command needs to separate the source and destination with `->`.") + return + + name_args_from = args[:delim_idx] + name_args_to = args[delim_idx+1:] + + name_seq_from = [TrackerUtils.sanitizeName(i) for i in name_args_from] + full_idx_from = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq_from, 0) + if full_idx_from is None: + await msg.channel.send(msg.author.mention + " No such Pokemon specified as source.") + return + if len(full_idx_from) > 2: + await msg.channel.send(msg.author.mention + " Can move only species or form. Source specified more than that.") + return + + name_seq_to = [TrackerUtils.sanitizeName(i) for i in name_args_to] + full_idx_to = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq_to, 0) + if full_idx_to is None: + await msg.channel.send(msg.author.mention + " No such Pokemon specified as destination.") + return + if len(full_idx_to) > 2: + await msg.channel.send(msg.author.mention + " Can move only species or form. Destination specified more than that.") + return + + chosen_node_from = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx_from, 0) + chosen_node_to = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx_to, 0) + + if chosen_node_from == chosen_node_to: + await msg.channel.send(msg.author.mention + " Cannot move to the same location.") + return + + explicit_idx_from = full_idx_from.copy() + if len(explicit_idx_from) < 2: + explicit_idx_from.append("0000") + explicit_idx_to = full_idx_to.copy() + if len(explicit_idx_to) < 2: + explicit_idx_to.append("0000") + + explicit_node_from = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx_from, 0) + explicit_node_to = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx_to, 0) + assert explicit_node_from is not None + assert explicit_node_to is not None + + # check the main nodes + try: + await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, "sprite") + await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, "portrait") + except SpriteUtils.SpriteVerifyError as e: + await msg.channel.send(msg.author.mention + " Cannot move the locked Pokemon specified as source:\n{0}".format(e.message)) + return + + try: + await self.spritebot.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, "sprite") + await self.spritebot.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, "portrait") + except SpriteUtils.SpriteVerifyError as e: + await msg.channel.send(msg.author.mention + " Cannot move the locked Pokemon specified as destination:\n{0}".format(e.message)) + return + + # check the subnodes + for sub_idx in explicit_node_from.subgroups: + sub_node = explicit_node_from.subgroups[sub_idx] + if TrackerUtils.hasLock(sub_node, "sprite", True) or TrackerUtils.hasLock(sub_node, "portrait", True): + await msg.channel.send(msg.author.mention + " Cannot move the locked subgroup specified as source.") + return + for sub_idx in explicit_node_to.subgroups: + sub_node = explicit_node_to.subgroups[sub_idx] + if TrackerUtils.hasLock(sub_node, "sprite", True) or TrackerUtils.hasLock(sub_node, "portrait", True): + await msg.channel.send(msg.author.mention + " Cannot move the locked subgroup specified as destination.") + return + + # clear caches + TrackerUtils.clearCache(chosen_node_from, True) + TrackerUtils.clearCache(chosen_node_to, True) + + # perform the swap + TrackerUtils.swapFolderPaths(self.spritebot.config.path, self.spritebot.tracker, "sprite", full_idx_from, full_idx_to) + TrackerUtils.swapFolderPaths(self.spritebot.config.path, self.spritebot.tracker, "portrait", full_idx_from, full_idx_to) + TrackerUtils.swapNodeMiscFeatures(chosen_node_from, chosen_node_to) + + # then, swap the subnodes + TrackerUtils.swapAllSubNodes(self.spritebot.config.path, self.spritebot.tracker, explicit_idx_from, explicit_idx_to) + + await msg.channel.send(msg.author.mention + " Swapped {0} with {1}.".format(" ".join(name_seq_from), " ".join(name_seq_to))) + # if the source is empty in sprite and portrait, and its subunits are empty in sprite and portrait + # remind to delete + if not TrackerUtils.isDataPopulated(chosen_node_from): + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_to))) + if not TrackerUtils.isDataPopulated(chosen_node_to): + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_from))) + + self.spritebot.saveTracker() + self.spritebot.changed = True + + await self.spritebot.gitCommit("Swapped {0} with {1} recursively".format(" ".join(name_seq_from), " ".join(name_seq_to))) \ No newline at end of file diff --git a/commands/MoveRessource.py b/commands/MoveRessource.py new file mode 100644 index 0000000..188bcce --- /dev/null +++ b/commands/MoveRessource.py @@ -0,0 +1,106 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import SpriteUtils +import TrackerUtils +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class MoveRessource(BaseCommand): + def __init__(self, spritebot: "SpriteBot", ressource_type: str): + super().__init__(spritebot) + self.ressource_type = ressource_type + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return f"move{self.ressource_type}" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return f"Swaps the {self.ressource_type}s for two Pokemon/formes" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}move{self.ressource_type} [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ + f"Swaps the contents of one {self.ressource_type} with another. " \ + "Good for promoting alternates to main, temp Pokemon to newly revealed dex numbers, " \ + "or just fixing mistakes.\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + f"`Shiny` - [Optional] Specifies if you want the shiny {self.ressource_type} or not\n" \ + "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "Escavalier -> Accelgor", + "Zoroark Alternate -> Zoroark", + "Missingno_ Kleavor -> Kleavor", + "Minior Blue -> Minior Indigo" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + try: + delim_idx = args.index("->") + except: + await msg.channel.send(msg.author.mention + " Command needs to separate the source and destination with `->`.") + return + + name_args_from = args[:delim_idx] + name_args_to = args[delim_idx+1:] + + name_seq_from = [TrackerUtils.sanitizeName(i) for i in name_args_from] + full_idx_from = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq_from, 0) + if full_idx_from is None: + await msg.channel.send(msg.author.mention + " No such Pokemon specified as source.") + return + + name_seq_to = [TrackerUtils.sanitizeName(i) for i in name_args_to] + full_idx_to = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq_to, 0) + if full_idx_to is None: + await msg.channel.send(msg.author.mention + " No such Pokemon specified as destination.") + return + + chosen_node_from = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx_from, 0) + chosen_node_to = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx_to, 0) + + if chosen_node_from == chosen_node_to: + await msg.channel.send(msg.author.mention + " Cannot move to the same location.") + return + + if not chosen_node_from.__dict__[self.ressource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when source {0} is unneeded.".format(self.ressource_type)) + return + if not chosen_node_to.__dict__[self.ressource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when destination {0} is unneeded.".format(self.ressource_type)) + return + + try: + await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, self.ressource_type) + except SpriteUtils.SpriteVerifyError as e: + await msg.channel.send(msg.author.mention + " Cannot move the locked Pokemon specified as source:\n{0}".format(e.message)) + return + + try: + await self.spritebot.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, self.ressource_type) + except SpriteUtils.SpriteVerifyError as e: + await msg.channel.send(msg.author.mention + " Cannot move the locked Pokemon specified as destination:\n{0}".format(e.message)) + return + + # clear caches + TrackerUtils.clearCache(chosen_node_from, True) + TrackerUtils.clearCache(chosen_node_to, True) + + TrackerUtils.swapFolderPaths(self.spritebot.config.path, self.spritebot.tracker, self.ressource_type, full_idx_from, full_idx_to) + + await msg.channel.send(msg.author.mention + " Swapped {0} with {1}.".format(" ".join(name_seq_from), " ".join(name_seq_to))) + # if the source is empty in sprite and portrait, and its subunits are empty in sprite and portrait + # remind to delete + if not TrackerUtils.isDataPopulated(chosen_node_from): + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_from))) + if not TrackerUtils.isDataPopulated(chosen_node_to): + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_to))) + + self.spritebot.saveTracker() + self.spritebot.changed = True + + await self.spritebot.gitCommit("Swapped {0} with {1}".format(" ".join(name_seq_from), " ".join(name_seq_to))) \ No newline at end of file diff --git a/commands/QueryRessourceCredit.py b/commands/QueryRessourceCredit.py index d5274cb..5ed8036 100644 --- a/commands/QueryRessourceCredit.py +++ b/commands/QueryRessourceCredit.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, List from .BaseCommand import BaseCommand +from Constants import PermissionLevel import TrackerUtils import discord import io @@ -13,6 +14,9 @@ def __init__(self, spritebot: "SpriteBot", ressource_type: str, display_history: self.ressource_type = ressource_type self.display_history = display_history + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: if self.display_history: return f"{self.ressource_type}history" @@ -113,6 +117,6 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): file_data = io.StringIO() file_data.write(credit_str) file_data.seek(0) - await msg.channel.send(response, file=discord.File(file_data, 'credit_msg.txt')) + await msg.channel.send(response, file=discord.File(file_data, 'credit_msg.txt')) # type: ignore else: await msg.channel.send(response + "```" + credit_str + "```") \ No newline at end of file diff --git a/commands/QueryRessourceStatus.py b/commands/QueryRessourceStatus.py index e071acb..62a506c 100644 --- a/commands/QueryRessourceStatus.py +++ b/commands/QueryRessourceStatus.py @@ -2,7 +2,7 @@ from .BaseCommand import BaseCommand import discord import TrackerUtils -from Constants import PHASES +from Constants import PHASES, PermissionLevel if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer @@ -13,6 +13,9 @@ def __init__(self, spritebot: "SpriteBot", ressource_type: str, is_derivation: b self.ressource_type = ressource_type self.is_derivation = is_derivation + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: if self.is_derivation: return f"recolor{self.ressource_type}" @@ -82,6 +85,7 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): recolor_shiny = True chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) + assert chosen_node is not None # post the statuses response = msg.author.mention + " " status = TrackerUtils.getStatusEmoji(chosen_node, self.ressource_type) diff --git a/commands/RenameNode.py b/commands/RenameNode.py new file mode 100644 index 0000000..708385f --- /dev/null +++ b/commands/RenameNode.py @@ -0,0 +1,72 @@ +from typing import List, TYPE_CHECKING +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import discord +import TrackerUtils + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class RenameNode(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "rename" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Renames a Pokemon or forme" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}rename [Form Name] `\n" \ + "Changes the existing species or form to the new name.\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + "`New Name` - New Pokemon of Form name\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "Calrex Calyrex", + "Vulpix Aloha Alola" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + if len(args) < 2 or len(args) > 3: + await msg.channel.send(msg.author.mention + " Invalid number of args!") + return + + species_name = TrackerUtils.sanitizeName(args[0]) + new_name = TrackerUtils.sanitizeName(args[-1]) + species_idx = TrackerUtils.findSlotIdx(self.spritebot.tracker, species_name) + if species_idx is None: + await msg.channel.send(msg.author.mention + " {0} does not exist!".format(species_name)) + return + + species_dict = self.spritebot.tracker[species_idx] + + if len(args) == 2: + new_species_idx = TrackerUtils.findSlotIdx(self.spritebot.tracker, new_name) + if new_species_idx is not None: + await msg.channel.send(msg.author.mention + " #{0:03d}: {1} already exists!".format(int(new_species_idx), new_name)) + return + + species_dict.name = new_name + await msg.channel.send(msg.author.mention + " Changed #{0:03d}: {1} to {2}!".format(int(species_idx), species_name, new_name)) + else: + + form_name = TrackerUtils.sanitizeName(args[1]) + form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, form_name) + if form_idx is None: + await msg.channel.send(msg.author.mention + " {2} doesn't exist within #{0:03d}: {1}!".format(int(species_idx), species_name, form_name)) + return + + new_form_idx = TrackerUtils.findSlotIdx(species_dict.subgroups, new_name) + if new_form_idx is not None: + await msg.channel.send(msg.author.mention + " {2} already exists within #{0:03d}: {1}!".format(int(species_idx), species_name, new_name)) + return + + form_dict = species_dict.subgroups[form_idx] + form_dict.name = new_name + + await msg.channel.send(msg.author.mention + " Changed {2} to {3} in #{0:03d}: {1}!".format(int(species_idx), species_name, form_name, new_name)) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file diff --git a/commands/ReplaceRessource.py b/commands/ReplaceRessource.py new file mode 100644 index 0000000..3ea265c --- /dev/null +++ b/commands/ReplaceRessource.py @@ -0,0 +1,97 @@ +from typing import List, TYPE_CHECKING +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import discord +import TrackerUtils +import SpriteUtils + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class ReplaceRessource(BaseCommand): + def __init__(self, spritebot: "SpriteBot", ressource_type: str): + super().__init__(spritebot) + self.ressource_type = ressource_type + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return f"replace{self.ressource_type}" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return f"Replace the content of one {self.ressource_type} with another" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}{self.getCommand()} [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ + "Replaces the contents of one {self.ressource_type} with another. " \ + "Good for promoting scratch-made alternates to main.\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ + "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, ["Zoroark Alternate -> Zoroark"]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + try: + delim_idx = args.index("->") + except: + await msg.channel.send(msg.author.mention + " Command needs to separate the source and destination with `->`.") + return + + name_args_from = args[:delim_idx] + name_args_to = args[delim_idx+1:] + + name_seq_from = [TrackerUtils.sanitizeName(i) for i in name_args_from] + full_idx_from = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq_from, 0) + if full_idx_from is None: + await msg.channel.send(msg.author.mention + " No such Pokemon specified as source.") + return + + name_seq_to = [TrackerUtils.sanitizeName(i) for i in name_args_to] + full_idx_to = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq_to, 0) + if full_idx_to is None: + await msg.channel.send(msg.author.mention + " No such Pokemon specified as destination.") + return + + chosen_node_from = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx_from, 0) + chosen_node_to = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx_to, 0) + + if chosen_node_from == chosen_node_to: + await msg.channel.send(msg.author.mention + " Cannot move to the same location.") + return + + if not chosen_node_from.__dict__[self.ressource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when source {0} is unneeded.".format(self.ressource_type)) + return + if not chosen_node_to.__dict__[self.ressource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when destination {0} is unneeded.".format(self.ressource_type)) + return + + try: + await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, self.ressource_type) + except SpriteUtils.SpriteVerifyError as e: + await msg.channel.send(msg.author.mention + " Cannot move out the locked Pokemon specified as source:\n{0}".format(e.message)) + return + + try: + await self.spritebot.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, self.ressource_type) + except SpriteUtils.SpriteVerifyError as e: + await msg.channel.send(msg.author.mention + " Cannot replace the locked Pokemon specified as destination:\n{0}".format(e.message)) + return + + # clear caches + TrackerUtils.clearCache(chosen_node_from, True) + TrackerUtils.clearCache(chosen_node_to, True) + + TrackerUtils.replaceFolderPaths(self.spritebot.config.path, self.spritebot.tracker, self.ressource_type, full_idx_from, full_idx_to) + + await msg.channel.send(msg.author.mention + " Replaced {0} with {1}.".format(" ".join(name_seq_to), " ".join(name_seq_from))) + # if the source is empty in sprite and portrait, and its subunits are empty in sprite and portrait + # remind to delete + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `!delete` if it is no longer needed.".format(" ".join(name_seq_from))) + + self.spritebot.saveTracker() + self.spritebot.changed = True + + await self.spritebot.gitCommit("Replaced {0} with {1}".format(" ".join(name_seq_to), " ".join(name_seq_from))) \ No newline at end of file diff --git a/commands/Rescan.py b/commands/Rescan.py new file mode 100644 index 0000000..4912903 --- /dev/null +++ b/commands/Rescan.py @@ -0,0 +1,30 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import SpriteUtils +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class Rescan(BaseCommand): + def getRequiredPermission(self): + return PermissionLevel.ADMIN + + def getCommand(self) -> str: + return "rescan" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Rescan the data (if not commented out in the code)" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}rescan`\n" \ + f"{self.getSingleLineHelp(server_config)}\n" \ + + self.generateMultiLineExample(server_config.prefix, [""]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + #SpriteUtils.iterateTracker(self.spritebot.tracker, self.spritebot.markPortraitFull, []) + #self.spritebot.changed = True + #self.spritebot.saveTracker() + #await msg.channel.send(msg.author.mention + " Rescan complete.") + await msg.channel.send(msg.author.mention + " Rescan disabled in the code") \ No newline at end of file diff --git a/commands/SetNeedNode.py b/commands/SetNeedNode.py new file mode 100644 index 0000000..c35d32a --- /dev/null +++ b/commands/SetNeedNode.py @@ -0,0 +1,74 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class SetNeedNode(BaseCommand): + def __init__(self, spritebot: "SpriteBot", needed: bool) -> None: + self.spritebot = spritebot + self.needed = needed + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + if self.needed: + return "need" + else: + return "dontneed" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + if self.needed: + return "Marks a sprite/portrait as needed" + else: + return "Marks a sprite/portrait as unneeded" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + if self.needed: + description = "Marks a sprite/portrait as Needed. This is the default for all sprites/portraits." + else: + description = "Marks a sprite/portrait as Unneeded. " \ + "Unneeded sprites/portraits are marked with \u26AB and do not need submissions." + return f"`{server_config.prefix}{self.getCommand()} [Pokemon Form] [Shiny]`\n" \ + + description + "\n" \ + + "`Asset Type` - \"sprite\" or \"portrait\"\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "Sprite Venusaur", + "Portrait Steelix", + "Portrait Minior Red", + "Portrait Minior Shiny", + "Sprite Alcremie Shiny" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + if len(args) < 2 or len(args) > 5: + await msg.channel.send(msg.author.mention + " Invalid number of args!") + return + + asset_type = args[0].lower() + if asset_type != "sprite" and asset_type != "portrait": + await msg.channel.send(msg.author.mention + " Must specify sprite or portrait!") + return + + name_seq = [TrackerUtils.sanitizeName(i) for i in args[1:]] + full_idx = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq, 0) + if full_idx is None: + await msg.channel.send(msg.author.mention + " No such Pokemon.") + return + chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) + chosen_node.__dict__[asset_type + "_required"] = self.needed + + if self.needed: + await msg.channel.send(msg.author.mention + " {0} {1} is now needed.".format(asset_type, " ".join(name_seq))) + else: + await msg.channel.send(msg.author.mention + " {0} {1} is no longer needed.".format(asset_type, " ".join(name_seq))) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file diff --git a/commands/SetNodeCanon.py b/commands/SetNodeCanon.py new file mode 100644 index 0000000..bbbc349 --- /dev/null +++ b/commands/SetNodeCanon.py @@ -0,0 +1,57 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class SetNodeCanon(BaseCommand): + def __init__(self, spritebot: "SpriteBot", canon: bool): + super().__init__(spritebot) + self.canon = canon + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.ADMIN + + def getCommand(self) -> str: + if self.canon: + return "canon" + else: + return "uncanon" + + def getCanonOrUncanon(self) -> str: + if self.canon: + return "canon" + else: + return "uncanon" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return f"Mark a Pokémon as {self.getCanonOrUncanon()}" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}{self.getCommand()} [Pokemon Form] [Shiny] [Gender]`\n" \ + f"{self.getSingleLineHelp(server_config)}\n" \ + + self.generateMultiLineExample( + server_config.prefix, + [ + "Pikachu" + ] + ) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + name_seq = [TrackerUtils.sanitizeName(i) for i in args] + full_idx = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq, 0) + if full_idx is None: + await msg.channel.send(msg.author.mention + " No such Pokemon.") + return + chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) + + TrackerUtils.setCanon(chosen_node, self.canon) + + # set to complete + await msg.channel.send(msg.author.mention + " {0} is now {1}.".format(" ".join(name_seq), self.getCanonOrUncanon())) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file diff --git a/commands/SetProfile.py b/commands/SetProfile.py new file mode 100644 index 0000000..52e1314 --- /dev/null +++ b/commands/SetProfile.py @@ -0,0 +1,83 @@ +from typing import List, TYPE_CHECKING +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import discord +import TrackerUtils + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class SetProfile(BaseCommand): + def __init__(self, spritebot: "SpriteBot", isStaffCommand: bool): + super().__init__(spritebot) + self.isStaffCommand = isStaffCommand + + def getRequiredPermission(self) -> PermissionLevel: + if self.isStaffCommand: + return PermissionLevel.STAFF + else: + return PermissionLevel.EVERYONE + + def getCommand(self) -> str: + if self.isStaffCommand: + return "forceregister" + else: + return "register" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + if self.isStaffCommand: + return "Set someone's profile" + else: + return "Set your profile" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + if self.isStaffCommand: + admin_examples = [ + "SUGIMORI Sugimori https://twitter.com/SUPER_32X", + "@Audino Audino https://github.com/audinowho", + "<@!117780585635643396> Audino https://github.com/audinowho" + ] + return f"`{server_config.prefix}forceregister `\n" \ + "Registers an absentee profile with name and contact info for crediting purposes. " \ + "If a discord ID is provided, the profile is force-edited " \ + "(can be used to remove inappropriate content)." \ + "This command is also available for self-registration. " \ + f"Check the `{server_config.prefix}register` version for more.\n" \ + "`Author ID` - The desired ID of the absentee profile\n" \ + "`Name` - The person's preferred name\n" \ + "`Contact` - The person's preferred contact info\n" \ + + self.generateMultiLineExample(server_config.prefix, admin_examples) + else: + return f"`{server_config.prefix}register `\n" \ + "Registers your name and contact info for crediting purposes. " \ + "If you do not register, credits will be given to your discord ID instead.\n" \ + "`Name` - Your preferred name\n" \ + "`Contact` - Your preferred contact info; can be email, url, etc.\n" \ + + self.generateMultiLineExample(server_config.prefix, ["Audino https://github.com/audinowho"]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + entry_key = "<@!{0}>".format(msg.author.id) + + if self.isStaffCommand: + if len(args) != 3: + await msg.channel.send(msg.author.mention + " Require 3 arguments") + return + entry_key = self.spritebot.getFormattedCredit(args[0]) + new_credit = TrackerUtils.CreditEntry(args[1], args[2]) + else: + if len(args) == 0: + new_credit = TrackerUtils.CreditEntry("", "") + elif len(args) == 1: + new_credit = TrackerUtils.CreditEntry(args[0], "") + elif len(args) == 2: + new_credit = TrackerUtils.CreditEntry(args[0], args[1]) + else: + await msg.channel.send(msg.author.mention + " Invalid amounts of arguments") + + if entry_key in self.spritebot.names: + new_credit.sprites = self.spritebot.names[entry_key].sprites + new_credit.portraits = self.spritebot.names[entry_key].portraits + self.spritebot.names[entry_key] = new_credit + self.spritebot.saveNames() + + await msg.channel.send(entry_key + " registered profile:\nName: \"{0}\" Contact: \"{1}\"".format(self.spritebot.names[entry_key].name, self.spritebot.names[entry_key].contact)) \ No newline at end of file diff --git a/commands/SetRessourceCompletion.py b/commands/SetRessourceCompletion.py new file mode 100644 index 0000000..7a0f10e --- /dev/null +++ b/commands/SetRessourceCompletion.py @@ -0,0 +1,100 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord +from Constants import PHASES + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class SetRessourceCompletion(BaseCommand): + def __init__(self, spritebot: "SpriteBot", ressource_type: str, completion: int): + super().__init__(spritebot) + self.ressource_type = ressource_type + #TODO: completion should eventually be replaced by a class or enum once better in-memory ressource typing is implemented. + self.completion = completion + + def getRequiredPermission(self): + return PermissionLevel.STAFF + + def getCompletionName(self) -> str: + if self.completion == TrackerUtils.PHASE_INCOMPLETE: + return "Incomplete" + elif self.completion == TrackerUtils.PHASE_EXISTS: + return "Available" + elif self.completion == TrackerUtils.PHASE_FULL: + return "Fully Featured" + else: + raise NotImplementedError() + + def getCompletionEmoji(self) -> str: + if self.completion == TrackerUtils.PHASE_INCOMPLETE: + return "\u26AA" + elif self.completion == TrackerUtils.PHASE_EXISTS: + return "\u2705" + elif self.completion == TrackerUtils.PHASE_FULL: + return "\u2B50" + else: + raise NotImplementedError() + + def getCompletionCommandCode(self) -> str: + if self.completion == TrackerUtils.PHASE_INCOMPLETE: + return "wip" + elif self.completion == TrackerUtils.PHASE_EXISTS: + return "exists" + elif self.completion == TrackerUtils.PHASE_FULL: + return "filled" + else: + raise NotImplementedError() + + def getCommand(self) -> str: + return self.ressource_type + self.getCompletionCommandCode() + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return f"Set the {self.ressource_type} status to {self.getCompletionName()}" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}{self.getCommand()} [Form Name] [Shiny] [Gender]`\n" \ + f"Manually sets the {self.ressource_type} status as {self.getCompletionEmoji()} {self.getCompletionName()}.\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + f"`Shiny` - [Optional] Specifies if you want the shiny {self.ressource_type} or not\n" \ + "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ + + self.generateMultiLineExample( + server_config.prefix, + [ + "Pikachu", + "Pikachu Shiny", + "Pikachu Female", + "Pikachu Shiny Female", + "Shaymin Sky", + "Shaymin Sky Shiny" + ] + ) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + name_seq = [TrackerUtils.sanitizeName(i) for i in args] + full_idx = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq, 0) + if full_idx is None: + await msg.channel.send(msg.author.mention + " No such Pokemon.") + return + chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) + + phase_str = PHASES[self.completion] + + # if the node has no credit, fail + if chosen_node.__dict__[self.ressource_type + "_credit"].primary == "" and self.completion > TrackerUtils.PHASE_INCOMPLETE: + status = TrackerUtils.getStatusEmoji(chosen_node, self.ressource_type) + await msg.channel.send(msg.author.mention + + " {0} #{1:03d}: {2} has no data and cannot be marked {3}.".format(status, int(full_idx[0]), " ".join(name_seq), phase_str)) + return + + # set to complete + chosen_node.__dict__[self.ressource_type + "_complete"] = self.completion + + status = TrackerUtils.getStatusEmoji(chosen_node, self.ressource_type) + await msg.channel.send(msg.author.mention + " {0} #{1:03d}: {2} marked as {3}.".format(status, int(full_idx[0]), " ".join(name_seq), phase_str)) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file diff --git a/commands/SetRessourceCredit.py b/commands/SetRessourceCredit.py new file mode 100644 index 0000000..6367668 --- /dev/null +++ b/commands/SetRessourceCredit.py @@ -0,0 +1,80 @@ +from typing import List, TYPE_CHECKING +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import discord +import TrackerUtils + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class SetRessourceCredit(BaseCommand): + def __init__(self, spritebot: "SpriteBot", ressource_type: str): + super().__init__(spritebot) + self.ressource_type = ressource_type + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "set{}credit".format(self.ressource_type) + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Sets the primary author of the {}".format(self.ressource_type) + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}set{self.ressource_type}credit [Form Name] [Shiny] [Gender]`\n" \ + f"Manually sets the primary author of a {self.ressource_type} to the specified author. " \ + f"The specified author must already exist in the credits for the {self.ressource_type}.\n" \ + "`Author ID` - The discord ID of the author to set as primary\n" \ + "`Pokemon Name` - Name of the Pokemon\n" \ + "`Form Name` - [Optional] Form name of the Pokemon\n" \ + f"`Shiny` - [Optional] Specifies if you want the shiny {self.ressource_type} or not\n" \ + "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "@Audino Unown Shiny", + "<@!117780585635643396> Unown Shiny", + "POWERCRISTAL Calyrex", + "POWERCRISTAL Calyrex Shiny", + "POWERCRISTAL Jellicent Shiny Female" + ]) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + + # compute answer from current status + if len(args) < 2: + await msg.channel.send(msg.author.mention + " Specify a user ID and Pokemon.") + return + + wanted_author = self.spritebot.getFormattedCredit(args[0]) + name_seq = [TrackerUtils.sanitizeName(i) for i in args[1:]] + full_idx = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq, 0) + if full_idx is None: + await msg.channel.send(msg.author.mention + " No such Pokemon.") + return + + chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) + + if chosen_node.__dict__[self.ressource_type + "_credit"].primary == "": + await msg.channel.send(msg.author.mention + " No credit found.") + return + gen_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.ressource_type, full_idx) + + credit_entries = TrackerUtils.getCreditEntries(gen_path) + + if wanted_author not in credit_entries: + await msg.channel.send(msg.author.mention + " Could not find ID `{0}` in credits for {1}.".format(wanted_author, self.ressource_type)) + return + + # make the credit array into the most current author by itself + credit_data = chosen_node.__dict__[self.ressource_type + "_credit"] + if credit_data.primary == "CHUNSOFT": + await msg.channel.send(msg.author.mention + " Cannot reset credit for a CHUNSOFT {0}.".format(self.ressource_type)) + return + + credit_data.primary = wanted_author + TrackerUtils.updateCreditFromEntries(credit_data, credit_entries) + + await msg.channel.send(msg.author.mention + " Credit display has been reset for {0} {1}:\n{2}".format(self.ressource_type, " ".join(name_seq), self.spritebot.createCreditBlock(credit_data, None))) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file diff --git a/commands/SetRessourceLock.py b/commands/SetRessourceLock.py new file mode 100644 index 0000000..64dc487 --- /dev/null +++ b/commands/SetRessourceLock.py @@ -0,0 +1,89 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class SetRessourceLock(BaseCommand): + def __init__(self, spritebot: "SpriteBot", ressource_type: str, lock: bool): + super().__init__(spritebot) + self.ressource_type = ressource_type + self.lock = lock + + def getRequiredPermission(self): + return PermissionLevel.ADMIN + + def getCommand(self) -> str: + if self.lock: + return "lock" + self.ressource_type + else: + return "unlock" + self.ressource_type + + def getEmotionOrActionText(self) -> str: + if self.ressource_type == "sprite": + return "action" + else: + return "emotion" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + + if self.lock: + return f"Mark a {self.ressource_type} {self.getEmotionOrActionText()}as locked" + else: + return f"Mark a {self.ressource_type} {self.getEmotionOrActionText()} as unlocked" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + if self.lock: + action = "lock" + description = "Set a so it cannot be modified" + else: + action = "unlock" + description = "so it can be modified again" + + if self.ressource_type == "sprite": + example = [ + "Pikachu pose", + "Pikachu Female wake" + ] + else: + example = [ + "Pikachu happy", + "Pikachu Female normal" + ] + + return f"`{server_config.prefix}{self.getCommand()} [Pokemon Form] [Shiny] [Gender] `\n" \ + f"Set a {self.ressource_type} {self.getEmotionOrActionText()} {description}. \n" \ + + self.generateMultiLineExample(server_config.prefix, example) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + name_seq = [TrackerUtils.sanitizeName(i) for i in args[:-1]] + full_idx = TrackerUtils.findFullTrackerIdx(self.spritebot.tracker, name_seq, 0) + if full_idx is None: + await msg.channel.send(msg.author.mention + " No such Pokemon.") + return + chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) + + file_name = args[-1] + for k in chosen_node.__dict__[self.ressource_type + "_files"]: + if file_name.lower() == k.lower(): + file_name = k + break + + if file_name not in chosen_node.__dict__[self.ressource_type + "_files"]: + await msg.channel.send(msg.author.mention + " Specify a Pokemon and an existing emotion/animation.") + return + chosen_node.__dict__[self.ressource_type + "_files"][file_name] = self.lock + + status = TrackerUtils.getStatusEmoji(chosen_node, self.ressource_type) + + lock_str = "unlocked" + if self.lock: + lock_str = "locked" + # set to complete + await msg.channel.send(msg.author.mention + " {0} #{1:03d}: {2} {3} is now {4}.".format(status, int(full_idx[0]), " ".join(name_seq), file_name, lock_str)) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file diff --git a/commands/Shutdown.py b/commands/Shutdown.py new file mode 100644 index 0000000..016fd65 --- /dev/null +++ b/commands/Shutdown.py @@ -0,0 +1,33 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import discord + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class Shutdown(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.ADMIN + + def getCommand(self) -> str: + return "shutdown" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Stop the bot" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}{self.getCommand()}`\n" \ + f"{self.getSingleLineHelp(server_config)}\n" \ + + self.generateMultiLineExample( + server_config.prefix, + [] + ) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + guild = msg.guild + if guild is not None: + resp_ch = self.spritebot.getChatChannel(guild.id) + await resp_ch.send("Shutting down.") + self.spritebot.saveConfig() + await self.spritebot.client.close() \ No newline at end of file diff --git a/commands/TransferProfile.py b/commands/TransferProfile.py new file mode 100644 index 0000000..62837c4 --- /dev/null +++ b/commands/TransferProfile.py @@ -0,0 +1,70 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord +import os + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class TransferProfile(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "transferprofile" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Transfers the credit from absentee profile to a real one" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}{self.getCommand()} `\n" \ + "Transfers the credit from absentee profile to a real one. " \ + "Used for when an absentee's discord account is confirmed " \ + "and credit needs te be moved to the new name.\n" \ + "`Author ID` - The desired ID of the absentee profile\n" \ + "`New Author ID` - The real discord ID of the author\n" \ + + self.generateMultiLineExample( + server_config.prefix, + ["AUDINO_WHO <@!117780585635643396>", "AUDINO_WHO @Audino"] + ) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + if len(args) != 2: + await msg.channel.send(msg.author.mention + " Invalid args") + return + + from_name = self.spritebot.getFormattedCredit(args[0]) + to_name = self.spritebot.getFormattedCredit(args[1]) + if from_name.startswith("<@!") or from_name == "CHUNSOFT": + await msg.channel.send(msg.author.mention + " Only transfers from absent registrations are allowed.") + return + if from_name not in self.spritebot.names: + await msg.channel.send(msg.author.mention + " Entry {0} doesn't exist!".format(from_name)) + return + if to_name not in self.spritebot.names: + await msg.channel.send(msg.author.mention + " Entry {0} doesn't exist!".format(to_name)) + return + + new_credit = TrackerUtils.CreditEntry(self.spritebot.names[to_name].name, self.spritebot.names[to_name].contact) + new_credit.sprites = self.spritebot.names[from_name].sprites or self.spritebot.names[to_name].sprites + new_credit.portraits = self.spritebot.names[from_name].portraits or self.spritebot.names[to_name].portraits + del self.spritebot.names[from_name] + self.spritebot.names[to_name] = new_credit + + # update tracker based on last-modify + over_dict = TrackerUtils.initSubNode("", True) + over_dict.subgroups = self.spritebot.tracker + + TrackerUtils.renameFileCredits(os.path.join(self.spritebot.config.path, "sprite"), from_name, to_name) + TrackerUtils.renameFileCredits(os.path.join(self.spritebot.config.path, "portrait"), from_name, to_name) + TrackerUtils.renameJsonCredits(over_dict, from_name, to_name) + + await msg.channel.send(msg.author.mention + " account {0} deleted and credits moved to {1}.".format(from_name, to_name)) + + self.spritebot.saveTracker() + self.spritebot.saveNames() + self.spritebot.changed = True + + await self.spritebot.gitCommit("Moved account {0} to {1}".format(from_name, to_name)) \ No newline at end of file diff --git a/commands/Update.py b/commands/Update.py new file mode 100644 index 0000000..899b050 --- /dev/null +++ b/commands/Update.py @@ -0,0 +1,44 @@ +from typing import TYPE_CHECKING, List +from .BaseCommand import BaseCommand +from Constants import PermissionLevel +import TrackerUtils +import discord +import os +import git + +if TYPE_CHECKING: + from SpriteBot import SpriteBot, BotServer + +class Update(BaseCommand): + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.ADMIN + + def getCommand(self) -> str: + return "update" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Update the SpriteBot using Git" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}{self.getCommand()}`\n" \ + f"{self.getSingleLineHelp(server_config)}\n" \ + + self.generateMultiLineExample( + server_config.prefix, + [] + ) + + async def executeCommand(self, msg: discord.Message, args: List[str]): + guild = msg.guild + if guild is not None: + resp_ch = self.spritebot.getChatChannel(guild.id) + resp = await resp_ch.send("Pulling from repo...") + # update self + bot_repo = git.Repo(self.spritebot.path) + origin = bot_repo.remotes.origin + origin.pull() + await resp.edit(content="Update complete! Bot will restart.") + self.spritebot.need_restart = True + self.spritebot.config.update_ch = resp_ch.id + self.spritebot.config.update_msg = resp.id + self.spritebot.saveConfig() + await self.spritebot.client.close() \ No newline at end of file diff --git a/utils.py b/utils.py index 621e621..6e2ac5e 100644 --- a/utils.py +++ b/utils.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU General Public License # along with SkyTemple. If not, see . -from typing import List, Set, Dict, Tuple, Optional +from typing import List, Set, Dict, Tuple, Optional, TypeVar class MultipleOffsetError(Exception): def __init__(self, message): @@ -48,14 +48,14 @@ def addToBounds(bounds: Tuple[int, int, int, int], add: Tuple[int, int], sub: bo return (bounds[0] + add[0] * mult, bounds[1] + add[1] * mult, bounds[2] + add[0] * mult, bounds[3] + add[1] * mult) -def addLoc(loc1: Tuple[int, int], loc2: Tuple[int, int], sub: bool = False): +def addLoc(loc1, loc2, sub: bool = False): mult = 1 if sub: mult = -1 return (loc1[0] + loc2[0] * mult, loc1[1] + loc2[1] * mult) -def getCoveredBounds(inImg, max_box: Tuple[int, int, int, int] = None): +def getCoveredBounds(inImg, max_box: Optional[Tuple[int, int, int, int]] = None): if max_box is None: max_box = (0, 0, inImg.size[0], inImg.size[1]) minX, minY = inImg.size @@ -86,7 +86,7 @@ def addToPalette(palette, img): def getOffsetFromRGB(img, bounds: Tuple[int, int, int, int], black: bool, r: bool, g: bool, b: bool, white: bool): datas = img.getdata() - results = [None] * 5 + results: List[Optional[Tuple[int, int]]] = [None] * 5 for i in range(bounds[0], bounds[2]): for j in range(bounds[1], bounds[3]): color = datas[i + j * img.size[0]] @@ -111,11 +111,11 @@ def getOffsetFromRGB(img, bounds: Tuple[int, int, int, int], black: bool, r: boo if results[0] is not None: existing_px.append((results[0][0] + bounds[0], results[0][1] + bounds[1])) if results[1] is not None: - existing_px.append((results[0][1] + bounds[0], results[1][1] + bounds[1])) + existing_px.append((results[0][1] + bounds[0], results[1][1] + bounds[1])) # type: ignore if results[2] is not None: - existing_px.append((results[0][2] + bounds[0], results[2][1] + bounds[1])) + existing_px.append((results[0][2] + bounds[0], results[2][1] + bounds[1])) # type: ignore if results[3] is not None: - existing_px.append((results[0][3] + bounds[0], results[3][1] + bounds[1])) + existing_px.append((results[0][3] + bounds[0], results[3][1] + bounds[1])) # type: ignore raise MultipleOffsetError("White pixel found at {0} when r/g/b pixel already found at {1} when searching for offsets!".format((i, j), existing_px)) else: if black and color[0] == 0 and color[1] == 0 and color[2] == 0: @@ -211,3 +211,11 @@ def offsetsEqual(offset1, offset2, imgWidth: int, flip: bool = False): if offset1.rhand != rhand: return False return True + +# from https://stackoverflow.com/questions/75833721/unpacking-an-optional-value +T = TypeVar('T') + +def unpack_optional(opt: Optional[T]) -> T: + if opt is None: + raise ValueError("Optional value is None") + return opt \ No newline at end of file