From a63fa759c48c86b6df9d4bee651f2fcd062cac17 Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 25 Aug 2024 14:06:49 +0200 Subject: [PATCH 01/35] Reworked permission logic into an enum, splited clearcache command --- Constants.py | 42 ++++++- SpriteBot.py | 192 +++++++++++++----------------- SpriteUtils.py | 2 +- commands/AutoRecolorRessource.py | 3 + commands/BaseCommand.py | 4 + commands/ClearCache.py | 47 ++++++++ commands/DeleteRessourceCredit.py | 10 +- commands/GetProfile.py | 3 + commands/ListRessource.py | 3 + commands/QueryRessourceCredit.py | 3 + commands/QueryRessourceStatus.py | 4 +- 11 files changed, 195 insertions(+), 118 deletions(-) create mode 100644 commands/ClearCache.py diff --git a/Constants.py b/Constants.py index dbde40c..dad358b 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,16 +10,16 @@ CROP_PORTRAITS = True -COMPLETION_EMOTIONS = [] +COMPLETION_EMOTIONS: List[List[str]] = [] -EMOTIONS = [] +EMOTIONS: List[str] = [] -ACTION_MAP = { } +ACTION_MAP: Dict[int, str] = { } -COMPLETION_ACTIONS = [] +COMPLETION_ACTIONS: List[List[int]] = [] -ACTIONS = [] +ACTIONS: List[str] = [] DUNGEON_ACTIONS = [] STARTER_ACTIONS = [] @@ -33,4 +35,32 @@ 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" ] + +class PermissionLevel(Enum): + EVERYONE = 0 + STAFF = 1 + ADMIN = 2 + + def canPerformAction(self, required_level) -> bool: + return required_level.value <= self.value + + def name(self) -> str: + if self == self.EVERYONE: + return "everyone" + elif self == self.STAFF: + return "staff" + elif self == self.ADMIN: + return "admin" + else: + return "unknown" + + def helpprefix(self) -> str: + if self == self.EVERYONE: + return "" + elif self == self.STAFF: + return "staff" + elif self == self.ADMIN: + return "admin" + else: + return "" \ No newline at end of file diff --git a/SpriteBot.py b/SpriteBot.py index cfa8993..9a92cd9 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -25,9 +25,10 @@ from commands.ListRessource import ListRessource from commands.QueryRessourceCredit import QueryRessourceCredit from commands.DeleteRessourceCredit import DeleteRessourceCredit +from commands.ClearCache import ClearCache from commands.GetProfile import GetProfile -from Constants import PHASES +from Constants import PHASES, PermissionLevel import psutil # Housekeeping for login information @@ -204,6 +205,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 +220,13 @@ def __init__(self, in_path, client): QueryRessourceCredit(self, "sprite", True), DeleteRessourceCredit(self, "portrait"), DeleteRessourceCredit(self, "sprite"), - GetProfile(self) + GetProfile(self), + + # staff + ClearCache(self) + + # admin + # (empty for now) ] self.writeLog("Startup Memory: {0}".format(psutil.Process().memory_info().rss)) @@ -411,16 +419,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 +438,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 @@ -1179,14 +1187,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 +1202,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) @@ -1807,7 +1815,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: @@ -2016,20 +2024,6 @@ async def listBounties(self, msg, name_args): 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" @@ -2170,7 +2164,7 @@ async def setProfile(self, msg, args): 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): + if not (await self.getUserPermission(msg.author, msg.guild)).canPerformAction(PermissionLevel.STAFF): await msg.channel.send(msg.author.mention + " Not authorized to create absent registration.") return msg_mention = self.getFormattedCredit(args[0]) @@ -2234,7 +2228,7 @@ async def deleteProfile(self, msg, args): "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): + 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]) @@ -2720,27 +2714,61 @@ async def removeGender(self, msg, args): self.saveTracker() self.changed = True - async def help(self, msg, args): + async def help(self, msg, args, permission_level: PermissionLevel): server_config = self.config.servers[str(msg.guild.id)] prefix = server_config.prefix use_bounties = self.config.use_bounties if len(args) == 0: return_msg = "**Commands**\n" + + if permission_level == PermissionLevel.EVERYONE: + return_msg += f"`{prefix}register` - Register your profile\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" + elif permission_level == PermissionLevel.STAFF: + 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}adminregister` - Use with arguments to make absentee profiles\n" \ + f"`{prefix}transferprofile` - Transfers the credit from absentee profile to a real one\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.DEFAULT_PERMISSION: + return_msg += f"`{prefix}{command.getCommand()}` - {command.getSingleLineHelp(server_config)}\n" + + if permission_level == PermissionLevel.EVERYONE: + return_msg += f"`{prefix}staffhelp` - Show staff commands\n" \ + f"`{prefix}adminhelp` - Show admin commands\n" + + return_msg += f"Type `{prefix}{permission_level.helpprefix()}help` with the name of a command to learn more about it." else: base_arg = args[0] return_msg = None for command in self.commands: - if command.getCommand() == base_arg: + if command.getCommand() == base_arg and permission_level == command.DEFAULT_PERMISSION: return_msg = "**Command Help**\n" \ + command.getMultiLineHelp(server_config) if return_msg != None: @@ -2806,44 +2834,7 @@ async def help(self, msg, args): "`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": + elif 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" \ @@ -3155,9 +3146,9 @@ async def staffhelp(self, msg, args): "**Examples**\n" \ f"`{prefix}modreward Unown`\n" \ f"`{prefix}modreward Minior Red`" - elif base_arg == "register": + elif base_arg == "adminregister": return_msg = "**Command Help**\n" \ - f"`{prefix}register `\n" \ + f"`{prefix}adminregister `\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)." \ @@ -3167,9 +3158,9 @@ async def staffhelp(self, msg, args): "`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`" + f"`{prefix}adminregister SUGIMORI Sugimori https://twitter.com/SUPER_32X`\n" \ + f"`{prefix}adminregister @Audino Audino https://github.com/audinowho`\n" \ + f"`{prefix}adminregister <@!117780585635643396> Audino https://github.com/audinowho`" elif base_arg == "transferprofile": return_msg = "**Command Help**\n" \ f"`{prefix}transferprofile `\n" \ @@ -3183,28 +3174,10 @@ async def staffhelp(self, msg, args): "**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') @@ -3246,18 +3219,25 @@ 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.DEFAULT_PERMISSION): + 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.DEFAULT_PERMISSION.name())) return if base_arg == "help": - await sprite_bot.help(msg, args[1:]) + await sprite_bot.help(msg, args[1:], PermissionLevel.EVERYONE) elif base_arg == "staffhelp": - await sprite_bot.staffhelp(msg, args[1:]) + await sprite_bot.help(msg, args[1:], PermissionLevel.STAFF) + elif base_arg == "adminhelp": + await sprite_bot.help(msg, args[1:], PermissionLevel.ADMIN) # primary commands elif base_arg == "spritebounty": await sprite_bot.placeBounty(msg, args[1:], "sprite") @@ -3265,7 +3245,7 @@ async def on_message(msg: discord.Message): await sprite_bot.placeBounty(msg, args[1:], "portrait") elif base_arg == "bounties": await sprite_bot.listBounties(msg, args[1:]) - elif base_arg == "register": + elif base_arg == "register" or base_arg == "adminregister": await sprite_bot.setProfile(msg, args[1:]) elif base_arg == "absentprofiles": await sprite_bot.getAbsentProfiles(msg) @@ -3320,8 +3300,6 @@ async def on_message(msg: discord.Message): 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:]) diff --git a/SpriteUtils.py b/SpriteUtils.py index 9fd04d4..d6b0351 100644 --- a/SpriteUtils.py +++ b/SpriteUtils.py @@ -1084,7 +1084,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 diff --git a/commands/AutoRecolorRessource.py b/commands/AutoRecolorRessource.py index 50b1a75..55e8ead 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 @@ -9,6 +10,8 @@ from SpriteBot import SpriteBot, BotServer class AutoRecolorRessource(BaseCommand): + DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE + def __init__(self, spritebot: "SpriteBot", ressource_type: str): super().__init__(spritebot) self.ressource_type = ressource_type diff --git a/commands/BaseCommand.py b/commands/BaseCommand.py index abf6a63..ba0e160 100644 --- a/commands/BaseCommand.py +++ b/commands/BaseCommand.py @@ -1,11 +1,15 @@ from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, List +from Constants import PermissionLevel import discord if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer class BaseCommand: + # The default permission level required to execute this command + DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.ADMIN + def __init__(self, spritebot: "SpriteBot") -> None: self.spritebot = spritebot diff --git a/commands/ClearCache.py b/commands/ClearCache.py new file mode 100644 index 0000000..711e60e --- /dev/null +++ b/commands/ClearCache.py @@ -0,0 +1,47 @@ +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): + DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.STAFF + + def __init__(self, spritebot: "SpriteBot") -> None: + self.spritebot = spritebot + + 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/DeleteRessourceCredit.py b/commands/DeleteRessourceCredit.py index a013c19..8b51f8e 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 @@ -8,6 +9,8 @@ from SpriteBot import SpriteBot, BotServer class DeleteRessourceCredit(BaseCommand): + DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE + def __init__(self, spritebot: "SpriteBot", ressource_type: str): super().__init__(spritebot) self.ressource_type = ressource_type @@ -62,7 +65,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 +105,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 == 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/GetProfile.py b/commands/GetProfile.py index f8344d6..a63e5ef 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,8 @@ from SpriteBot import SpriteBot, BotServer class GetProfile(BaseCommand): + DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE + def getCommand(self) -> str: return "profile" diff --git a/commands/ListRessource.py b/commands/ListRessource.py index 0909a67..f4148d0 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 @@ -7,6 +8,8 @@ from SpriteBot import SpriteBot, BotServer class ListRessource(BaseCommand): + DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE + def __init__(self, spritebot: "SpriteBot", ressource_type: str): super().__init__(spritebot) self.ressource_type = ressource_type diff --git a/commands/QueryRessourceCredit.py b/commands/QueryRessourceCredit.py index d5274cb..ffc857e 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 @@ -8,6 +9,8 @@ from SpriteBot import SpriteBot, BotServer class QueryRessourceCredit(BaseCommand): + DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE + def __init__(self, spritebot: "SpriteBot", ressource_type: str, display_history: bool): super().__init__(spritebot) self.ressource_type = ressource_type diff --git a/commands/QueryRessourceStatus.py b/commands/QueryRessourceStatus.py index e071acb..b3ccb03 100644 --- a/commands/QueryRessourceStatus.py +++ b/commands/QueryRessourceStatus.py @@ -2,12 +2,14 @@ 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 class QueryRessourceStatus(BaseCommand): + DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE + def __init__(self, spritebot: "SpriteBot", ressource_type: str, is_derivation: bool): super().__init__(spritebot) self.ressource_type = ressource_type From 5922ccf8d5563bdf6b288a7ae3a48e6f109815df Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 25 Aug 2024 14:13:41 +0200 Subject: [PATCH 02/35] moved where the permission level is accessed for command --- SpriteBot.py | 8 ++++---- commands/AutoRecolorRessource.py | 5 +++-- commands/BaseCommand.py | 8 +++++--- commands/ClearCache.py | 5 +++-- commands/DeleteRessourceCredit.py | 5 +++-- commands/GetProfile.py | 3 ++- commands/ListRessource.py | 5 +++-- commands/QueryRessourceCredit.py | 5 +++-- commands/QueryRessourceStatus.py | 5 +++-- 9 files changed, 29 insertions(+), 20 deletions(-) diff --git a/SpriteBot.py b/SpriteBot.py index 9a92cd9..37f6053 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -2755,7 +2755,7 @@ async def help(self, msg, args, permission_level: PermissionLevel): f"`{prefix}transferprofile` - Transfers the credit from absentee profile to a real one\n" for command in self.commands: - if permission_level == command.DEFAULT_PERMISSION: + if permission_level == command.getRequiredPermission(): return_msg += f"`{prefix}{command.getCommand()}` - {command.getSingleLineHelp(server_config)}\n" if permission_level == PermissionLevel.EVERYONE: @@ -2768,7 +2768,7 @@ async def help(self, msg, args, permission_level: PermissionLevel): base_arg = args[0] return_msg = None for command in self.commands: - if command.getCommand() == base_arg and permission_level == command.DEFAULT_PERMISSION: + if command.getCommand() == base_arg and permission_level == command.getRequiredPermission(): return_msg = "**Command Help**\n" \ + command.getMultiLineHelp(server_config) if return_msg != None: @@ -3226,10 +3226,10 @@ async def on_message(msg: discord.Message): for command in sprite_bot.commands: if base_arg == command.getCommand(): #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.DEFAULT_PERMISSION): + 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.DEFAULT_PERMISSION.name())) + await msg.channel.send("{} Not authorized (you need the permission level of at least “{}” to run this command)".format(msg.author.mention, command.getRequiredPermission().name())) return if base_arg == "help": diff --git a/commands/AutoRecolorRessource.py b/commands/AutoRecolorRessource.py index 55e8ead..7508f4a 100644 --- a/commands/AutoRecolorRessource.py +++ b/commands/AutoRecolorRessource.py @@ -10,12 +10,13 @@ from SpriteBot import SpriteBot, BotServer class AutoRecolorRessource(BaseCommand): - DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE - 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 ba0e160..0131104 100644 --- a/commands/BaseCommand.py +++ b/commands/BaseCommand.py @@ -7,12 +7,14 @@ from SpriteBot import SpriteBot, BotServer class BaseCommand: - # The default permission level required to execute this command - DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.ADMIN - 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" """ diff --git a/commands/ClearCache.py b/commands/ClearCache.py index 711e60e..6c6f9b3 100644 --- a/commands/ClearCache.py +++ b/commands/ClearCache.py @@ -8,10 +8,11 @@ from SpriteBot import SpriteBot, BotServer class ClearCache(BaseCommand): - DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.STAFF - def __init__(self, spritebot: "SpriteBot") -> None: self.spritebot = spritebot + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF def getCommand(self) -> str: return "clearcache" diff --git a/commands/DeleteRessourceCredit.py b/commands/DeleteRessourceCredit.py index 8b51f8e..81f8549 100644 --- a/commands/DeleteRessourceCredit.py +++ b/commands/DeleteRessourceCredit.py @@ -9,12 +9,13 @@ from SpriteBot import SpriteBot, BotServer class DeleteRessourceCredit(BaseCommand): - DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE - 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" diff --git a/commands/GetProfile.py b/commands/GetProfile.py index a63e5ef..9fa1bfe 100644 --- a/commands/GetProfile.py +++ b/commands/GetProfile.py @@ -8,7 +8,8 @@ from SpriteBot import SpriteBot, BotServer class GetProfile(BaseCommand): - DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE def getCommand(self) -> str: return "profile" diff --git a/commands/ListRessource.py b/commands/ListRessource.py index f4148d0..fff5ec6 100644 --- a/commands/ListRessource.py +++ b/commands/ListRessource.py @@ -8,12 +8,13 @@ from SpriteBot import SpriteBot, BotServer class ListRessource(BaseCommand): - DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE - 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/QueryRessourceCredit.py b/commands/QueryRessourceCredit.py index ffc857e..1b58fc8 100644 --- a/commands/QueryRessourceCredit.py +++ b/commands/QueryRessourceCredit.py @@ -9,13 +9,14 @@ from SpriteBot import SpriteBot, BotServer class QueryRessourceCredit(BaseCommand): - DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE - def __init__(self, spritebot: "SpriteBot", ressource_type: str, display_history: bool): super().__init__(spritebot) 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" diff --git a/commands/QueryRessourceStatus.py b/commands/QueryRessourceStatus.py index b3ccb03..ea0ee62 100644 --- a/commands/QueryRessourceStatus.py +++ b/commands/QueryRessourceStatus.py @@ -8,13 +8,14 @@ from SpriteBot import SpriteBot, BotServer class QueryRessourceStatus(BaseCommand): - DEFAULT_PERMISSION: PermissionLevel = PermissionLevel.EVERYONE - def __init__(self, spritebot: "SpriteBot", ressource_type: str, is_derivation: bool): super().__init__(spritebot) 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}" From 87b1039fdd680e1b873b845dc399d230ebdc80fb Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 25 Aug 2024 14:48:48 +0200 Subject: [PATCH 03/35] more static typing --- Constants.py | 6 +++--- SpriteBot.py | 17 ++++++++--------- SpriteUtils.py | 2 +- TrackerUtils.py | 4 +++- commands/DeleteRessourceCredit.py | 2 +- utils.py | 4 ++-- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Constants.py b/Constants.py index dad358b..81f1bbb 100644 --- a/Constants.py +++ b/Constants.py @@ -20,8 +20,8 @@ COMPLETION_ACTIONS: List[List[int]] = [] ACTIONS: List[str] = [] -DUNGEON_ACTIONS = [] -STARTER_ACTIONS = [] +DUNGEON_ACTIONS: List[str] = [] +STARTER_ACTIONS: List[str] = [] DIRECTIONS = [ "Down", "DownRight", @@ -45,7 +45,7 @@ class PermissionLevel(Enum): def canPerformAction(self, required_level) -> bool: return required_level.value <= self.value - def name(self) -> str: + def displayname(self) -> str: if self == self.EVERYONE: return "everyone" elif self == self.STAFF: diff --git a/SpriteBot.py b/SpriteBot.py index 37f6053..b6952d6 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Dict, Any import os @@ -100,18 +100,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 = { } @@ -3229,7 +3228,7 @@ async def on_message(msg: discord.Message): 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().name())) + 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": diff --git a/SpriteUtils.py b/SpriteUtils.py index d6b0351..254f741 100644 --- a/SpriteUtils.py +++ b/SpriteUtils.py @@ -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() diff --git a/TrackerUtils.py b/TrackerUtils.py index 1617944..7ddb3bb 100644 --- a/TrackerUtils.py +++ b/TrackerUtils.py @@ -1,4 +1,6 @@ +from typing import Dict, List +import sys import os import re import shutil @@ -690,7 +692,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 diff --git a/commands/DeleteRessourceCredit.py b/commands/DeleteRessourceCredit.py index 81f8549..fbea3b1 100644 --- a/commands/DeleteRessourceCredit.py +++ b/commands/DeleteRessourceCredit.py @@ -107,7 +107,7 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): return guild = msg.guild - if guild == None: + if guild is None: raise BaseException("The message has not been posted to a guild!") chat_id = self.spritebot.config.servers[str(guild.id)].submit diff --git a/utils.py b/utils.py index 621e621..f5d5353 100644 --- a/utils.py +++ b/utils.py @@ -55,7 +55,7 @@ def addLoc(loc1: Tuple[int, int], loc2: Tuple[int, int], sub: bool = False): 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]] From 897cdbdfb7538e9a09062a9fa4535e0b58e41ee6 Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 25 Aug 2024 15:35:05 +0200 Subject: [PATCH 04/35] SetProfile: moved register and forceregister --- SpriteBot.py | 33 ++--------------- commands/SetProfile.py | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 commands/SetProfile.py diff --git a/SpriteBot.py b/SpriteBot.py index b6952d6..f8f3349 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -27,6 +27,7 @@ from commands.DeleteRessourceCredit import DeleteRessourceCredit from commands.ClearCache import ClearCache from commands.GetProfile import GetProfile +from commands.SetProfile import SetProfile from Constants import PHASES, PermissionLevel import psutil @@ -220,9 +221,11 @@ def __init__(self, in_path, client): DeleteRessourceCredit(self, "portrait"), DeleteRessourceCredit(self, "sprite"), GetProfile(self), + SetProfile(self, False), # staff - ClearCache(self) + ClearCache(self), + SetProfile(self, True), # admin # (empty for now) @@ -2721,7 +2724,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): return_msg = "**Commands**\n" if permission_level == PermissionLevel.EVERYONE: - return_msg += f"`{prefix}register` - Register your profile\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" \ @@ -2750,7 +2752,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): 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}adminregister` - Use with arguments to make absentee profiles\n" \ f"`{prefix}transferprofile` - Transfers the credit from absentee profile to a real one\n" for command in self.commands: @@ -2824,15 +2825,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): 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`" elif base_arg == "add": return_msg = "**Command Help**\n" \ f"`{prefix}add [Form Name]`\n" \ @@ -3145,21 +3137,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): "**Examples**\n" \ f"`{prefix}modreward Unown`\n" \ f"`{prefix}modreward Minior Red`" - elif base_arg == "adminregister": - return_msg = "**Command Help**\n" \ - f"`{prefix}adminregister `\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}adminregister SUGIMORI Sugimori https://twitter.com/SUPER_32X`\n" \ - f"`{prefix}adminregister @Audino Audino https://github.com/audinowho`\n" \ - f"`{prefix}adminregister <@!117780585635643396> Audino https://github.com/audinowho`" elif base_arg == "transferprofile": return_msg = "**Command Help**\n" \ f"`{prefix}transferprofile `\n" \ @@ -3244,8 +3221,6 @@ async def on_message(msg: discord.Message): await sprite_bot.placeBounty(msg, args[1:], "portrait") elif base_arg == "bounties": await sprite_bot.listBounties(msg, args[1:]) - elif base_arg == "register" or base_arg == "adminregister": - await sprite_bot.setProfile(msg, args[1:]) elif base_arg == "absentprofiles": await sprite_bot.getAbsentProfiles(msg) elif base_arg == "unregister": 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 From 43bbb581d6d4e4be39cce6ec7fbc611a8bd9ad93 Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 25 Aug 2024 15:57:20 +0200 Subject: [PATCH 05/35] rename: move to a seperate command file --- SpriteBot.py | 86 +----------------------------------------- commands/RenameNode.py | 72 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 84 deletions(-) create mode 100644 commands/RenameNode.py diff --git a/SpriteBot.py b/SpriteBot.py index f8f3349..bf60cd3 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -28,6 +28,7 @@ from commands.ClearCache import ClearCache from commands.GetProfile import GetProfile from commands.SetProfile import SetProfile +from commands.RenameNode import RenameNode from Constants import PHASES, PermissionLevel import psutil @@ -226,6 +227,7 @@ def __init__(self, in_path, client): # staff ClearCache(self), SetProfile(self, True), + RenameNode(self), # admin # (empty for now) @@ -2156,33 +2158,6 @@ async def getAbsentProfiles(self, msg): 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): - msg_mention = "<@!{0}>".format(msg.author.id) - - 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]) - elif len(args) == 3: - if not (await self.getUserPermission(msg.author, msg.guild)).canPerformAction(PermissionLevel.STAFF): - await msg.channel.send(msg.author.mention + " Not authorized to create absent registration.") - return - msg_mention = self.getFormattedCredit(args[0]) - new_credit = TrackerUtils.CreditEntry(args[1], args[2]) - 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") @@ -2444,50 +2419,6 @@ async def addSpeciesForm(self, msg, args): 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!") @@ -2733,7 +2664,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): 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" \ @@ -2845,16 +2775,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): "**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" \ @@ -3230,8 +3150,6 @@ async def on_message(msg: discord.Message): 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: 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 From 2271005f226630560a06b161d3c0e9be85b9ef12 Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 25 Aug 2024 17:27:42 +0200 Subject: [PATCH 06/35] replaceressource: moved out of SpriteBot --- SpriteBot.py | 94 ++-------------------------------- commands/ReplaceRessource.py | 97 ++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 91 deletions(-) create mode 100644 commands/ReplaceRessource.py diff --git a/SpriteBot.py b/SpriteBot.py index bf60cd3..192d08b 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -29,6 +29,7 @@ from commands.GetProfile import GetProfile from commands.SetProfile import SetProfile from commands.RenameNode import RenameNode +from commands.ReplaceRessource import ReplaceRessource from Constants import PHASES, PermissionLevel import psutil @@ -228,6 +229,8 @@ def __init__(self, in_path, client): ClearCache(self), SetProfile(self, True), RenameNode(self), + ReplaceRessource(self, "portrait"), + ReplaceRessource(self, "sprite"), # admin # (empty for now) @@ -1659,71 +1662,6 @@ async def moveSlotRecursive(self, msg, name_args): 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("->") @@ -2871,28 +2809,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): 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" \ @@ -3164,10 +3080,6 @@ async def on_message(msg: discord.Message): 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: 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 From eb542c612648fcd28519d8717a1f9c7ee6ce4a61 Mon Sep 17 00:00:00 2001 From: marius david Date: Wed, 28 Aug 2024 12:40:18 +0200 Subject: [PATCH 07/35] Moved MoveNode command out of SpriteBot --- SpriteBot.py | 116 +------------------------------------ commands/MoveNode.py | 132 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 114 deletions(-) create mode 100644 commands/MoveNode.py diff --git a/SpriteBot.py b/SpriteBot.py index 192d08b..9b17bf5 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -30,6 +30,7 @@ from commands.SetProfile import SetProfile from commands.RenameNode import RenameNode from commands.ReplaceRessource import ReplaceRessource +from commands.MoveNode import MoveNode from Constants import PHASES, PermissionLevel import psutil @@ -231,6 +232,7 @@ def __init__(self, in_path, client): RenameNode(self), ReplaceRessource(self, "portrait"), ReplaceRessource(self, "sprite"), + MoveNode(self), # admin # (empty for now) @@ -1565,103 +1567,6 @@ async def checkMoveLock(self, full_idx_from, chosen_node_from, full_idx_to, chos 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 moveSlot(self, msg, name_args, asset_type): try: delim_idx = name_args.index("->") @@ -2608,7 +2513,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): 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" \ @@ -2795,20 +2699,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): 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 == "spritewip": return_msg = "**Command Help**\n" \ f"`{prefix}spritewip [Form Name] [Shiny] [Gender]`\n" \ @@ -3078,8 +2968,6 @@ async def on_message(msg: discord.Message): 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 == "spritewip" and authorized: await sprite_bot.completeSlot(msg, args[1:], "sprite", TrackerUtils.PHASE_INCOMPLETE) elif base_arg == "portraitwip" and authorized: diff --git a/commands/MoveNode.py b/commands/MoveNode.py new file mode 100644 index 0000000..c310a9b --- /dev/null +++ b/commands/MoveNode.py @@ -0,0 +1,132 @@ +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) + + # 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 From 11ed0afdce93ef07449e30c41b553d67a417bb2b Mon Sep 17 00:00:00 2001 From: marius david Date: Wed, 28 Aug 2024 12:49:48 +0200 Subject: [PATCH 08/35] help command: put all into a single command (instead of separate staffhelp and help) --- Constants.py | 12 +----------- SpriteBot.py | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Constants.py b/Constants.py index 81f1bbb..428b03b 100644 --- a/Constants.py +++ b/Constants.py @@ -53,14 +53,4 @@ def displayname(self) -> str: elif self == self.ADMIN: return "admin" else: - return "unknown" - - def helpprefix(self) -> str: - if self == self.EVERYONE: - return "" - elif self == self.STAFF: - return "staff" - elif self == self.ADMIN: - return "admin" - else: - return "" \ No newline at end of file + return "unknown" \ No newline at end of file diff --git a/SpriteBot.py b/SpriteBot.py index 9b17bf5..a039155 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -2491,10 +2491,24 @@ async def removeGender(self, msg, args): self.changed = True async def help(self, msg, args, permission_level: 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 + 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: @@ -2531,16 +2545,16 @@ async def help(self, msg, args, permission_level: PermissionLevel): return_msg += f"`{prefix}{command.getCommand()}` - {command.getSingleLineHelp(server_config)}\n" if permission_level == PermissionLevel.EVERYONE: - return_msg += f"`{prefix}staffhelp` - Show staff commands\n" \ - f"`{prefix}adminhelp` - Show admin commands\n" + return_msg += f"`{prefix}help staff` - List staff commands\n" \ + f"`{prefix}help admin` - List admin commands\n" - return_msg += f"Type `{prefix}{permission_level.helpprefix()}help` with the name of a command to learn more about it." + return_msg += f"Type `{prefix}help` with the name of a command to learn more about it." else: base_arg = args[0] return_msg = None for command in self.commands: - if command.getCommand() == base_arg and permission_level == command.getRequiredPermission(): + if command.getCommand() == base_arg: return_msg = "**Command Help**\n" \ + command.getMultiLineHelp(server_config) if return_msg != None: @@ -2935,11 +2949,10 @@ async def on_message(msg: discord.Message): return if base_arg == "help": - await sprite_bot.help(msg, args[1:], PermissionLevel.EVERYONE) + await sprite_bot.help(msg, args[1:], None) + # legacy link to help staff elif base_arg == "staffhelp": await sprite_bot.help(msg, args[1:], PermissionLevel.STAFF) - elif base_arg == "adminhelp": - await sprite_bot.help(msg, args[1:], PermissionLevel.ADMIN) # primary commands elif base_arg == "spritebounty": await sprite_bot.placeBounty(msg, args[1:], "sprite") From 25b74cf6336e88260030af1e86a08e97307a0b42 Mon Sep 17 00:00:00 2001 From: marius david Date: Wed, 28 Aug 2024 13:19:19 +0200 Subject: [PATCH 09/35] SetRessourceCredit moved out of SpriteBot.py --- SpriteBot.py | 85 ++-------------------------------- commands/SetRessourceCredit.py | 80 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 80 deletions(-) create mode 100644 commands/SetRessourceCredit.py diff --git a/SpriteBot.py b/SpriteBot.py index a039155..33e7930 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional import os @@ -31,6 +31,7 @@ from commands.RenameNode import RenameNode from commands.ReplaceRessource import ReplaceRessource from commands.MoveNode import MoveNode +from commands.SetRessourceCredit import SetRessourceCredit from Constants import PHASES, PermissionLevel import psutil @@ -233,6 +234,8 @@ def __init__(self, in_path, client): ReplaceRessource(self, "portrait"), ReplaceRessource(self, "sprite"), MoveNode(self), + SetRessourceCredit(self, "portrait"), + SetRessourceCredit(self, "sprite"), # admin # (empty for now) @@ -1915,46 +1918,6 @@ 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: @@ -2490,7 +2453,7 @@ async def removeGender(self, msg, args): self.saveTracker() self.changed = True - async def help(self, msg, args, permission_level: PermissionLevel): + async def help(self, msg, args, permission_level: Optional[PermissionLevel]): list_commands = len(args) == 0 if permission_level == None: if len(args) > 0: @@ -2533,8 +2496,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): 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" \ @@ -2803,38 +2764,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): 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" \ @@ -2993,10 +2922,6 @@ async def on_message(msg: discord.Message): 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: 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 From c71f76f89925d7c6767d0da4599790268f326fe5 Mon Sep 17 00:00:00 2001 From: marius david Date: Wed, 28 Aug 2024 17:41:11 +0200 Subject: [PATCH 10/35] MoveRessource: move out of SpriteBot.py --- SpriteBot.py | 107 ++------------------------------------ commands/MoveRessource.py | 106 +++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 103 deletions(-) create mode 100644 commands/MoveRessource.py diff --git a/SpriteBot.py b/SpriteBot.py index 33e7930..79d546c 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -31,6 +31,7 @@ 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 Constants import PHASES, PermissionLevel @@ -234,6 +235,8 @@ def __init__(self, in_path, client): ReplaceRessource(self, "portrait"), ReplaceRessource(self, "sprite"), MoveNode(self), + MoveRessource(self, "portrait"), + MoveRessource(self, "sprite"), SetRessourceCredit(self, "portrait"), SetRessourceCredit(self, "sprite"), @@ -1569,73 +1572,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 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: @@ -2488,8 +2425,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): 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}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" \ @@ -2644,36 +2579,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): 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 == "spritewip": return_msg = "**Command Help**\n" \ f"`{prefix}spritewip [Form Name] [Shiny] [Gender]`\n" \ @@ -2906,10 +2811,6 @@ async def on_message(msg: discord.Message): 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 == "spritewip" and authorized: await sprite_bot.completeSlot(msg, args[1:], "sprite", TrackerUtils.PHASE_INCOMPLETE) elif base_arg == "portraitwip" and authorized: 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 From 3ecbbc104b0546e8304e485e1d61fdbcb8c91aac Mon Sep 17 00:00:00 2001 From: marius david Date: Wed, 28 Aug 2024 18:09:29 +0200 Subject: [PATCH 11/35] Solve some mypy warning --- Constants.py | 2 +- TrackerUtils.py | 32 +++++++++++++++----------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Constants.py b/Constants.py index 428b03b..900561c 100644 --- a/Constants.py +++ b/Constants.py @@ -10,7 +10,7 @@ CROP_PORTRAITS = True -COMPLETION_EMOTIONS: List[List[str]] = [] +COMPLETION_EMOTIONS: List[List[int]] = [] EMOTIONS: List[str] = [] diff --git a/TrackerUtils.py b/TrackerUtils.py index 7ddb3bb..3c79ba1 100644 --- a/TrackerUtils.py +++ b/TrackerUtils.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Any import sys import os @@ -93,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) @@ -160,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 = {} @@ -173,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__: @@ -223,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 @@ -309,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): From ec5bec15dc470d0a45e30063e4ed3894c263857e Mon Sep 17 00:00:00 2001 From: marius david Date: Wed, 28 Aug 2024 19:12:40 +0200 Subject: [PATCH 12/35] ignore or solve all remaining mypy error --- SpriteBot.py | 22 ++++++++-------- SpriteUtils.py | 44 +++++++++++++++++--------------- commands/QueryRessourceCredit.py | 2 +- utils.py | 8 +++--- 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/SpriteBot.py b/SpriteBot.py index 79d546c..edddfc6 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -249,7 +249,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) @@ -682,7 +682,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." @@ -1053,7 +1053,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) @@ -1784,7 +1784,7 @@ async def listBounties(self, msg, name_args): await msg.channel.send(msg.author.mention + " Use 'sprite' or 'portrait' as argument.") return - entries = [] + entries = [] # type: ignore over_dict = TrackerUtils.initSubNode("", True) over_dict.subgroups = self.tracker @@ -1895,7 +1895,7 @@ async def addCredit(self, msg, name_args, asset_type): async def getAbsentProfiles(self, msg): total_names = ["Absentee profiles:"] - msg_ids = [] + msg_ids = [] # type: ignore 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)) @@ -2089,8 +2089,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 @@ -2731,8 +2731,8 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): @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() @@ -2874,11 +2874,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 254f741..e67560b 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') @@ -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]) @@ -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,7 +1801,7 @@ 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() k = 0 diff --git a/commands/QueryRessourceCredit.py b/commands/QueryRessourceCredit.py index 1b58fc8..5ed8036 100644 --- a/commands/QueryRessourceCredit.py +++ b/commands/QueryRessourceCredit.py @@ -117,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/utils.py b/utils.py index f5d5353..9461153 100644 --- a/utils.py +++ b/utils.py @@ -48,7 +48,7 @@ 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 @@ -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: From b7d39658eb883d010c53f40c7986a9da3fac61d3 Mon Sep 17 00:00:00 2001 From: marius david Date: Sat, 31 Aug 2024 10:05:47 +0200 Subject: [PATCH 13/35] SetRessourceLock: moved out of SpriteBot.py --- SpriteBot.py | 46 ++----------------- commands/SetRessourceLock.py | 89 ++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 41 deletions(-) create mode 100644 commands/SetRessourceLock.py diff --git a/SpriteBot.py b/SpriteBot.py index edddfc6..fd3090b 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -33,6 +33,7 @@ from commands.MoveNode import MoveNode from commands.MoveRessource import MoveRessource from commands.SetRessourceCredit import SetRessourceCredit +from commands.SetRessourceLock import SetRessourceLock from Constants import PHASES, PermissionLevel import psutil @@ -241,7 +242,10 @@ def __init__(self, in_path, client): SetRessourceCredit(self, "sprite"), # admin - # (empty for now) + SetRessourceLock(self, "portrait", True), + SetRessourceLock(self, "portrait", False), + SetRessourceLock(self, "sprite", True), + SetRessourceLock(self, "sprite", False), ] self.writeLog("Startup Memory: {0}".format(psutil.Process().memory_info().rss)) @@ -1735,38 +1739,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) @@ -2836,14 +2808,6 @@ async def on_message(msg: discord.Message): 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: 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 From ccd040122df071c814e24b8d9e00797527d3b3be Mon Sep 17 00:00:00 2001 From: marius david Date: Sat, 31 Aug 2024 14:16:13 +0200 Subject: [PATCH 14/35] AddNode: move out of SpriteBot.py --- SpriteBot.py | 71 ++----------------------------------- commands/AddNode.py | 86 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 69 deletions(-) create mode 100644 commands/AddNode.py diff --git a/SpriteBot.py b/SpriteBot.py index fd3090b..290370d 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -33,6 +33,7 @@ from commands.MoveNode import MoveNode from commands.MoveRessource import MoveRessource from commands.SetRessourceCredit import SetRessourceCredit +from commands.AddNode import AddNode from commands.SetRessourceLock import SetRessourceLock from Constants import PHASES, PermissionLevel @@ -230,6 +231,7 @@ def __init__(self, in_path, client): SetProfile(self, False), # staff + AddNode(self), ClearCache(self), SetProfile(self, True), RenameNode(self), @@ -2078,62 +2080,6 @@ async def rescan(self, msg): #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 modSpeciesForm(self, msg, args): if len(args) < 1 or len(args) > 2: await msg.channel.send(msg.author.mention + " Invalid number of args!") @@ -2391,7 +2337,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): elif permission_level == PermissionLevel.STAFF: 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}addgender` - Adds the female sprite/portrait to the Pokemon\n" \ f"`{prefix}deletegender` - Removes the female sprite/portrait from the Pokemon\n" \ @@ -2479,16 +2424,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): f"`{prefix}bounties sprite`" else: return_msg = MESSAGE_BOUNTIES_DISABLED - elif 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" \ @@ -2771,8 +2706,6 @@ async def on_message(msg: discord.Message): 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 == "addgender" and authorized: 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 From 48b33f90eb29d0226e8ff79745969aeab96b9cf5 Mon Sep 17 00:00:00 2001 From: marius david Date: Tue, 3 Sep 2024 18:22:30 +0200 Subject: [PATCH 15/35] delete: move out of SpriteBot --- SpriteBot.py | 63 ++-------------------------------- commands/DeleteNode.py | 77 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 61 deletions(-) create mode 100644 commands/DeleteNode.py diff --git a/SpriteBot.py b/SpriteBot.py index 290370d..bb81cf8 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -34,6 +34,7 @@ from commands.MoveRessource import MoveRessource from commands.SetRessourceCredit import SetRessourceCredit from commands.AddNode import AddNode +from commands.DeleteNode import DeleteNode from commands.SetRessourceLock import SetRessourceLock from Constants import PHASES, PermissionLevel @@ -232,6 +233,7 @@ def __init__(self, in_path, client): # staff AddNode(self), + DeleteNode(self), ClearCache(self), SetProfile(self, True), RenameNode(self), @@ -2119,54 +2121,6 @@ async def modSpeciesForm(self, msg, args): self.saveTracker() self.changed = True - - 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!") @@ -2337,7 +2291,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): elif permission_level == PermissionLevel.STAFF: return_msg = "**Approver Commands**\n" \ - f"`{prefix}delete` - Deletes an empty 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" \ @@ -2424,16 +2377,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): f"`{prefix}bounties sprite`" else: return_msg = MESSAGE_BOUNTIES_DISABLED - 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 == "addgender": return_msg = "**Command Help**\n" \ f"`{prefix}addgender [Pokemon Form] `\n" \ @@ -2706,8 +2649,6 @@ async def on_message(msg: discord.Message): elif base_arg == "unregister": await sprite_bot.deleteProfile(msg, args[1:]) # authorized commands - elif base_arg == "delete" and authorized: - await sprite_bot.removeSpeciesForm(msg, args[1:]) elif base_arg == "addgender" and authorized: await sprite_bot.addGender(msg, args[1:]) elif base_arg == "deletegender" and authorized: 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 From 0d8adeb3a9ab452f367f2883a371d612b3a31a9f Mon Sep 17 00:00:00 2001 From: marius david Date: Tue, 3 Sep 2024 18:46:41 +0200 Subject: [PATCH 16/35] addgender: move out of SpriteBot.py --- SpriteBot.py | 72 +----------------------------------- commands/AddGender.py | 86 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 70 deletions(-) create mode 100644 commands/AddGender.py diff --git a/SpriteBot.py b/SpriteBot.py index bb81cf8..3228a7c 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -34,6 +34,7 @@ from commands.MoveRessource import MoveRessource from commands.SetRessourceCredit import SetRessourceCredit from commands.AddNode import AddNode +from commands.AddGender import AddGender from commands.DeleteNode import DeleteNode from commands.SetRessourceLock import SetRessourceLock @@ -233,6 +234,7 @@ def __init__(self, in_path, client): # staff AddNode(self), + AddGender(self), DeleteNode(self), ClearCache(self), SetProfile(self, True), @@ -2147,62 +2149,6 @@ async def setNeed(self, msg, args, needed): 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!") @@ -2291,7 +2237,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): elif permission_level == PermissionLevel.STAFF: return_msg = "**Approver Commands**\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" \ @@ -2377,17 +2322,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): f"`{prefix}bounties sprite`" else: return_msg = MESSAGE_BOUNTIES_DISABLED - 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" \ @@ -2649,8 +2583,6 @@ async def on_message(msg: discord.Message): elif base_arg == "unregister": await sprite_bot.deleteProfile(msg, args[1:]) # authorized commands - 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: 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 From 907ec2daa3a2e0a3810d2cd684e7dff957f5211a Mon Sep 17 00:00:00 2001 From: marius david Date: Mon, 16 Sep 2024 13:34:59 +0200 Subject: [PATCH 17/35] need and dontneed: migrated out of SpriteBot.py --- SpriteBot.py | 64 ++--------------------------------- commands/SetNeedNode.py | 74 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 61 deletions(-) create mode 100644 commands/SetNeedNode.py diff --git a/SpriteBot.py b/SpriteBot.py index 3228a7c..e2688a1 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -37,6 +37,7 @@ from commands.AddGender import AddGender from commands.DeleteNode import DeleteNode from commands.SetRessourceLock import SetRessourceLock +from commands.SetNeedNode import SetNeedNode from Constants import PHASES, PermissionLevel import psutil @@ -246,6 +247,8 @@ def __init__(self, in_path, client): MoveRessource(self, "sprite"), SetRessourceCredit(self, "portrait"), SetRessourceCredit(self, "sprite"), + SetNeedNode(self, True), + SetNeedNode(self, False), # admin SetRessourceLock(self, "portrait", True), @@ -2123,32 +2126,6 @@ async def modSpeciesForm(self, msg, args): self.saveTracker() self.changed = True - 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 removeGender(self, msg, args): if len(args) < 2 or len(args) > 3: await msg.channel.send(msg.author.mention + " Invalid number of args!") @@ -2238,8 +2215,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): elif permission_level == PermissionLevel.STAFF: return_msg = "**Approver Commands**\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}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" \ @@ -2334,35 +2309,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): 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 == "spritewip": return_msg = "**Command Help**\n" \ f"`{prefix}spritewip [Form Name] [Shiny] [Gender]`\n" \ @@ -2585,10 +2531,6 @@ async def on_message(msg: discord.Message): # authorized commands 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 == "spritewip" and authorized: await sprite_bot.completeSlot(msg, args[1:], "sprite", TrackerUtils.PHASE_INCOMPLETE) elif base_arg == "portraitwip" and authorized: 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 From eaf4706040f6aa4c7aea8fde394877220702ffd5 Mon Sep 17 00:00:00 2001 From: marius david Date: Sat, 5 Oct 2024 12:37:04 +0200 Subject: [PATCH 18/35] AddRessourceCredit: move to dedicated file --- SpriteBot.py | 79 ++-------------------------------- SpriteUtils.py | 1 + commands/AddRessourceCredit.py | 79 ++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 76 deletions(-) create mode 100644 commands/AddRessourceCredit.py diff --git a/SpriteBot.py b/SpriteBot.py index e2688a1..833d00b 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -33,6 +33,7 @@ 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.DeleteNode import DeleteNode @@ -247,6 +248,8 @@ def __init__(self, in_path, client): MoveRessource(self, "sprite"), SetRessourceCredit(self, "portrait"), SetRessourceCredit(self, "sprite"), + AddRessourceCredit(self, "portrait"), + AddRessourceCredit(self, "sprite"), SetNeedNode(self, True), SetNeedNode(self, False), @@ -1836,44 +1839,6 @@ def createCreditBlock(self, credit, base_credit, plainName=False): block += " +{0} more".format(credit_diff) return block - 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 = [] # type: ignore @@ -2221,8 +2186,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): 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}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}transferprofile` - Transfers the credit from absentee profile to a real one\n" @@ -2399,38 +2362,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): f"`{prefix}portraitfilled Pikachu Shiny Female`\n" \ f"`{prefix}portraitfilled Shaymin Sky`\n" \ f"`{prefix}portraitfilled Shaymin Sky Shiny`" - 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" \ @@ -2543,10 +2474,6 @@ async def on_message(msg: discord.Message): 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 == "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: diff --git a/SpriteUtils.py b/SpriteUtils.py index e67560b..633c81b 100644 --- a/SpriteUtils.py +++ b/SpriteUtils.py @@ -1804,6 +1804,7 @@ def simple_quant(img: Image.Image, colors) -> Image.Image: 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/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 From 14d758be559114ab8ec5f95c07aad6a2ea968330 Mon Sep 17 00:00:00 2001 From: marius david Date: Sat, 5 Oct 2024 13:02:03 +0200 Subject: [PATCH 19/35] ignore some mypy error --- BlueSkyUtils.py | 4 ++-- SpriteUtils.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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/SpriteUtils.py b/SpriteUtils.py index 633c81b..4279d9e 100644 --- a/SpriteUtils.py +++ b/SpriteUtils.py @@ -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) From aa5a504e9e209d99e3744d5d6c4c4fee7d77705b Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 3 Nov 2024 11:26:06 +0100 Subject: [PATCH 20/35] forcepush: switch to separate file --- SpriteBot.py | 9 +++------ commands/ForcePush.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 commands/ForcePush.py diff --git a/SpriteBot.py b/SpriteBot.py index 833d00b..c23a05c 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -39,6 +39,7 @@ from commands.DeleteNode import DeleteNode from commands.SetRessourceLock import SetRessourceLock from commands.SetNeedNode import SetNeedNode +from commands.ForcePush import ForcePush from Constants import PHASES, PermissionLevel import psutil @@ -258,6 +259,7 @@ def __init__(self, in_path, client): SetRessourceLock(self, "portrait", False), SetRessourceLock(self, "sprite", True), SetRessourceLock(self, "sprite", False), + ForcePush(self) ] self.writeLog("Startup Memory: {0}".format(psutil.Process().memory_info().rss)) @@ -1183,7 +1185,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)) @@ -2491,11 +2493,6 @@ async def on_message(msg: discord.Message): 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: 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.") From 988b8cf4a1faf226814b42ad01189f00cec76961 Mon Sep 17 00:00:00 2001 From: marius david Date: Mon, 4 Nov 2024 20:26:44 +0100 Subject: [PATCH 21/35] deletegender: move to a separate file --- SpriteBot.py | 76 +-------------------------------- commands/DeleteGender.py | 90 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 74 deletions(-) create mode 100644 commands/DeleteGender.py diff --git a/SpriteBot.py b/SpriteBot.py index c23a05c..816c072 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -36,6 +36,7 @@ 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.SetRessourceLock import SetRessourceLock from commands.SetNeedNode import SetNeedNode @@ -238,6 +239,7 @@ def __init__(self, in_path, client): # staff AddNode(self), AddGender(self), + DeleteGender(self), DeleteNode(self), ClearCache(self), SetProfile(self, True), @@ -2093,65 +2095,6 @@ async def modSpeciesForm(self, msg, args): 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, permission_level: Optional[PermissionLevel]): list_commands = len(args) == 0 if permission_level == None: @@ -2181,7 +2124,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): elif permission_level == PermissionLevel.STAFF: return_msg = "**Approver Commands**\n" \ - f"`{prefix}deletegender` - Removes the female sprite/portrait from the Pokemon\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" \ @@ -2262,18 +2204,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): f"`{prefix}bounties sprite`" else: return_msg = MESSAGE_BOUNTIES_DISABLED - 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 == "spritewip": return_msg = "**Command Help**\n" \ f"`{prefix}spritewip [Form Name] [Shiny] [Gender]`\n" \ @@ -2462,8 +2392,6 @@ async def on_message(msg: discord.Message): elif base_arg == "unregister": await sprite_bot.deleteProfile(msg, args[1:]) # authorized commands - elif base_arg == "deletegender" and authorized: - await sprite_bot.removeGender(msg, args[1:]) elif base_arg == "spritewip" and authorized: await sprite_bot.completeSlot(msg, args[1:], "sprite", TrackerUtils.PHASE_INCOMPLETE) elif base_arg == "portraitwip" and authorized: 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 From bfffe646e95a0e8064d367d20af38f8771eee8f5 Mon Sep 17 00:00:00 2001 From: marius david Date: Sat, 9 Nov 2024 13:15:47 +0100 Subject: [PATCH 22/35] canon/uncanon: move to a separate file --- SpriteBot.py | 28 +++----------------- commands/SetNodeCanon.py | 57 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 commands/SetNodeCanon.py diff --git a/SpriteBot.py b/SpriteBot.py index 816c072..6039889 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -39,6 +39,7 @@ from commands.DeleteGender import DeleteGender from commands.DeleteNode import DeleteNode from commands.SetRessourceLock import SetRessourceLock +from commands.SetNodeCanon import SetNodeCanon from commands.SetNeedNode import SetNeedNode from commands.ForcePush import ForcePush @@ -261,6 +262,8 @@ def __init__(self, in_path, client): SetRessourceLock(self, "portrait", False), SetRessourceLock(self, "sprite", True), SetRessourceLock(self, "sprite", False), + SetNodeCanon(self, True), + SetNodeCanon(self, False), ForcePush(self) ] @@ -1669,27 +1672,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: @@ -2413,10 +2395,6 @@ async def on_message(msg: discord.Message): 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 == "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: 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 From e1ccad6c9736b859e0541424ed31833e326fc984 Mon Sep 17 00:00:00 2001 From: marius david Date: Sat, 9 Nov 2024 13:32:59 +0100 Subject: [PATCH 23/35] rescan: move to a separate file --- SpriteBot.py | 12 +++--------- commands/Rescan.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 commands/Rescan.py diff --git a/SpriteBot.py b/SpriteBot.py index 6039889..e250727 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -42,6 +42,7 @@ from commands.SetNodeCanon import SetNodeCanon from commands.SetNeedNode import SetNeedNode from commands.ForcePush import ForcePush +from commands.Rescan import Rescan from Constants import PHASES, PermissionLevel import psutil @@ -264,7 +265,8 @@ def __init__(self, in_path, client): SetRessourceLock(self, "sprite", False), SetNodeCanon(self, True), SetNodeCanon(self, False), - ForcePush(self) + ForcePush(self), + Rescan(self) ] self.writeLog("Startup Memory: {0}".format(psutil.Process().memory_info().rss)) @@ -2032,12 +2034,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 modSpeciesForm(self, msg, args): if len(args) < 1 or len(args) > 2: await msg.channel.send(msg.author.mention + " Invalid number of args!") @@ -2393,8 +2389,6 @@ async def on_message(msg: discord.Message): # 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 == "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: 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 From bae4783976870a719ec29958200b38223dfd41cd Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 10 Nov 2024 10:29:26 +0100 Subject: [PATCH 24/35] More mypy typing around tracker dict --- SpriteBot.py | 34 ++++++++++++++++++++++++-------- TrackerUtils.py | 12 ++++++++--- commands/MoveNode.py | 2 ++ commands/QueryRessourceStatus.py | 1 + utils.py | 10 +++++++++- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/SpriteBot.py b/SpriteBot.py index e250727..32f2882 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -45,6 +45,7 @@ from commands.Rescan import Rescan from Constants import PHASES, PermissionLevel +from utils import unpack_optional import psutil # Housekeeping for login information @@ -191,7 +192,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)) @@ -745,7 +746,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) @@ -851,7 +857,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) @@ -1065,8 +1071,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) @@ -1125,8 +1131,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 @@ -1251,6 +1260,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) @@ -1290,14 +1302,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()) diff --git a/TrackerUtils.py b/TrackerUtils.py index 3c79ba1..1530f05 100644 --- a/TrackerUtils.py +++ b/TrackerUtils.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any +from typing import Dict, List, Any, Optional, Tuple import sys import os @@ -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("-") @@ -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/MoveNode.py b/commands/MoveNode.py index c310a9b..adedee4 100644 --- a/commands/MoveNode.py +++ b/commands/MoveNode.py @@ -78,6 +78,8 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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: diff --git a/commands/QueryRessourceStatus.py b/commands/QueryRessourceStatus.py index ea0ee62..62a506c 100644 --- a/commands/QueryRessourceStatus.py +++ b/commands/QueryRessourceStatus.py @@ -85,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/utils.py b/utils.py index 9461153..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): @@ -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 From d8a2f09e12efc9e75f83a957e189e1da4f63e92f Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 10 Nov 2024 12:07:44 +0100 Subject: [PATCH 25/35] ressource completion: move to a separate file --- SpriteBot.py | 143 ++--------------------------- commands/SetRessourceCompletion.py | 100 ++++++++++++++++++++ 2 files changed, 107 insertions(+), 136 deletions(-) create mode 100644 commands/SetRessourceCompletion.py diff --git a/SpriteBot.py b/SpriteBot.py index 32f2882..7f96c41 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -38,6 +38,7 @@ from commands.AddGender import AddGender from commands.DeleteGender import DeleteGender from commands.DeleteNode import DeleteNode +from commands.SetRessourceCompletion import SetRessourceCompletion from commands.SetRessourceLock import SetRessourceLock from commands.SetNodeCanon import SetNodeCanon from commands.SetNeedNode import SetNeedNode @@ -258,6 +259,12 @@ def __init__(self, in_path, client): 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), @@ -1577,34 +1584,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) @@ -2120,12 +2099,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): elif permission_level == PermissionLevel.STAFF: return_msg = "**Approver Commands**\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}modreward` - Toggles whether a sprite/portrait will have a custom reward\n" \ f"`{prefix}transferprofile` - Transfers the credit from absentee profile to a real one\n" @@ -2200,96 +2173,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): f"`{prefix}bounties sprite`" else: return_msg = MESSAGE_BOUNTIES_DISABLED - 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 == "modreward": return_msg = "**Command Help**\n" \ f"`{prefix}modreward [Form Name]`\n" \ @@ -2388,18 +2271,6 @@ async def on_message(msg: discord.Message): elif base_arg == "unregister": await sprite_bot.deleteProfile(msg, args[1:]) # authorized commands - 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 == "modreward" and authorized: await sprite_bot.modSpeciesForm(msg, args[1:]) elif base_arg == "transferprofile" and authorized: 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 From 1a5ba2dcb321252c5927a78e8d3fa3ad8d328914 Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 10 Nov 2024 14:43:29 +0100 Subject: [PATCH 26/35] absentprofiles: move to a separate file --- SpriteBot.py | 12 ++---------- commands/GetAbsenteeProfiles.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 commands/GetAbsenteeProfiles.py diff --git a/SpriteBot.py b/SpriteBot.py index 7f96c41..616b7ae 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -28,6 +28,7 @@ from commands.ClearCache import ClearCache from commands.GetProfile import GetProfile 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 @@ -239,6 +240,7 @@ def __init__(self, in_path, client): DeleteRessourceCredit(self, "sprite"), GetProfile(self), SetProfile(self, False), + GetAbsenteeProfiles(self), # staff AddNode(self), @@ -1824,14 +1826,6 @@ def createCreditBlock(self, credit, base_credit, plainName=False): block += " +{0} more".format(credit_diff) return block - async def getAbsentProfiles(self, msg): - total_names = ["Absentee profiles:"] - msg_ids = [] # type: ignore - 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 transferProfile(self, msg, args): if len(args) != 2: await msg.channel.send(msg.author.mention + " Invalid args") @@ -2266,8 +2260,6 @@ async def on_message(msg: discord.Message): await sprite_bot.placeBounty(msg, args[1:], "portrait") elif base_arg == "bounties": await sprite_bot.listBounties(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 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 From 446a64ec15a1c114aea1fc14db64b24d2a915b8d Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 10 Nov 2024 18:20:44 +0100 Subject: [PATCH 27/35] shutdown: move to a separate file --- SpriteBot.py | 12 +++--------- commands/Shutdown.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 commands/Shutdown.py diff --git a/SpriteBot.py b/SpriteBot.py index 616b7ae..89841aa 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -45,6 +45,7 @@ from commands.SetNeedNode import SetNeedNode from commands.ForcePush import ForcePush from commands.Rescan import Rescan +from commands.Shutdown import Shutdown from Constants import PHASES, PermissionLevel from utils import unpack_optional @@ -276,7 +277,8 @@ def __init__(self, in_path, client): SetNodeCanon(self, True), SetNodeCanon(self, False), ForcePush(self), - Rescan(self) + Rescan(self), + Shutdown(self) ] self.writeLog("Startup Memory: {0}".format(psutil.Process().memory_info().rss)) @@ -342,12 +344,6 @@ async def updateBot(self, msg): 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) @@ -2272,8 +2268,6 @@ async def on_message(msg: discord.Message): await sprite_bot.promote(msg, args[1:]) 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 in ["gr", "tr", "checkr"]: pass else: 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 From 6af9ba1efa41a6f46702252c8fe513dd5fab3c49 Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 10 Nov 2024 19:01:01 +0100 Subject: [PATCH 28/35] TransferProfile: move to a separate file --- SpriteBot.py | 59 ++----------------------------- commands/TransferProfile.py | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 56 deletions(-) create mode 100644 commands/TransferProfile.py diff --git a/SpriteBot.py b/SpriteBot.py index 89841aa..47ff33c 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -39,6 +39,7 @@ 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 @@ -250,6 +251,7 @@ def __init__(self, in_path, client): DeleteNode(self), ClearCache(self), SetProfile(self, True), + TransferProfile(self), RenameNode(self), ReplaceRessource(self, "portrait"), ReplaceRessource(self, "sprite"), @@ -1822,45 +1824,6 @@ def createCreditBlock(self, credit, base_credit, plainName=False): block += " +{0} more".format(credit_diff) return block - 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) @@ -2089,8 +2052,7 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): elif permission_level == PermissionLevel.STAFF: return_msg = "**Approver Commands**\n" \ - f"`{prefix}modreward` - Toggles whether a sprite/portrait will have a custom reward\n" \ - f"`{prefix}transferprofile` - Transfers the credit from absentee profile to a real one\n" + f"`{prefix}modreward` - Toggles whether a sprite/portrait will have a custom reward\n" for command in self.commands: if permission_level == command.getRequiredPermission(): @@ -2173,19 +2135,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): "**Examples**\n" \ f"`{prefix}modreward Unown`\n" \ f"`{prefix}modreward Minior Red`" - 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`" else: return_msg = "Unknown Command." await msg.channel.send(msg.author.mention + " {0}".format(return_msg)) @@ -2261,8 +2210,6 @@ async def on_message(msg: discord.Message): # authorized commands 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:]) # root commands elif base_arg == "promote" and msg.author.id == sprite_bot.config.root: await sprite_bot.promote(msg, args[1:]) 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 From f28ac1906c2430060a2086dafdb412b73e57ad52 Mon Sep 17 00:00:00 2001 From: marius david Date: Sun, 10 Nov 2024 20:40:01 +0100 Subject: [PATCH 29/35] update: move to a separate file --- SpriteBot.py | 18 ++---------------- commands/Update.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 commands/Update.py diff --git a/SpriteBot.py b/SpriteBot.py index 47ff33c..48eb9bf 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -46,6 +46,7 @@ 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 @@ -280,6 +281,7 @@ def __init__(self, in_path, client): SetNodeCanon(self, False), ForcePush(self), Rescan(self), + Update(self), Shutdown(self) ] @@ -332,20 +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 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) @@ -2213,8 +2201,6 @@ async def on_message(msg: discord.Message): # root commands elif base_arg == "promote" and msg.author.id == sprite_bot.config.root: await sprite_bot.promote(msg, args[1:]) - elif base_arg == "update" and msg.author.id == sprite_bot.config.root: - await sprite_bot.updateBot(msg) elif base_arg in ["gr", "tr", "checkr"]: pass else: 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 From 443ce0685dc154c6f971557d4cdbad6f16339662 Mon Sep 17 00:00:00 2001 From: marius david Date: Thu, 14 Nov 2024 18:40:05 +0100 Subject: [PATCH 30/35] bounties: move to a seperate file --- Constants.py | 2 + SpriteBot.py | 73 ++++-------------------------------- commands/BaseCommand.py | 14 ++++--- commands/ListBounties.py | 81 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 72 deletions(-) create mode 100644 commands/ListBounties.py diff --git a/Constants.py b/Constants.py index 900561c..7347bbd 100644 --- a/Constants.py +++ b/Constants.py @@ -37,6 +37,8 @@ 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 diff --git a/SpriteBot.py b/SpriteBot.py index 48eb9bf..195b842 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any, Optional, Tuple import os @@ -25,6 +25,7 @@ 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 commands.SetProfile import SetProfile @@ -49,7 +50,7 @@ from commands.Update import Update from commands.Shutdown import Shutdown -from Constants import PHASES, PermissionLevel +from Constants import PHASES, PermissionLevel, MESSAGE_BOUNTIES_DISABLED from utils import unpack_optional import psutil @@ -62,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() @@ -244,6 +243,7 @@ def __init__(self, in_path, client): GetProfile(self), SetProfile(self, False), GetAbsenteeProfiles(self), + ListBounties(self), # staff AddNode(self), @@ -431,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]) @@ -1724,50 +1724,6 @@ async def promote(self, msg, name_args): await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) - 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 = [] # type: ignore - 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) - def createCreditAttribution(self, mention, plainName=False): if plainName: # "plainName" actually refers to "social-media-ready name" @@ -2035,15 +1991,14 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): 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" \ - f"`{prefix}bounties` - View top bounties\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: - if permission_level == command.getRequiredPermission(): + if permission_level == command.getRequiredPermission() and command.shouldListInHelp(): return_msg += f"`{prefix}{command.getCommand()}` - {command.getSingleLineHelp(server_config)}\n" if permission_level == PermissionLevel.EVERYONE: @@ -2101,18 +2056,6 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): 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 == "modreward": return_msg = "**Command Help**\n" \ f"`{prefix}modreward [Form Name]`\n" \ @@ -2191,8 +2134,6 @@ async def on_message(msg: discord.Message): 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 == "unregister": await sprite_bot.deleteProfile(msg, args[1:]) # authorized commands diff --git a/commands/BaseCommand.py b/commands/BaseCommand.py index 0131104..09dbe1f 100644 --- a/commands/BaseCommand.py +++ b/commands/BaseCommand.py @@ -30,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""" @@ -37,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/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 From 31fa1e082405a79b38d5b4e41d744c0568f9e354 Mon Sep 17 00:00:00 2001 From: Audino <2676737+audinowho@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:03:18 -0800 Subject: [PATCH 31/35] utils merge from marius --- BlueSkyUtils.py | 2 +- Constants.py | 38 +++++++++++++++++++++++++++-------- SpriteUtils.py | 53 ++++++++++++++++++++++++++----------------------- TrackerUtils.py | 16 +++++++++++---- utils.py | 20 +++++++++++++------ 5 files changed, 85 insertions(+), 44 deletions(-) diff --git a/BlueSkyUtils.py b/BlueSkyUtils.py index 6f2c83d..11bf380 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() 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/SpriteUtils.py b/SpriteUtils.py index 1eb5cb3..6b6af9e 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: @@ -922,7 +924,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]) @@ -1087,7 +1089,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 @@ -1250,8 +1252,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 @@ -1281,7 +1283,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]) @@ -1313,7 +1315,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 @@ -1510,7 +1512,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: @@ -1555,7 +1557,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)): @@ -1584,11 +1586,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: @@ -1620,7 +1622,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) @@ -1808,9 +1810,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 52ed370..0a57b22 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 @@ -228,7 +230,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 @@ -524,7 +526,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: @@ -540,7 +542,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("-") @@ -709,7 +711,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 @@ -910,6 +912,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 @@ -992,6 +997,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/utils.py b/utils.py index 621e621..0105fe5 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): @@ -55,7 +55,7 @@ def addLoc(loc1: Tuple[int, int], loc2: Tuple[int, int], sub: bool = False): 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 From 195ba8edca075cd86e074a1ec6db93825e2469ae Mon Sep 17 00:00:00 2001 From: Audino <2676737+audinowho@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:50:34 -0800 Subject: [PATCH 32/35] add permission level --- commands/AutoRecolorResource.py | 4 ++++ commands/BaseCommand.py | 20 ++++++++++++++------ commands/DeleteResourceCredit.py | 13 +++++++++---- commands/GetProfile.py | 4 ++++ commands/ListResource.py | 4 ++++ commands/QueryResourceCredit.py | 6 +++++- commands/QueryResourceStatus.py | 6 +++++- 7 files changed, 45 insertions(+), 12 deletions(-) diff --git a/commands/AutoRecolorResource.py b/commands/AutoRecolorResource.py index 3922f60..7c7f78e 100644 --- a/commands/AutoRecolorResource.py +++ b/commands/AutoRecolorResource.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", resource_type: str): super().__init__(spritebot) self.resource_type = resource_type + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: return f"autocolor{self.resource_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/DeleteResourceCredit.py b/commands/DeleteResourceCredit.py index 59110cf..4a402b6 100644 --- a/commands/DeleteResourceCredit.py +++ b/commands/DeleteResourceCredit.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", resource_type: str): super().__init__(spritebot) self.resource_type = resource_type + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: return f"delete{self.resource_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 @@ -118,4 +123,4 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): # stage a post in submissions await self.spritebot.postStagedSubmission(submit_channel, "--deleteauthor", "", full_idx, chosen_node, self.resource_type, author + "/" + wanted_author, - False, None, base_file, base_name, None) \ No newline at end of file + False, None, base_file, base_name, None) \ 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/ListResource.py b/commands/ListResource.py index 1acf6d2..b5504c5 100644 --- a/commands/ListResource.py +++ b/commands/ListResource.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", resource_type: str): super().__init__(spritebot) self.resource_type = resource_type + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: return f"list{self.resource_type}" diff --git a/commands/QueryResourceCredit.py b/commands/QueryResourceCredit.py index 00e9a7b..9a0e384 100644 --- a/commands/QueryResourceCredit.py +++ b/commands/QueryResourceCredit.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", resource_type: str, display_history: self.resource_type = resource_type self.display_history = display_history + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.EVERYONE + def getCommand(self) -> str: if self.display_history: return f"{self.resource_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/QueryResourceStatus.py b/commands/QueryResourceStatus.py index adb9759..87cb6f9 100644 --- a/commands/QueryResourceStatus.py +++ b/commands/QueryResourceStatus.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", resource_type: str, is_derivation: bo self.resource_type = resource_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.resource_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.resource_type) From 32bf500b528c37cd53ea577ad8e688963dd3e897 Mon Sep 17 00:00:00 2001 From: Audino <2676737+audinowho@users.noreply.github.com> Date: Sat, 11 Jul 2026 20:09:09 -0800 Subject: [PATCH 33/35] wave 1 command merge from marius --- SpriteBot.py | 954 ++++--------------------------- commands/AddGender.py | 86 +++ commands/AddNode.py | 76 +++ commands/ClearCache.py | 48 ++ commands/DeleteGender.py | 90 +++ commands/DeleteNode.py | 77 +++ commands/DeleteResourceCredit.py | 4 +- commands/ForcePush.py | 28 + commands/GetAbsenteeProfiles.py | 33 ++ commands/ListBounties.py | 81 +++ commands/MoveNode.py | 139 +++++ commands/RenameNode.py | 72 +++ commands/Rescan.py | 30 + commands/SetNeedNode.py | 74 +++ commands/SetNodeCanon.py | 61 ++ commands/SetProfile.py | 83 +++ commands/Shutdown.py | 33 ++ commands/TransferProfile.py | 70 +++ commands/Update.py | 44 ++ 19 files changed, 1236 insertions(+), 847 deletions(-) create mode 100644 commands/AddGender.py create mode 100644 commands/AddNode.py create mode 100644 commands/ClearCache.py create mode 100644 commands/DeleteGender.py create mode 100644 commands/DeleteNode.py create mode 100644 commands/ForcePush.py create mode 100644 commands/GetAbsenteeProfiles.py create mode 100644 commands/ListBounties.py create mode 100644 commands/MoveNode.py create mode 100644 commands/RenameNode.py create mode 100644 commands/Rescan.py create mode 100644 commands/SetNeedNode.py create mode 100644 commands/SetNodeCanon.py create mode 100644 commands/SetProfile.py create mode 100644 commands/Shutdown.py create mode 100644 commands/TransferProfile.py create mode 100644 commands/Update.py diff --git a/SpriteBot.py b/SpriteBot.py index 15b5934..5706afe 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,7 +25,27 @@ from commands.ListResource import ListResource from commands.QueryResourceCredit import QueryResourceCredit from commands.DeleteResourceCredit import DeleteResourceCredit +from commands.ListBounties import ListBounties +from commands.ClearCache import ClearCache from commands.GetProfile import GetProfile +from commands.SetProfile import SetProfile +from commands.GetAbsenteeProfiles import GetAbsenteeProfiles +from commands.RenameNode import RenameNode +from commands.MoveNode import MoveNode +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.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 from Constants import PHASES import psutil @@ -39,8 +59,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() @@ -221,7 +239,31 @@ def __init__(self, in_path, client): QueryResourceCredit(self, "sprite", True), DeleteResourceCredit(self, "portrait"), DeleteResourceCredit(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), + MoveNode(self), + SetNeedNode(self, True), + SetNeedNode(self, False), + + # admin + 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)) @@ -273,26 +315,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) @@ -438,6 +460,30 @@ async def isAuthorized(self, user, guild): return True return False + async def getUserPermission(self, user, guild): + """Get a user permission level""" + if user.id == self.client.user.id: + return PermissionLevel.EVERYONE + if user.id == self.config.root: + return PermissionLevel.ADMIN + guild_id_str = str(guild.id) + + if self.config.servers[guild_id_str].approval == 0: + return PermissionLevel.EVERYONE + + approve_role = guild.get_role(self.config.servers[guild_id_str].approval) + + try: + user_member = await guild.fetch_member(user.id) + except discord.NotFound as e: + user_member = None + + if user_member is None: + return PermissionLevel.EVERYONE + if approve_role in user_member.roles: + return PermissionLevel.STAFF + return PermissionLevel.EVERYONE + def remove_self_mention(self, split_args): for idx in range(len(split_args)): single_arg = split_args[len(split_args) - 1 - idx] @@ -1613,109 +1659,6 @@ async def checkMoveLock(self, full_idx_from, chosen_node_from, full_idx_to, chos 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) - - diff_forms_on_same_species = False - if len(full_idx_from) == 2 and len(full_idx_to) == 2 and full_idx_from[0] == full_idx_to[0]: - diff_forms_on_same_species = True - - if not diff_forms_on_same_species: - # 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("->") @@ -1987,30 +1930,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 - if len(full_idx) != 2: - await msg.channel.send(msg.author.mention + " Must specify Pokemon and form.") - return - - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) - - TrackerUtils.setCanon(chosen_node, True) - - 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 showcase(self, msg, name_args): @@ -2140,64 +2059,6 @@ async def setLock(self, msg, name_args, asset_type, lock_state): 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" @@ -2334,80 +2195,6 @@ async def addCredit(self, msg, name_args, asset_type): await self.postStagedSubmission(submit_channel, submit_args, "", 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): - msg_mention = "<@!{0}>".format(msg.author.id) - - 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]) - 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.") - return - msg_mention = self.getFormattedCredit(args[0]) - new_credit = TrackerUtils.CreditEntry(args[1], args[2]) - 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) @@ -2568,102 +2355,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 - - new_id_int = max([int(i) for i in self.tracker.keys()]) + 1 - new_idx = "{:04d}".format(new_id_int) - self.tracker[new_idx] = TrackerUtils.createSpeciesNode(species_name) - - await msg.channel.send(msg.author.mention + " Added #{0:03d}: {1}!".format(new_id_int, 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 = TrackerUtils.canonCheck(species_name, form_name) - - new_id_int = max([int(i) for i in species_dict.subgroups.keys()]) + 1 - new_idx = "{:04d}".format(new_id_int) - species_dict.subgroups[new_idx] = 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!") @@ -2709,210 +2400,43 @@ async def modSpeciesForm(self, msg, args): self.changed = True - 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): + async def help(self, msg, args, permission_level: PermissionLevel): + list_commands = len(args) == 0 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}movesprite` - Swaps the sprites for two Pokemon/formes\n" \ + f"`{prefix}moveportrait` - Swaps the portraits for two Pokemon/formes\n" \ + f"`{prefix}clonesprite` - Copies the sprites for two Pokemon/formes\n" \ + f"`{prefix}cloneportrait` - Copies the portraits 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}showcase` - Showcases a sprite or portrait to social media channels\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" + return_msg += f"Type `{prefix}help` with the name of a command to learn more about it." else: base_arg = args[0] return_msg = None @@ -2962,151 +2486,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}clonesprite` - Copies the sprites for two Pokemon/formes\n" \ - f"`{prefix}cloneportrait` - Copies 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}showcase` - Showcases a sprite or portrait to social media channels\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"`{prefix}canon` - Marks the Pokemon forme as non-canon.\n" \ - f"`{prefix}noncanon` - Marks the Pokemon forme as canon.\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" \ @@ -3165,20 +2544,6 @@ async def staffhelp(self, msg, args): f"`{prefix}cloneportrait Zoroark Alternate -> Zoroark`\n" \ f"`{prefix}cloneportrait Missingno_ Kleavor -> Kleavor`\n" \ f"`{prefix}cloneportrait 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" \ @@ -3381,71 +2746,6 @@ 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`" - elif base_arg == "canon": - return_msg = "**Command Help**\n" \ - f"`{prefix}canon [Form Name]`\n" \ - "Marks the Pokemon forme as canon. Canon forms generally have their own data slot within a main-series game." \ - " Only works on formes and does not work on system-wide non-canon forms.\n" \ - " Affects shiny and gender within the form, even if created later.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}canon Pichu Spiky`" - elif base_arg == "noncanon": - return_msg = "**Command Help**\n" \ - f"`{prefix}noncanon [Form Name]`\n" \ - "Marks the Pokemon forme as non-canon. Non-canon forms generally do not have their own data slot within a main-series game." \ - " Only works on formes and does not work on system-wide non-canon forms.\n" \ - " Affects shiny and gender within the form, even if created later.\n" \ - "`Pokemon Name` - Name of the Pokemon\n" \ - "`Form Name` - [Optional] Form name of the Pokemon\n" \ - "**Examples**\n" \ - f"`{prefix}noncanon Burmy No_Cloak`" else: return_msg = "Unknown Command." await msg.channel.send(msg.author.mention + " {0}".format(return_msg)) @@ -3506,45 +2806,30 @@ async def on_message(msg: discord.Message): if not mentioned: return - 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) for command in sprite_bot.commands: if base_arg == command.getCommand(): - await command.executeCommand(msg, cmd_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, cmd_args[1:]) + else: + await msg.channel.send("{} Not authorized (Permission level `{}` needed.)".format(msg.author.mention, command.getRequiredPermission().displayname())) return if base_arg == "help": - await sprite_bot.help(msg, cmd_args[1:]) + await sprite_bot.help(msg, cmd_args[1:], PermissionLevel.EVERYONE) elif base_arg == "staffhelp": - await sprite_bot.staffhelp(msg, cmd_args[1:]) + await sprite_bot.help(msg, cmd_args[1:], PermissionLevel.STAFF) # primary commands elif base_arg == "spritebounty": await sprite_bot.placeBounty(msg, cmd_args[1:], "sprite") elif base_arg == "portraitbounty": await sprite_bot.placeBounty(msg, cmd_args[1:], "portrait") - elif base_arg == "bounties": - await sprite_bot.listBounties(msg, cmd_args[1:]) - elif base_arg == "register": - await sprite_bot.setProfile(msg, cmd_args[1:]) - elif base_arg == "absentprofiles": - await sprite_bot.getAbsentProfiles(msg) elif base_arg == "unregister": await sprite_bot.deleteProfile(msg, cmd_args[1:]) # authorized commands - elif base_arg == "add" and authorized: - await sprite_bot.addSpeciesForm(msg, cmd_args[1:]) - elif base_arg == "delete" and authorized: - await sprite_bot.removeSpeciesForm(msg, cmd_args[1:]) - elif base_arg == "rename" and authorized: - await sprite_bot.renameSpeciesForm(msg, cmd_args[1:]) - elif base_arg == "addgender" and authorized: - await sprite_bot.addGender(msg, cmd_args[1:]) - elif base_arg == "deletegender" and authorized: - await sprite_bot.removeGender(msg, cmd_args[1:]) - elif base_arg == "need" and authorized: - await sprite_bot.setNeed(msg, cmd_args[1:], True) - elif base_arg == "dontneed" and authorized: - await sprite_bot.setNeed(msg, cmd_args[1:], False) elif base_arg == "movesprite" and authorized: await sprite_bot.moveSlot(msg, cmd_args[1:], "sprite") elif base_arg == "moveportrait" and authorized: @@ -3553,8 +2838,6 @@ async def on_message(msg: discord.Message): await sprite_bot.cloneSlot(msg, cmd_args[1:], "sprite") elif base_arg == "cloneportrait" and msg.author.id == sprite_bot.config.root: await sprite_bot.cloneSlot(msg, cmd_args[1:], "portrait") - elif base_arg == "move" and authorized: - await sprite_bot.moveSlotRecursive(msg, cmd_args[1:]) elif base_arg == "replacesprite" and authorized: await sprite_bot.replaceSlot(msg, cmd_args[1:], "sprite") elif base_arg == "replaceportrait" and authorized: @@ -3581,19 +2864,9 @@ async def on_message(msg: discord.Message): await sprite_bot.addCredit(msg, cmd_args[1:], "portrait") elif base_arg == "modreward" and authorized: await sprite_bot.modSpeciesForm(msg, cmd_args[1:]) - elif base_arg == "transferprofile" and authorized: - await sprite_bot.transferProfile(msg, cmd_args[1:]) - elif base_arg == "clearcache" and authorized: - await sprite_bot.clearCache(msg, cmd_args[1:]) - elif base_arg == "canon" and authorized: - await sprite_bot.setCanon(msg, cmd_args[1:], True) - elif base_arg == "noncanon" and authorized: - await sprite_bot.setCanon(msg, cmd_args[1:], False) # root commands elif base_arg == "showcase" and authorized: await sprite_bot.showcase(msg, cmd_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, cmd_args[1:], "portrait", False) elif base_arg == "unlocksprite" and msg.author.id == sprite_bot.config.root: @@ -3602,15 +2875,6 @@ async def on_message(msg: discord.Message): await sprite_bot.setLock(msg, cmd_args[1:], "portrait", True) elif base_arg == "locksprite" and msg.author.id == sprite_bot.config.root: await sprite_bot.setLock(msg, cmd_args[1:], "sprite", True) - 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: 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..4955c96 --- /dev/null +++ b/commands/AddNode.py @@ -0,0 +1,76 @@ +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 + + new_id_int = max([int(i) for i in self.tracker.keys()]) + 1 + new_idx = "{:04d}".format(new_id_int) + self.spritebot.tracker[new_idx] = TrackerUtils.createSpeciesNode(species_name) + + await msg.channel.send(msg.author.mention + " Added #{0:03d}: {1}!".format(new_id_int, 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 = TrackerUtils.canonCheck(species_name, form_name) + + new_id_int = max([int(i) for i in species_dict.subgroups.keys()]) + 1 + new_idx = "{:04d}".format(new_id_int) + species_dict.subgroups[new_idx] = 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/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/DeleteResourceCredit.py b/commands/DeleteResourceCredit.py index 4a402b6..6ccf8bb 100644 --- a/commands/DeleteResourceCredit.py +++ b/commands/DeleteResourceCredit.py @@ -35,7 +35,7 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: "Deletes the specified author from the credits of the portrait. " \ "This makes a post in the submissions channel, asking other approvers to sign off." \ "The post must be approved by the author being removed.\n" \ - "`Author ID` - The discord ID of the author to set as primary\n" \ + "`Author ID` - The discord ID of the author to remove\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" \ @@ -46,7 +46,7 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: "Deletes the specified author from the credits of the sprite. " \ "This makes a post in the submissions channel, asking other approvers to sign off." \ "The post must be approved by the author being removed.\n" \ - "`Author ID` - The discord ID of the author to set as primary\n" \ + "`Author ID` - The discord ID of the author to remove\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" \ 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/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/MoveNode.py b/commands/MoveNode.py new file mode 100644 index 0000000..ac32d4b --- /dev/null +++ b/commands/MoveNode.py @@ -0,0 +1,139 @@ +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 + + diff_forms_on_same_species = False + if len(full_idx_from) == 2 and len(full_idx_to) == 2 and full_idx_from[0] == full_idx_to[0]: + diff_forms_on_same_species = True + + if not diff_forms_on_same_species: + # 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/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/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..5a08c77 --- /dev/null +++ b/commands/SetNodeCanon.py @@ -0,0 +1,61 @@ +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 + if len(full_idx) != 2: + await msg.channel.send(msg.author.mention + " Must specify Pokemon and form.") + 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/Shutdown.py b/commands/Shutdown.py new file mode 100644 index 0000000..1915e85 --- /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() 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 From 341f53f192b41573afe0c2b967b5cf20c97e772c Mon Sep 17 00:00:00 2001 From: Audino <2676737+audinowho@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:41:29 -0800 Subject: [PATCH 34/35] wave 2 command merge from marius --- SpriteBot.py | 618 ++++-------------------------- commands/AddNode.py | 2 +- commands/AddResourceCredit.py | 93 +++++ commands/DeleteGender.py | 2 +- commands/MoveNode.py | 5 +- commands/MoveResource.py | 117 ++++++ commands/ReplaceResource.py | 98 +++++ commands/SetProfile.py | 1 + commands/SetResourceCompletion.py | 100 +++++ commands/SetResourceCredit.py | 80 ++++ commands/SetResourceLock.py | 87 +++++ 11 files changed, 663 insertions(+), 540 deletions(-) create mode 100644 commands/AddResourceCredit.py create mode 100644 commands/MoveResource.py create mode 100644 commands/ReplaceResource.py create mode 100644 commands/SetResourceCompletion.py create mode 100644 commands/SetResourceCredit.py create mode 100644 commands/SetResourceLock.py diff --git a/SpriteBot.py b/SpriteBot.py index 5706afe..80f2e62 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -31,12 +31,18 @@ from commands.SetProfile import SetProfile from commands.GetAbsenteeProfiles import GetAbsenteeProfiles from commands.RenameNode import RenameNode +from commands.ReplaceResource import ReplaceResource from commands.MoveNode import MoveNode +from commands.MoveResource import MoveResource +from commands.SetResourceCredit import SetResourceCredit +from commands.AddResourceCredit import AddResourceCredit 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.SetResourceCompletion import SetResourceCompletion +from commands.SetResourceLock import SetResourceLock from commands.SetNodeCanon import SetNodeCanon from commands.SetNeedNode import SetNeedNode from commands.ForcePush import ForcePush @@ -120,7 +126,7 @@ 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 @@ -196,7 +202,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)) @@ -225,6 +231,7 @@ def __init__(self, in_path, client): # register commands self.commands = [ + # everyone QueryResourceStatus(self, "portrait", False), QueryResourceStatus(self, "portrait", True), QueryResourceStatus(self, "sprite", False), @@ -253,11 +260,29 @@ def __init__(self, in_path, client): SetProfile(self, True), TransferProfile(self), RenameNode(self), + ReplaceResource(self, "portrait"), + ReplaceResource(self, "sprite"), MoveNode(self), + MoveResource(self, "portrait"), + MoveResource(self, "sprite"), + SetResourceCredit(self, "portrait"), + SetResourceCredit(self, "sprite"), + AddResourceCredit(self, "portrait"), + AddResourceCredit(self, "sprite"), SetNeedNode(self, True), SetNeedNode(self, False), + SetResourceCompletion(self, "portrait", TrackerUtils.PHASE_INCOMPLETE), + SetResourceCompletion(self, "portrait", TrackerUtils.PHASE_EXISTS), + SetResourceCompletion(self, "portrait", TrackerUtils.PHASE_FULL), + SetResourceCompletion(self, "sprite", TrackerUtils.PHASE_INCOMPLETE), + SetResourceCompletion(self, "sprite", TrackerUtils.PHASE_EXISTS), + SetResourceCompletion(self, "sprite", TrackerUtils.PHASE_FULL), # admin + SetResourceLock(self, "portrait", True), + SetResourceLock(self, "portrait", False), + SetResourceLock(self, "sprite", True), + SetResourceLock(self, "sprite", False), SetNodeCanon(self, True), SetNodeCanon(self, False), ForcePush(self), @@ -271,7 +296,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) @@ -412,7 +437,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]) @@ -718,7 +743,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((reduced_file, return_name.replace('.png', '_reduced.png'))) add_msg += "\nReduced Color Preview included." @@ -765,6 +790,10 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals, sil await msg.delete() return + assert full_idx is not None + assert asset_type is not None + assert recolor is not None + # TODO: refactor with above code chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) if not chosen_node: @@ -885,7 +914,7 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals, sil 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) @@ -1099,8 +1128,8 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals, sil 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) @@ -1112,7 +1141,7 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals, sil 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) @@ -1150,8 +1179,12 @@ async def submissionDeclined(self, msg, orig_sender, declines): await msg.delete() return + assert full_idx is not None + assert asset_type is not None + assert recolor is not None + # TODO: refactor with above code - chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) + chosen_node = unpack_optional(TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0)) if not chosen_node: await self.getChatChannel(msg.guild.id).send(orig_sender + " " + "Removed unknown file: {0}".format(file_name)) await msg.delete() @@ -1225,7 +1258,8 @@ async def pollSubmission(self, msg): ms = reaction else: async for user in reaction.users(): - if await self.isAuthorized(user, msg.guild): + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.canPerformAction(PermissionLevel.STAFF): pass else: remove_users.append((reaction, user)) @@ -1250,7 +1284,8 @@ async def pollSubmission(self, msg): if ss: async for user in ss.users(): - if user.id == self.config.root: + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.canPerformAction(PermissionLevel.ADMIN): auto = True approve.append(user.id) else: @@ -1262,14 +1297,17 @@ async def pollSubmission(self, msg): if (deleting or no_credit) and user_author_id == orig_author: approve.append(user.id) consent = True - elif await self.isAuthorized(user, msg.guild): - approve.append(user.id) - elif user.id != self.client.user.id: - remove_users.append((cks, user)) + else: + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.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): + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.canPerformAction(PermissionLevel.STAFF): warn = True else: remove_users.append((ws, user)) @@ -1277,7 +1315,8 @@ 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: + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.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) @@ -1286,7 +1325,8 @@ async def pollSubmission(self, msg): if ms: async for user in ms.users(): - if await self.isAuthorized(user, msg.guild) or user.id == orig_sender_id: + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.canPerformAction(PermissionLevel.STAFF) or user.id == orig_sender_id: silent = True else: remove_users.append((ms, user)) @@ -1294,6 +1334,10 @@ 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) return True @@ -1332,15 +1376,21 @@ 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 + mentioned = False for mention in msg.mentions: if mention.id == self.client.user.id: @@ -1621,33 +1671,6 @@ async def retrieveLinkMsg(self, full_idx, chosen_node, asset_type, recolor): 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) @@ -1659,147 +1682,6 @@ async def checkMoveLock(self, full_idx_from, chosen_node_from, full_idx_to, chos chosen_img_to = SpriteUtils.getLinkImg(chosen_img_to_link) SpriteUtils.verifyPortraitLock(chosen_node_from, chosen_path_from, chosen_img_to, False) - 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))) - - if not TrackerUtils.reportableCheck(name_seq_from): - #urls = await self.postSocialMedia(full_idx_to, asset_type, "Showcased", self.createCreditBlock(credit_data, None, True)) - #await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) - pass - - if not TrackerUtils.reportableCheck(name_seq_to): - #urls = await self.postSocialMedia(full_idx_to, asset_type, "Showcased", self.createCreditBlock(credit_data, None, True)) - #await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) - pass - async def cloneSlot(self, msg, name_args, asset_type): try: delim_idx = name_args.index("->") @@ -1883,7 +1765,8 @@ async def placeBounty(self, msg, name_args, asset_type): return if self.config.points == 0: - if not await self.isAuthorized(msg.author, msg.guild): + user_perms = await self.getUserPermission(msg.author, msg.guild) + if not user_perms.canPerformAction(PermissionLevel.STAFF): await msg.channel.send(msg.author.mention + " Not authorized.") return else: @@ -2030,35 +1913,6 @@ def parseFileNames(self, chosen_node, asset_type, file_args): failed_file_names.append(file_name) return final_file_names, failed_file_names - 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) - - final_file_names, failed_file_names = self.parseFileNames(chosen_node, asset_type, name_args[-1]) - - if len(failed_file_names) > 0: - await msg.channel.send(msg.author.mention + " Could not find the emotion/animations:\n{0}.".format(",".join(failed_file_names))) - return - - for file_name in final_file_names: - 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), ",".join(final_file_names), lock_str)) - - self.saveTracker() - self.changed = True - def createCreditAttribution(self, mention, plainName=False): if plainName: # "plainName" actually refers to "social-media-ready name" @@ -2145,56 +1999,6 @@ async def resetCredit(self, msg, name_args, asset_type): self.saveTracker() self.changed = True - async def addCredit(self, msg, name_args, asset_type): - # compute answer from current status - if len(name_args) < 3: - await msg.channel.send(msg.author.mention + " Specify a user ID, file list, and Pokemon.") - return - - wanted_author = self.getFormattedCredit(name_args[0]) - name_seq = [TrackerUtils.sanitizeName(i) for i in name_args[1:-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_args = "--addauthor" - - file_names = name_args[-1] - - if file_names != "\"": - final_file_names, failed_file_names = self.parseFileNames(chosen_node, asset_type, file_names) - if len(failed_file_names) > 0: - await msg.channel.send(msg.author.mention + " Could not find the emotion/animations:\n{0}.".format(",".join(failed_file_names))) - return False - - submit_args = submit_args + " --files " + ",".join(final_file_names) - - 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, submit_args, "", full_idx, chosen_node, asset_type, author + "/" + wanted_author, - False, None, base_file, base_name, None) - async def deleteProfile(self, msg, args): msg_mention = "<@!{0}>".format(msg.author.id) @@ -2203,7 +2007,8 @@ async def deleteProfile(self, msg, args): "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): + user_perms = await self.getUserPermission(msg.author, msg.guild) + if not user_perms.canPerformAction(PermissionLevel.STAFF): await msg.channel.send(msg.author.mention + " Not authorized to delete registration.") return msg_mention = self.getFormattedCredit(args[0]) @@ -2222,7 +2027,6 @@ async def deleteProfile(self, msg, args): return - if self.names[msg_mention].sprites or self.names[msg_mention].portraits: if msg_mention == "<@!{0}>".format(msg.author.id): # find a proper anonymous name to transfer to @@ -2344,8 +2148,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 @@ -2415,20 +2219,8 @@ async def help(self, msg, args, permission_level: PermissionLevel): elif permission_level == PermissionLevel.STAFF: return_msg = "**Approver Commands**\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}clonesprite` - Copies the sprites for two Pokemon/formes\n" \ f"`{prefix}cloneportrait` - Copies the portraits 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}showcase` - Showcases a sprite or portrait to social media channels\n" @@ -2486,36 +2278,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): f"`{prefix}portraitbounty Diancie Mega Shiny 1`" else: return_msg = MESSAGE_BOUNTIES_DISABLED - 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 == "clonesprite": return_msg = "**Command Help**\n" \ f"`{prefix}clonesprite [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ @@ -2544,186 +2306,6 @@ async def help(self, msg, args, permission_level: PermissionLevel): f"`{prefix}cloneportrait Zoroark Alternate -> Zoroark`\n" \ f"`{prefix}cloneportrait Missingno_ Kleavor -> Kleavor`\n" \ f"`{prefix}cloneportrait 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" \ - "`Files` - A comma-separated list of the files to change, or \" to represent the last applied credit\n" \ - "**Examples**\n" \ - f"`{prefix}addspritecredit @Audino Unown Shiny \"`\n" \ - f"`{prefix}addspritecredit <@!117780585635643396> Unown Shiny \"`\n" \ - f"`{prefix}addspritecredit @Audino Unown Shiny Idle,Rotate,Sleep`\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" \ - "`Files` - A comma-separated list of the files to change, or \" to represent the last applied credit\n" \ - "**Examples**\n" \ - f"`{prefix}addportraitcredit @Audino Unown Shiny \"`\n" \ - f"`{prefix}addportraitcredit <@!117780585635643396> Unown Shiny \"`\n" \ - f"`{prefix}addportraitcredit @Audino Unown Shiny Normal,Happy,Angry`\n" \ - f"`{prefix}addportraitcredit POWERCRISTAL Calyrex \"`\n" \ - f"`{prefix}addportraitcredit POWERCRISTAL Calyrex Shiny \"`\n" \ - f"`{prefix}addportraitcredit POWERCRISTAL Jellicent Shiny Female \"`" elif base_arg == "showcase": return_msg = "**Command Help**\n" \ f"`{prefix}showcase [Form Name] [Shiny] [Gender] `\n" \ @@ -2754,8 +2336,8 @@ async def help(self, msg, args, permission_level: PermissionLevel): @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() @@ -2830,51 +2412,15 @@ async def on_message(msg: discord.Message): elif base_arg == "unregister": await sprite_bot.deleteProfile(msg, cmd_args[1:]) # authorized commands - elif base_arg == "movesprite" and authorized: - await sprite_bot.moveSlot(msg, cmd_args[1:], "sprite") - elif base_arg == "moveportrait" and authorized: - await sprite_bot.moveSlot(msg, cmd_args[1:], "portrait") elif base_arg == "clonesprite" and msg.author.id == sprite_bot.config.root: await sprite_bot.cloneSlot(msg, cmd_args[1:], "sprite") elif base_arg == "cloneportrait" and msg.author.id == sprite_bot.config.root: await sprite_bot.cloneSlot(msg, cmd_args[1:], "portrait") - elif base_arg == "replacesprite" and authorized: - await sprite_bot.replaceSlot(msg, cmd_args[1:], "sprite") - elif base_arg == "replaceportrait" and authorized: - await sprite_bot.replaceSlot(msg, cmd_args[1:], "portrait") - elif base_arg == "spritewip" and authorized: - await sprite_bot.completeSlot(msg, cmd_args[1:], "sprite", TrackerUtils.PHASE_INCOMPLETE) - elif base_arg == "portraitwip" and authorized: - await sprite_bot.completeSlot(msg, cmd_args[1:], "portrait", TrackerUtils.PHASE_INCOMPLETE) - elif base_arg == "spriteexists" and authorized: - await sprite_bot.completeSlot(msg, cmd_args[1:], "sprite", TrackerUtils.PHASE_EXISTS) - elif base_arg == "portraitexists" and authorized: - await sprite_bot.completeSlot(msg, cmd_args[1:], "portrait", TrackerUtils.PHASE_EXISTS) - elif base_arg == "spritefilled" and authorized: - await sprite_bot.completeSlot(msg, cmd_args[1:], "sprite", TrackerUtils.PHASE_FULL) - elif base_arg == "portraitfilled" and authorized: - await sprite_bot.completeSlot(msg, cmd_args[1:], "portrait", TrackerUtils.PHASE_FULL) - elif base_arg == "setspritecredit" and authorized: - await sprite_bot.resetCredit(msg, cmd_args[1:], "sprite") - elif base_arg == "setportraitcredit" and authorized: - await sprite_bot.resetCredit(msg, cmd_args[1:], "portrait") - elif base_arg == "addspritecredit" and authorized: - await sprite_bot.addCredit(msg, cmd_args[1:], "sprite") - elif base_arg == "addportraitcredit" and authorized: - await sprite_bot.addCredit(msg, cmd_args[1:], "portrait") elif base_arg == "modreward" and authorized: await sprite_bot.modSpeciesForm(msg, cmd_args[1:]) # root commands elif base_arg == "showcase" and authorized: await sprite_bot.showcase(msg, cmd_args[1:]) - elif base_arg == "unlockportrait" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setLock(msg, cmd_args[1:], "portrait", False) - elif base_arg == "unlocksprite" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setLock(msg, cmd_args[1:], "sprite", False) - elif base_arg == "lockportrait" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setLock(msg, cmd_args[1:], "portrait", True) - elif base_arg == "locksprite" and msg.author.id == sprite_bot.config.root: - await sprite_bot.setLock(msg, cmd_args[1:], "sprite", True) elif base_arg in ["gr", "tr", "checkr"]: pass else: @@ -2892,11 +2438,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/commands/AddNode.py b/commands/AddNode.py index 4955c96..1262229 100644 --- a/commands/AddNode.py +++ b/commands/AddNode.py @@ -41,7 +41,7 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): await msg.channel.send(msg.author.mention + " {0} already exists!".format(species_name)) return - new_id_int = max([int(i) for i in self.tracker.keys()]) + 1 + new_id_int = max([int(i) for i in self.spritebot.tracker.keys()]) + 1 new_idx = "{:04d}".format(new_id_int) self.spritebot.tracker[new_idx] = TrackerUtils.createSpeciesNode(species_name) diff --git a/commands/AddResourceCredit.py b/commands/AddResourceCredit.py new file mode 100644 index 0000000..1a3ad47 --- /dev/null +++ b/commands/AddResourceCredit.py @@ -0,0 +1,93 @@ +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 AddResourceCredit(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): + super().__init__(spritebot) + self.resource_type = resource_type + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return f"add{self.resource_type}credit" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return f"Adds a new author to the credits of the {self.resource_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.resource_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" \ + "`Files` - A comma-separated list of the files to change, or \" to represent the last applied credit\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "@Audino Unown Shiny \"", + "<@!117780585635643396> Unown Shiny \"", + "`{prefix}addspritecredit @Audino Unown Shiny Idle,Rotate,Sleep`", + "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) < 3: + await msg.channel.send(msg.author.mention + " Specify a user ID, file list, 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.resource_type + "_credit"].primary == "": + await msg.channel.send(msg.author.mention + " This command only works on filled {0}.".format(self.resource_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) + + submit_args = "--addauthor" + file_names = args[-1] + + if file_names != "\"": + final_file_names, failed_file_names = self.spritebot.parseFileNames(chosen_node, self.resource_type, file_names) + if len(failed_file_names) > 0: + await msg.channel.send(msg.author.mention + " Could not find the emotion/animations:\n{0}.".format(",".join(failed_file_names))) + return False + + submit_args = submit_args + " --files " + ",".join(final_file_names) + + 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.resource_type, False) + base_file, base_name = SpriteUtils.getLinkData(base_link) + + # stage a post in submissions + await self.spritebot.postStagedSubmission(submit_channel, submit_args, "", full_idx, chosen_node, self.resource_type, author + "/" + wanted_author, + False, None, base_file, base_name, None) \ No newline at end of file diff --git a/commands/DeleteGender.py b/commands/DeleteGender.py index 9bba3b7..e755d93 100644 --- a/commands/DeleteGender.py +++ b/commands/DeleteGender.py @@ -15,7 +15,7 @@ def getCommand(self) -> str: return "deletegender" def getSingleLineHelp(self, server_config: "BotServer") -> str: - return "Removes the female sprite/portrait from the Pokemon" + return "Removes the male/female slot from the Pokemon's sprite/portrait" def getMultiLineHelp(self, server_config: "BotServer") -> str: return f"`{server_config.prefix}deletegender [Pokemon Form]`\n" \ diff --git a/commands/MoveNode.py b/commands/MoveNode.py index ac32d4b..666bc2f 100644 --- a/commands/MoveNode.py +++ b/commands/MoveNode.py @@ -128,10 +128,11 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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 + server_config = self.spritebot.config.servers[str(msg.guild.id)] 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))) + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_to), server_config.prefix)) 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))) + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_from), server_config.prefix)) self.spritebot.saveTracker() self.spritebot.changed = True diff --git a/commands/MoveResource.py b/commands/MoveResource.py new file mode 100644 index 0000000..a3e7b01 --- /dev/null +++ b/commands/MoveResource.py @@ -0,0 +1,117 @@ +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 MoveResource(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): + super().__init__(spritebot) + self.resource_type = resource_type + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return f"move{self.resource_type}" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return f"Swaps the {self.resource_type}s for two Pokemon/formes" + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}move{self.resource_type} [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ + f"Swaps the contents of one {self.resource_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.resource_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.resource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when source {0} is unneeded.".format(self.resource_type)) + return + if not chosen_node_to.__dict__[self.resource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when destination {0} is unneeded.".format(self.resource_type)) + return + + try: + await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, self.resource_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.resource_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.resource_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 + server_config = self.spritebot.config.servers[str(msg.guild.id)] + if not TrackerUtils.isDataPopulated(chosen_node_from): + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_from), server_config.prefix)) + if not TrackerUtils.isDataPopulated(chosen_node_to): + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_to), server_config.prefix)) + + self.spritebot.saveTracker() + self.spritebot.changed = True + + await self.spritebot.gitCommit("Swapped {0} with {1}".format(" ".join(name_seq_from), " ".join(name_seq_to))) + + if not TrackerUtils.reportableCheck(name_seq_from): + #urls = await self.postSocialMedia(full_idx_to, asset_type, "Showcased", self.createCreditBlock(credit_data, None, True)) + #await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) + pass + + if not TrackerUtils.reportableCheck(name_seq_to): + #urls = await self.postSocialMedia(full_idx_to, asset_type, "Showcased", self.createCreditBlock(credit_data, None, True)) + #await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) + pass \ No newline at end of file diff --git a/commands/ReplaceResource.py b/commands/ReplaceResource.py new file mode 100644 index 0000000..14578b0 --- /dev/null +++ b/commands/ReplaceResource.py @@ -0,0 +1,98 @@ +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 ReplaceResource(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): + super().__init__(spritebot) + self.resource_type = resource_type + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return f"replace{self.resource_type}" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return f"Replace the content of one {self.resource_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.resource_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.resource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when source {0} is unneeded.".format(self.resource_type)) + return + if not chosen_node_to.__dict__[self.resource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when destination {0} is unneeded.".format(self.resource_type)) + return + + try: + await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, self.resource_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.resource_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.resource_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 + server_config = self.spritebot.config.servers[str(msg.guild.id)] + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_from), server_config.prefix)) + + 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/SetProfile.py b/commands/SetProfile.py index 52e1314..c99e21b 100644 --- a/commands/SetProfile.py +++ b/commands/SetProfile.py @@ -73,6 +73,7 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): new_credit = TrackerUtils.CreditEntry(args[0], args[1]) else: await msg.channel.send(msg.author.mention + " Invalid amounts of arguments") + return if entry_key in self.spritebot.names: new_credit.sprites = self.spritebot.names[entry_key].sprites diff --git a/commands/SetResourceCompletion.py b/commands/SetResourceCompletion.py new file mode 100644 index 0000000..f774fb5 --- /dev/null +++ b/commands/SetResourceCompletion.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 SetResourceCompletion(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str, completion: int): + super().__init__(spritebot) + self.resource_type = resource_type + #TODO: completion should eventually be replaced by a class or enum once better in-memory resource 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.resource_type + self.getCompletionCommandCode() + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return f"Set the {self.resource_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.resource_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.resource_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.resource_type + "_credit"].primary == "" and self.completion > TrackerUtils.PHASE_INCOMPLETE: + status = TrackerUtils.getStatusEmoji(chosen_node, self.resource_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.resource_type + "_complete"] = self.completion + + status = TrackerUtils.getStatusEmoji(chosen_node, self.resource_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/SetResourceCredit.py b/commands/SetResourceCredit.py new file mode 100644 index 0000000..7797471 --- /dev/null +++ b/commands/SetResourceCredit.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 SetResourceCredit(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): + super().__init__(spritebot) + self.resource_type = resource_type + + def getRequiredPermission(self) -> PermissionLevel: + return PermissionLevel.STAFF + + def getCommand(self) -> str: + return "set{}credit".format(self.resource_type) + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + return "Sets the primary author of the {}".format(self.resource_type) + + def getMultiLineHelp(self, server_config: "BotServer") -> str: + return f"`{server_config.prefix}set{self.resource_type}credit [Form Name] [Shiny] [Gender]`\n" \ + f"Manually sets the primary author of a {self.resource_type} to the specified author. " \ + f"The specified author must already exist in the credits for the {self.resource_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.resource_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.resource_type + "_credit"].primary == "": + await msg.channel.send(msg.author.mention + " No credit found.") + return + gen_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.resource_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.resource_type)) + return + + # make the credit array into the most current author by itself + credit_data = chosen_node.__dict__[self.resource_type + "_credit"] + if credit_data.primary == "CHUNSOFT": + await msg.channel.send(msg.author.mention + " Cannot reset credit for a CHUNSOFT {0}.".format(self.resource_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.resource_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/SetResourceLock.py b/commands/SetResourceLock.py new file mode 100644 index 0000000..22cd8d3 --- /dev/null +++ b/commands/SetResourceLock.py @@ -0,0 +1,87 @@ +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 SetResourceLock(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str, lock: bool): + super().__init__(spritebot) + self.resource_type = resource_type + self.lock = lock + + def getRequiredPermission(self): + return PermissionLevel.ADMIN + + def getCommand(self) -> str: + if self.lock: + return "lock" + self.resource_type + else: + return "unlock" + self.resource_type + + def getEmotionOrActionText(self) -> str: + if self.resource_type == "sprite": + return "action" + else: + return "emotion" + + def getSingleLineHelp(self, server_config: "BotServer") -> str: + + if self.lock: + return f"Mark a {self.resource_type} {self.getEmotionOrActionText()}as locked" + else: + return f"Mark a {self.resource_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.resource_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.resource_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) + + final_file_names, failed_file_names = self.spritebot.parseFileNames(chosen_node, self.resource_type, args[-1]) + + if len(failed_file_names) > 0: + await msg.channel.send(msg.author.mention + " Could not find the emotion/animations:\n{0}.".format(",".join(failed_file_names))) + return + + for file_name in final_file_names: + chosen_node.__dict__[self.resource_type + "_files"][file_name] = self.lock + + status = TrackerUtils.getStatusEmoji(chosen_node, self.resource_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), ",".join(final_file_names), lock_str)) + + self.spritebot.saveTracker() + self.spritebot.changed = True \ No newline at end of file From 04f94c7bdcffeee0c7856f688736bb5ef7fe9501 Mon Sep 17 00:00:00 2001 From: Audino <2676737+audinowho@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:54:06 -0800 Subject: [PATCH 35/35] marius merge content --- BlueSkyUtils.py | 41 +- MastodonUtils.py | 2 +- SpriteBot.py | 663 +++++++++++++----- SpriteUtils.py | 17 +- TrackerUtils.py | 167 +++-- commands/AddNode.py | 24 +- ...essourceCredit.py => AddResourceCredit.py} | 52 +- ...lorRessource.py => AutoRecolorResource.py} | 38 +- commands/DeleteGender.py | 2 +- ...ourceCredit.py => DeleteResourceCredit.py} | 30 +- .../{ListRessource.py => ListResource.py} | 18 +- commands/MoveNode.py | 58 +- .../{MoveRessource.py => MoveResource.py} | 47 +- ...sourceCredit.py => QueryResourceCredit.py} | 28 +- ...sourceStatus.py => QueryResourceStatus.py} | 60 +- ...ReplaceRessource.py => ReplaceResource.py} | 29 +- commands/SetNodeCanon.py | 4 + commands/SetProfile.py | 1 + ...Completion.py => SetResourceCompletion.py} | 24 +- ...essourceCredit.py => SetResourceCredit.py} | 30 +- ...SetRessourceLock.py => SetResourceLock.py} | 38 +- commands/Shutdown.py | 2 +- utils.py | 2 +- 23 files changed, 904 insertions(+), 473 deletions(-) rename commands/{AddRessourceCredit.py => AddResourceCredit.py} (62%) rename commands/{AutoRecolorRessource.py => AutoRecolorResource.py} (82%) rename commands/{DeleteRessourceCredit.py => DeleteResourceCredit.py} (85%) rename commands/{ListRessource.py => ListResource.py} (81%) rename commands/{MoveRessource.py => MoveResource.py} (67%) rename commands/{QueryRessourceCredit.py => QueryResourceCredit.py} (87%) rename commands/{QueryRessourceStatus.py => QueryResourceStatus.py} (74%) rename commands/{ReplaceRessource.py => ReplaceResource.py} (79%) rename commands/{SetRessourceCompletion.py => SetResourceCompletion.py} (81%) rename commands/{SetRessourceCredit.py => SetResourceCredit.py} (75%) rename commands/{SetRessourceLock.py => SetResourceLock.py} (65%) diff --git a/BlueSkyUtils.py b/BlueSkyUtils.py index f030e3d..8688d16 100644 --- a/BlueSkyUtils.py +++ b/BlueSkyUtils.py @@ -48,7 +48,32 @@ def upload_blob(img_data, jwt, mime_type): blob_request = json.loads(blob_request.data) return blob_request["blob"] # type: ignore -def send_post(user, jwt, text, blob, image_alt): + +def send_video_post(user, jwt, text, blob, image_alt): + http = urllib3.PoolManager() + post_record = { + "collection": "app.bsky.feed.post", + "repo": user, + "record": { + "text": text, + "createdAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "embed": { + "$type": "app.bsky.embed.video", + "video": blob, + "alt": image_alt, + }, + }, + } + post_request = http.request( + "POST", + "https://bsky.social/xrpc/com.atproto.repo.createRecord", + body=json.dumps(post_record), + headers={"Content-Type": "application/json", "Authorization": f"Bearer {jwt}"}, + ) + post_request = json.loads(post_request.data) + return post_request + +def send_img_post(user, jwt, text, blob, image_alt): http = urllib3.PoolManager() post_record = { "collection": "app.bsky.feed.post", @@ -74,17 +99,23 @@ def send_post(user, jwt, text, blob, image_alt): async def post_image(api, text, img_title, img_file, asset_type): img_file.seek(0) jwt = get_api_key(api.user, api.password) + status = None + media = None if asset_type == "sprite": - media = upload_blob(img_file, jwt, "image/gif") + media = upload_blob(img_file, jwt, "video/mp4") + await asyncio.sleep(20) + status = send_video_post(api.user, jwt, text, media, img_title) elif asset_type == "portrait": media = upload_blob(img_file, jwt, "image/png") - await asyncio.sleep(20) - status = send_post(api.user, jwt, text, media, img_title) + await asyncio.sleep(20) + status = send_img_post(api.user, jwt, text, media, img_title) + + if "uri" in status: uri = status["uri"] post_id = uri.split("/")[-1] - return "https://bsky.app/profile/{0}/post/{1}".format("pmd-spritebot.bsky.social", post_id) + return "https://bsky.app/profile/{0}/post/{1}".format(api.user, post_id) else: trace_str = traceback.format_exc() raise KeyError("Missing uri in status, media:\n{0}\n{1}\n{2}".format(json.dumps(status), json.dumps(media), diff --git a/MastodonUtils.py b/MastodonUtils.py index b7174ff..2afef53 100644 --- a/MastodonUtils.py +++ b/MastodonUtils.py @@ -28,7 +28,7 @@ async def post_image(api, text, img_title, img_file, asset_type): media = api.media_post(file_name=img_title, mime_type="image/png", media_file=img_file) await asyncio.sleep(20) status = api.status_post(status=text, media_ids=media) - return status["url"] + return status["url"], status["media_attachments"][0]["url"] def post_text(api, orig_post, msg, media): diff --git a/SpriteBot.py b/SpriteBot.py index 195b842..305047e 100644 --- a/SpriteBot.py +++ b/SpriteBot.py @@ -20,29 +20,29 @@ import MastodonUtils import BlueSkyUtils -from commands.QueryRessourceStatus import QueryRessourceStatus -from commands.AutoRecolorRessource import AutoRecolorRessource -from commands.ListRessource import ListRessource -from commands.QueryRessourceCredit import QueryRessourceCredit -from commands.DeleteRessourceCredit import DeleteRessourceCredit +from commands.QueryResourceStatus import QueryResourceStatus +from commands.AutoRecolorResource import AutoRecolorResource +from commands.ListResource import ListResource +from commands.QueryResourceCredit import QueryResourceCredit +from commands.DeleteResourceCredit import DeleteResourceCredit from commands.ListBounties import ListBounties from commands.ClearCache import ClearCache from commands.GetProfile import GetProfile from commands.SetProfile import SetProfile from commands.GetAbsenteeProfiles import GetAbsenteeProfiles from commands.RenameNode import RenameNode -from commands.ReplaceRessource import ReplaceRessource +from commands.ReplaceResource import ReplaceResource from commands.MoveNode import MoveNode -from commands.MoveRessource import MoveRessource -from commands.SetRessourceCredit import SetRessourceCredit -from commands.AddRessourceCredit import AddRessourceCredit +from commands.MoveResource import MoveResource +from commands.SetResourceCredit import SetResourceCredit +from commands.AddResourceCredit import AddResourceCredit 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.SetResourceCompletion import SetResourceCompletion +from commands.SetResourceLock import SetResourceLock from commands.SetNodeCanon import SetNodeCanon from commands.SetNeedNode import SetNeedNode from commands.ForcePush import ForcePush @@ -52,6 +52,8 @@ from Constants import PHASES, PermissionLevel, MESSAGE_BOUNTIES_DISABLED from utils import unpack_optional + +from Constants import PHASES import psutil # Housekeeping for login information @@ -74,8 +76,10 @@ parser.add_argument('--colormod', type=int) parser.add_argument('--colors', type=int) parser.add_argument('--author') +parser.add_argument('--nocredit', nargs='?', const=True, default=False) parser.add_argument('--addauthor', nargs='?', const=True, default=False) parser.add_argument('--deleteauthor', nargs='?', const=True, default=False) +parser.add_argument('--files') class MyClient(discord.Client): async def setup_hook(self): @@ -118,6 +122,7 @@ def __init__(self, main_dict=None): self.points = 0 self.error_ch = 0 self.points_ch = 0 + self.points_user = 0 self.update_ch = 0 self.update_msg = 0 self.use_bounties = False @@ -196,7 +201,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: Dict[str, TrackerUtils.TrackerNode] = { } + 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)) @@ -226,20 +231,20 @@ def __init__(self, in_path, client): # register commands self.commands = [ # everyone - QueryRessourceStatus(self, "portrait", False), - QueryRessourceStatus(self, "portrait", True), - QueryRessourceStatus(self, "sprite", False), - QueryRessourceStatus(self, "sprite", True), - AutoRecolorRessource(self, "portrait"), - AutoRecolorRessource(self, "sprite"), - ListRessource(self, "portrait"), - ListRessource(self, "sprite"), - QueryRessourceCredit(self, "portrait", False), - QueryRessourceCredit(self, "sprite", False), - QueryRessourceCredit(self, "portrait", True), - QueryRessourceCredit(self, "sprite", True), - DeleteRessourceCredit(self, "portrait"), - DeleteRessourceCredit(self, "sprite"), + QueryResourceStatus(self, "portrait", False), + QueryResourceStatus(self, "portrait", True), + QueryResourceStatus(self, "sprite", False), + QueryResourceStatus(self, "sprite", True), + AutoRecolorResource(self, "portrait"), + AutoRecolorResource(self, "sprite"), + ListResource(self, "portrait"), + ListResource(self, "sprite"), + QueryResourceCredit(self, "portrait", False), + QueryResourceCredit(self, "sprite", False), + QueryResourceCredit(self, "portrait", True), + QueryResourceCredit(self, "sprite", True), + DeleteResourceCredit(self, "portrait"), + DeleteResourceCredit(self, "sprite"), GetProfile(self), SetProfile(self, False), GetAbsenteeProfiles(self), @@ -254,29 +259,29 @@ def __init__(self, in_path, client): SetProfile(self, True), TransferProfile(self), RenameNode(self), - ReplaceRessource(self, "portrait"), - ReplaceRessource(self, "sprite"), + ReplaceResource(self, "portrait"), + ReplaceResource(self, "sprite"), MoveNode(self), - MoveRessource(self, "portrait"), - MoveRessource(self, "sprite"), - SetRessourceCredit(self, "portrait"), - SetRessourceCredit(self, "sprite"), - AddRessourceCredit(self, "portrait"), - AddRessourceCredit(self, "sprite"), + MoveResource(self, "portrait"), + MoveResource(self, "sprite"), + SetResourceCredit(self, "portrait"), + SetResourceCredit(self, "sprite"), + AddResourceCredit(self, "portrait"), + AddResourceCredit(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), + SetResourceCompletion(self, "portrait", TrackerUtils.PHASE_INCOMPLETE), + SetResourceCompletion(self, "portrait", TrackerUtils.PHASE_EXISTS), + SetResourceCompletion(self, "portrait", TrackerUtils.PHASE_FULL), + SetResourceCompletion(self, "sprite", TrackerUtils.PHASE_INCOMPLETE), + SetResourceCompletion(self, "sprite", TrackerUtils.PHASE_EXISTS), + SetResourceCompletion(self, "sprite", TrackerUtils.PHASE_FULL), # admin - SetRessourceLock(self, "portrait", True), - SetRessourceLock(self, "portrait", False), - SetRessourceLock(self, "sprite", True), - SetRessourceLock(self, "sprite", False), + SetResourceLock(self, "portrait", True), + SetResourceLock(self, "portrait", False), + SetResourceLock(self, "sprite", True), + SetResourceLock(self, "sprite", False), SetNodeCanon(self, True), SetNodeCanon(self, False), ForcePush(self), @@ -455,6 +460,30 @@ def getBountiesFromDict(self, asset_type, tracker_dict, entries: List[Tuple[int, self.getBountiesFromDict(asset_type, tracker_dict.subgroups[sub_dict], entries, indices + [sub_dict]) + async def isAuthorized(self, user, guild): + + if user.id == self.client.user.id: + return False + if user.id == self.config.root: + return True + guild_id_str = str(guild.id) + + if self.config.servers[guild_id_str].approval == 0: + return False + + approve_role = guild.get_role(self.config.servers[guild_id_str].approval) + + try: + user_member = await guild.fetch_member(user.id) + except discord.NotFound as e: + user_member = None + + if user_member is None: + return False + if approve_role in user_member.roles: + return True + return False + async def getUserPermission(self, user, guild): """Get a user permission level""" if user.id == self.client.user.id: @@ -479,6 +508,13 @@ async def getUserPermission(self, user, guild): return PermissionLevel.STAFF return PermissionLevel.EVERYONE + def remove_self_mention(self, split_args): + for idx in range(len(split_args)): + single_arg = split_args[len(split_args) - 1 - idx] + if single_arg == self.client.user.mention: + del split_args[len(split_args) - 1 - idx] + return split_args + async def generateLink(self, file_data, filename): # file_data is a file-like object to post with # post the file to the admin under a specific filename @@ -635,7 +671,7 @@ async def returnMsgFile(self, msg, thread, msg_body, asset_type, quant_img=None) await msg.delete() - async def stageSubmission(self, msg, full_idx, chosen_node, asset_type, author, recolor, diffs, overcolor): + async def stageSubmission(self, msg, split_args, full_idx, chosen_node, asset_type, author, recolor, diffs, overcolor): try: return_file, return_name = SpriteUtils.getLinkFile(msg.attachments[0].url, asset_type) @@ -654,7 +690,7 @@ async def stageSubmission(self, msg, full_idx, chosen_node, asset_type, author, if recolor: overcolor_img = SpriteUtils.removePalette(overcolor_img) - await self.postStagedSubmission(msg.channel, msg.content.replace('\n', ' '), "", full_idx, chosen_node, asset_type, author, recolor, + await self.postStagedSubmission(msg.channel, split_args, "", full_idx, chosen_node, asset_type, author, recolor, diffs, return_file, return_name, overcolor_img) await msg.delete() @@ -662,17 +698,20 @@ async def stageSubmission(self, msg, full_idx, chosen_node, asset_type, author, async def postStagedSubmission(self, channel, cmd_str, formatted_content, full_idx, chosen_node, asset_type, author, recolor, diffs, return_file, return_name, overcolor_img): - deleting = cmd_str == "--deleteauthor" + deleting = ("--deleteauthor" in cmd_str) + no_credit = ("--nocredit" in cmd_str) title = TrackerUtils.getIdxName(self.tracker, full_idx) return_copy = io.BytesIO() return_copy.write(return_file.read()) return_copy.seek(0) return_file.seek(0) - send_files = [discord.File(return_copy, return_name)] + send_files = [(return_copy, return_name)] - if deleting: - diff_str = "Approvers AND the author in question must approve this. Use \U00002705 to approve." + if no_credit: + diff_str = "The author must approve to confirm the choice to waive credit. Use \U00002705 to approve.\nChanges: {0}".format(", ".join(diffs)) + elif deleting: + diff_str = "Approvers AND the author in question must approve deletion. Use \U00002705 to approve." elif diffs is not None and len(diffs) > 0: diff_str = "Changes: {0}".format(", ".join(diffs)) else: @@ -691,7 +730,7 @@ async def postStagedSubmission(self, channel, cmd_str, formatted_content, full_i preview_file = io.BytesIO() preview_img.save(preview_file, format='PNG') preview_file.seek(0) - send_files.append(discord.File(preview_file, return_name.replace('.zip', '.png'))) + send_files.append((preview_file, return_name.replace('.zip', '.png'))) add_msg += "\nPreview included." if overcolor_img is not None: @@ -705,15 +744,19 @@ async def postStagedSubmission(self, channel, cmd_str, formatted_content, full_i reduced_file = io.BytesIO() 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'))) + send_files.append((reduced_file, return_name.replace('.png', '_reduced.png'))) add_msg += "\nReduced Color Preview included." if chosen_node.__dict__[asset_type + "_credit"].primary != "": if recolor or asset_type == "portrait": orig_link = await self.retrieveLinkMsg(full_idx, chosen_node, asset_type, recolor) add_msg += "\nCurrent Version: {0}".format(orig_link) + + main_files = [] + for file, name in send_files: + main_files.append(discord.File(file, name)) new_msg = await channel.send("{0} {1}\n{2}\n{3}{4}\n{5}".format(author, " ".join(title), cmd_str, diff_str, - thread_link, formatted_content + add_msg), files=send_files) + thread_link, formatted_content + add_msg), files=main_files) pending_dict = chosen_node.__dict__[asset_type+"_pending"] change_status = len(pending_dict) == 0 @@ -724,12 +767,17 @@ async def postStagedSubmission(self, channel, cmd_str, formatted_content, full_i await new_msg.add_reaction('\U0000274C') if review_thread: - await review_thread.send("New post by {0}: {1}".format(author, new_msg.jump_url)) + review_files = [] + for file, name in send_files: + file.seek(0) + review_files.append(discord.File(file, name)) + await review_thread.send("{0} {1}\n{2}\n{3}{4}\n{5}".format(author, " ".join(title), cmd_str, diff_str, + new_msg.jump_url, formatted_content + add_msg), files=review_files) self.changed |= change_status - async def submissionApproved(self, msg, orig_sender, orig_author, approvals): + async def submissionApproved(self, msg, orig_sender, orig_author, approvals, silent): sender_info = orig_sender if orig_author != orig_sender: sender_info = "{0}/{1}".format(orig_sender, orig_author) @@ -745,8 +793,13 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): assert asset_type is not None assert recolor is not None + # TODO: refactor with above code chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) - assert chosen_node is not None + if not chosen_node: + await self.getChatChannel(msg.guild.id).send(orig_sender + " " + "Removed unknown file: {0}".format(file_name)) + await msg.delete() + return + 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) @@ -755,6 +808,8 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): base_idx = None add_author = False delete_author = False + no_credit = False + diffs = [] if len(msg_lines) > 1: try: msg_args = parser.parse_args(msg_lines[1].split()) @@ -762,8 +817,12 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): await msg.delete() await self.getChatChannel(msg.guild.id).send(msg.author.mention + " Invalid arguments used in submission post.\n`{0}`".format(msg.content)) return + if msg_args.nocredit: + no_credit = True if msg_args.addauthor: add_author = True + if msg_args.files: + diffs, failed_file_names = self.parseFileNames(chosen_node, asset_type, msg_args.files) if msg_args.deleteauthor: delete_author = True if msg_args.base: @@ -774,9 +833,11 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): await msg.delete() return - diffs = [] if not add_author and not delete_author and len(msg_lines) > 2: - msg_changes = msg_lines[2] + changes_idx = 2 + if no_credit: + changes_idx += 1 + msg_changes = msg_lines[changes_idx] if msg_changes.startswith("Changes: "): diffs = msg_changes.replace("Changes: ", "").split(", ") elif msg_changes != "No Changes.": @@ -880,7 +941,7 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): new_credit = False break - TrackerUtils.appendCredits(gen_path, orig_author, ",".join(diffs)) + TrackerUtils.appendCredits(gen_path, orig_author, ",".join(diffs), no_credit) # add to universal names list and save if changed if orig_author not in self.names: @@ -894,7 +955,7 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): chosen_node.__dict__[asset_type + "_modified"] = str(datetime.datetime.utcnow()) credit_data = chosen_node.__dict__[asset_type + "_credit"] - if credit_data.primary != orig_author: + if not no_credit and credit_data.primary != orig_author: # only update credit name if the new author is different from the primary credit_entries = TrackerUtils.getCreditEntries(gen_path) if credit_data.primary == "": @@ -1033,9 +1094,9 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): if bounty_points > 0: reward_changes.append(str(bounty_points)) - if len(reward_changes) > 0 and orig_author.startswith("<@!") and self.config.points_ch != 0: + if len(reward_changes) > 0 and orig_author.startswith("<@!") and self.config.points_ch != 0 and self.config.points_user != 0: orig_author_id = orig_author[3:-1] - await self.client.get_channel(self.config.points_ch).send("!gr {0} {1} {2}".format(orig_author_id, "+".join(reward_changes), self.config.servers[str(msg.guild.id)].chat)) + await self.client.get_channel(self.config.points_ch).send("<@!{0}> !gr {1} {2} {3}".format(self.config.points_user, orig_author_id, "+".join(reward_changes), self.config.servers[str(msg.guild.id)].chat)) if not is_shiny: @@ -1098,24 +1159,14 @@ async def submissionApproved(self, msg, orig_sender, orig_author, approvals): await self.postStagedSubmission(msg.channel, cmd_str, content, shiny_idx, shiny_node, asset_type, sender_info, True, auto_diffs, auto_recolor_file, return_name, overcolor_img) - if self.config.mastodon or self.config.bluesky: - status = TrackerUtils.getStatusEmoji(chosen_node, asset_type) - tl_msg = "{5} #{3:03d}: {4}\n{0} {1} by {2}".format(new_revise, - asset_type, - self.createCreditAttribution(orig_author, True), - int(full_idx[0]), new_name_str, status) + name_arr = TrackerUtils.getIdxName(self.tracker, full_idx) + postable = TrackerUtils.reportableCheck(name_arr) + if silent: + postable = False + if postable and (self.config.mastodon or self.config.bluesky): - img_file = SpriteUtils.getSocialMediaImage(new_link, asset_type) - if self.config.mastodon: - try: - await MastodonUtils.post_image(self.tl_api, tl_msg, new_name_str, img_file, asset_type) - except: - await self.sendError("Error sending post!\n{0}".format(traceback.format_exc())) - if self.config.bluesky: - try: - await BlueSkyUtils.post_image(self.bsky_api, tl_msg, new_name_str, img_file, asset_type) - except: - await self.sendError("Error sending post!\n{0}".format(traceback.format_exc())) + await self.postSocialMedia(full_idx, asset_type, new_revise, + self.createCreditAttribution(orig_author, True)) async def submissionDeclined(self, msg, orig_sender, declines): @@ -1126,11 +1177,18 @@ 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 + # TODO: refactor with above code chosen_node = unpack_optional(TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0)) + if not chosen_node: + await self.getChatChannel(msg.guild.id).send(orig_sender + " " + "Removed unknown file: {0}".format(file_name)) + await msg.delete() + return + review_thread = await self.retrieveDiscussion(full_idx, chosen_node, asset_type, msg.guild.id) # change the status of the sprite @@ -1184,6 +1242,7 @@ async def pollSubmission(self, msg): xs = None ws = None ss = None + ms = None remove_users = [] for reaction in msg.reactions: if reaction.emoji == '\u2705': @@ -1194,9 +1253,12 @@ async def pollSubmission(self, msg): ws = reaction elif reaction.emoji == '\u2B50': ss = reaction + elif reaction.emoji == '\U0001F515': + ms = reaction else: async for user in reaction.users(): - if await self.getUserPermission(user, msg.guild).canPerformAction(PermissionLevel.STAFF): + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.canPerformAction(PermissionLevel.STAFF): pass else: remove_users.append((reaction, user)) @@ -1208,18 +1270,21 @@ async def pollSubmission(self, msg): orig_author = sender_data[-1] orig_sender_id = int(orig_sender[3:-1]) args = msg_lines[1] - deleting = args == "--deleteauthor" + deleting = "--deleteauthor" in args + no_credit = "--nocredit" in args auto = False warn = False consent = False + silent = False approve = [] decline = [] if ss: async for user in ss.users(): - if user.id == self.config.root: + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.canPerformAction(PermissionLevel.ADMIN): auto = True approve.append(user.id) else: @@ -1228,17 +1293,20 @@ async def pollSubmission(self, msg): if cks: async for user in cks.users(): user_author_id = "<@!{0}>".format(user.id) - if deleting and user_author_id == orig_author: + if (deleting or no_credit) and user_author_id == orig_author: approve.append(user.id) consent = True - 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)) + else: + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.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.getUserPermission(user, msg.guild)).canPerformAction(PermissionLevel.STAFF): + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.canPerformAction(PermissionLevel.STAFF): warn = True else: remove_users.append((ws, user)) @@ -1246,15 +1314,25 @@ async def pollSubmission(self, msg): if xs: async for user in xs.users(): user_author_id = "<@!{0}>".format(user.id) - if (await self.getUserPermission(user, msg.guild)).canPerformAction(PermissionLevel.STAFF) or user.id == orig_sender_id: + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.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) elif user.id != self.client.user.id: remove_users.append((xs, user)) + if ms: + async for user in ms.users(): + user_perms = await self.getUserPermission(user, msg.guild) + if user_perms.canPerformAction(PermissionLevel.STAFF) or user.id == orig_sender_id: + silent = True + else: + remove_users.append((ms, user)) + 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 @@ -1263,18 +1341,18 @@ async def pollSubmission(self, msg): await self.submissionDeclined(msg, orig_sender, decline) return True elif auto: - await self.submissionApproved(msg, orig_sender, orig_author, approve) + await self.submissionApproved(msg, orig_sender, orig_author, approve, silent) return False elif not warn: if deleting: if len(approve) >= 3 and consent: - await self.submissionApproved(msg, orig_sender, orig_author, approve) + await self.submissionApproved(msg, orig_sender, orig_author, approve, silent) return False elif asset_type == "sprite" and len(approve) >= 3: - await self.submissionApproved(msg, orig_sender, orig_author, approve) + await self.submissionApproved(msg, orig_sender, orig_author, approve, silent) return False elif asset_type == "portrait" and len(approve) >= 2: - await self.submissionApproved(msg, orig_sender, orig_author, approve) + await self.submissionApproved(msg, orig_sender, orig_author, approve, silent) return False chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) @@ -1307,13 +1385,24 @@ async def pollSubmission(self, msg): 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 + mentioned = False + for mention in msg.mentions: + if mention.id == self.client.user.id: + mentioned = True + + if not mentioned: + await msg.delete() + await self.getChatChannel(msg.guild.id).send(msg.author.mention + " Please ping me in your submission. Slash command support coming soon. Your message:\n`{0}`".format(msg.content)) + return False + try: - msg_args = parser.parse_args(msg.content.split()) + split_args = self.remove_self_mention(msg.content.split()) + msg_args = parser.parse_args(split_args) except SystemExit: await msg.delete() await self.getChatChannel(msg.guild.id).send(msg.author.mention + " Invalid arguments used in submission post.\n`{0}`".format(msg.content)) @@ -1332,6 +1421,12 @@ async def pollSubmission(self, msg): await self.getChatChannel(msg.guild.id).send(msg.author.mention + " Cannot base on the same Pokemon.") return + if msg_args.files: + await msg.delete() + await self.getChatChannel(msg.guild.id).send(msg.author.mention + " Cannot specify --files in an upload.") + return + + overcolor = msg_args.overcolor # at this point, we confirm the file name is valid, now check the contents verified, diffs = await self.verifySubmission(msg, full_idx, base_idx, asset_type, recolor, msg_args) @@ -1352,7 +1447,7 @@ async def pollSubmission(self, msg): author = "{0}/{1}".format(author, sanitized_author) - await self.stageSubmission(msg, full_idx, chosen_node, asset_type, author, recolor, diffs, overcolor) + await self.stageSubmission(msg, " ".join(split_args), full_idx, chosen_node, asset_type, author, recolor, diffs, overcolor) return True @@ -1364,6 +1459,8 @@ async def sendInfoPosts(self, channel, posts: List[str], msg_ids, msg_idx): line_len = 0 while line_idx + line_len < len(posts): new_len = len(posts[line_idx + line_len]) + if new_len >= 1950: + raise Exception("Message too large for post {0}!".format(posts[line_idx + line_len][:1000])) if cur_len + new_len < 1950 and line_len < 25: cur_len += new_len line_len += 1 @@ -1572,6 +1669,7 @@ async def retrieveLinkMsg(self, full_idx, chosen_node, asset_type, recolor): self.saveTracker() return new_link + 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) @@ -1582,7 +1680,61 @@ 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 cloneSlot(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 clone to the same location.") + return + + if not chosen_node_to.__dict__[asset_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot clone 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 clone the locked Pokemon specified as source:\n{0}".format(e.message)) + return + + if TrackerUtils.isDataPopulated(chosen_node_to, asset_type == "sprite", asset_type == "portrait", False): + await msg.channel.send(msg.author.mention + " Cannot clone to an occupied destination!") + return + + # clear caches + TrackerUtils.clearCache(chosen_node_from, True) + TrackerUtils.clearCache(chosen_node_to, True) + + TrackerUtils.copyFolderPaths(self.config.path, self.tracker, asset_type, full_idx_from, full_idx_to) + + await msg.channel.send(msg.author.mention + " Copied {0} to {1}.".format(" ".join(name_seq_from), " ".join(name_seq_to))) + self.saveTracker() + self.changed = True + + await self.gitCommit("Copied {0} to {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: @@ -1612,12 +1764,13 @@ async def placeBounty(self, msg, name_args, asset_type): return if self.config.points == 0: - if not (await self.getUserPermission(msg.author, msg.guild)).canPerformAction(PermissionLevel.STAFF): + user_perms = await self.getUserPermission(msg.author, msg.guild) + if not user_perms.canPerformAction(PermissionLevel.STAFF): await msg.channel.send(msg.author.mention + " Not authorized.") return else: channel = self.client.get_channel(self.config.points_ch) - resp = await channel.send("!checkr {0}".format(msg.author.id)) + resp = await channel.send("<@!{0}> !checkr {1}".format(self.config.points_user, msg.author.id)) # check for enough points def check(m): @@ -1635,7 +1788,7 @@ def check(m): if cur_amt < amt: await msg.channel.send(msg.author.mention + " Not enough guild points! You currently have **{0}GP**.".format(cur_amt)) return - resp = await channel.send("!tr {0} {1} {2}".format(msg.author.id, amt, msg.channel.id)) + resp = await channel.send("<@!{0}> !tr {0} {1} {2}".format(self.config.points_user, msg.author.id, amt, msg.channel.id)) try: wait_msg = await client.wait_for('message', check=check, timeout=10.0) @@ -1659,7 +1812,8 @@ def check(m): self.saveTracker() self.changed = True - async def promote(self, msg, name_args): + + async def showcase(self, msg, name_args): if not self.config.mastodon and not self.config.bluesky: await msg.channel.send(msg.author.mention + " Social Media posting is disabled.") @@ -1698,31 +1852,65 @@ async def promote(self, msg, name_args): return credit_data = chosen_node.__dict__[asset_type + "_credit"] + + urls = await self.postSocialMedia(full_idx, asset_type, "Showcased", + self.createCreditBlock(credit_data, None, True), file_name) + + await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) + + + async def postSocialMedia(self, full_idx, asset_type, update_verb, author, file_name = "Idle"): + chosen_node = TrackerUtils.getNodeFromIdx(self.tracker, full_idx, 0) chosen_link = await self.retrieveLinkMsg(full_idx, chosen_node, asset_type, False) + name_arr = TrackerUtils.getIdxName(self.tracker, full_idx) + name_str = " ".join(name_arr) + status = TrackerUtils.getStatusEmoji(chosen_node, asset_type) - tl_msg = "{5} #{3:03d}: {4}\n{0} {1} by {2}".format("Showcased", + tl_msg = "{5} #{3:03d}: {4}\n{0} {1} by {2}".format(update_verb, asset_type, - self.createCreditBlock(credit_data, None, True), - int(full_idx[0]), " ".join(name_seq), status) + author, + int(full_idx[0]), name_str, status) img_file = SpriteUtils.getSocialMediaImage(chosen_link, asset_type, file_name) urls = [] + masto_img = None if self.config.mastodon: try: - url = await MastodonUtils.post_image(self.tl_api, tl_msg, " ".join(name_seq), img_file, asset_type) + url, masto_img = await MastodonUtils.post_image(self.tl_api, tl_msg, name_str, img_file, asset_type) urls.append(url) except: await self.sendError("Error sending post!\n{0}".format(traceback.format_exc())) + if self.config.bluesky: try: - url = await BlueSkyUtils.post_image(self.bsky_api, tl_msg, " ".join(name_seq), img_file, asset_type) + bsky_file = img_file + # a little hack workaround for bsky not supporting gifs: use mastodon's conversion + if asset_type == "sprite" and masto_img is not None: + bsky_file, bsky_name = SpriteUtils.getLinkData(masto_img) + url = await BlueSkyUtils.post_image(self.bsky_api, tl_msg, name_str, bsky_file, asset_type) urls.append(url) except: await self.sendError("Error sending post!\n{0}".format(traceback.format_exc())) - await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) + return urls + + + def parseFileNames(self, chosen_node, asset_type, file_args): + file_names = file_args.split(',') + final_file_names = [] + failed_file_names = [] + for file_name in file_names: + failed = True + for k in chosen_node.__dict__[asset_type + "_files"]: + if file_name.lower() == k.lower(): + final_file_names.append(k) + failed = False + break + if failed: + failed_file_names.append(file_name) + return final_file_names, failed_file_names def createCreditAttribution(self, mention, plainName=False): if plainName: @@ -1762,12 +1950,54 @@ def createCreditBlock(self, credit, base_credit, plainName=False): if attr not in author_arr: author_arr.append(attr) - block = "By: {0}".format(", ".join(author_arr)) + block = ", ".join(author_arr) + if not plainName: + block = "By: {0}".format(block) credit_diff = credit.total - len(author_arr) if credit_diff > 0: 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 deleteProfile(self, msg, args): msg_mention = "<@!{0}>".format(msg.author.id) @@ -1776,7 +2006,8 @@ async def deleteProfile(self, msg, args): "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.getUserPermission(msg.author, msg.guild)).canPerformAction(PermissionLevel.STAFF): + user_perms = await self.getUserPermission(msg.author, msg.guild) + if not user_perms.canPerformAction(PermissionLevel.STAFF): await msg.channel.send(msg.author.mention + " Not authorized to delete registration.") return msg_mention = self.getFormattedCredit(args[0]) @@ -1795,7 +2026,6 @@ async def deleteProfile(self, msg, args): return - if self.names[msg_mention].sprites or self.names[msg_mention].portraits: if msg_mention == "<@!{0}>".format(msg.author.id): # find a proper anonymous name to transfer to @@ -1942,12 +2172,16 @@ async def modSpeciesForm(self, msg, args): species_dict = self.tracker[species_idx] if len(args) == 1: - species_dict.modreward = not species_dict.modreward + new_modreward = not species_dict.modreward + species_dict.modreward = new_modreward + + form_dict = species_dict.subgroups['0000'] + TrackerUtils.setNodeModReward(form_dict, new_modreward, True) if species_dict.modreward: - await msg.channel.send(msg.author.mention + " #{0:03d}: {1}'s rewards will be decided by approvers.".format(int(species_idx), species_name)) + await msg.channel.send(msg.author.mention + " #{0:03d}: {1}'s rewards will be decided by approvers. (Including shiny and gender slots)".format(int(species_idx), species_name)) else: - await msg.channel.send(msg.author.mention + " #{0:03d}: {1}'s rewards will be given automatically.".format(int(species_idx), species_name)) + await msg.channel.send(msg.author.mention + " #{0:03d}: {1}'s rewards will be given automatically. (Including shiny and gender slots)".format(int(species_idx), species_name)) else: form_name = TrackerUtils.sanitizeName(args[1]) @@ -1957,105 +2191,132 @@ async def modSpeciesForm(self, msg, args): return form_dict = species_dict.subgroups[form_idx] - form_dict.modreward = not form_dict.modreward + new_modreward = not form_dict.modreward + TrackerUtils.setNodeModReward(form_dict, new_modreward, True) if form_dict.modreward: - await msg.channel.send(msg.author.mention + " #{0:03d}: {1} {2}'s rewards will be decided by approvers.".format(int(species_idx), species_name, form_name)) + await msg.channel.send(msg.author.mention + " #{0:03d}: {1} {2}'s rewards will be decided by approvers. (Including shiny and gender slots)".format(int(species_idx), species_name, form_name)) else: - await msg.channel.send(msg.author.mention + " #{0:03d}: {1} {2}'s rewards will be given automatically.".format(int(species_idx), species_name, form_name)) + await msg.channel.send(msg.author.mention + " #{0:03d}: {1} {2}'s rewards will be given automatically. (Including shiny and gender slots)".format(int(species_idx), species_name, form_name)) 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 help(self, msg, args, permission_level: PermissionLevel): + list_commands = len(args) == 0 server_config = self.config.servers[str(msg.guild.id)] prefix = server_config.prefix use_bounties = self.config.use_bounties 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" + 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" - + f"`{prefix}clonesprite` - Copies the sprites for two Pokemon/formes\n" \ + f"`{prefix}cloneportrait` - Copies the portraits for two Pokemon/formes\n" \ + f"`{prefix}modreward` - Toggles whether a sprite/portrait will have a custom reward\n" \ + f"`{prefix}showcase` - Showcases a sprite or portrait to social media channels\n" + for command in self.commands: 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." + return_msg += f"Type `{prefix}help` with the name of a command to learn more about it." else: base_arg = args[0] return_msg = None for command in self.commands: if command.getCommand() == base_arg: return_msg = "**Command Help**\n" \ - + command.getMultiLineHelp(server_config) + + command.getMultiLineHelp(server_config) if return_msg != None: pass elif base_arg == "spritebounty": if use_bounties: return_msg = "**Command Help**\n" \ - f"`{prefix}spritebounty [Form Name] [Shiny] [Gender] `\n" \ - "Places a bounty on a missing or incomplete sprite, using your Guild Points.\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, for those with gender differences\n" \ - "`Points` - The number of guild points you wish to donate\n" \ - "**Examples**\n" \ - f"`{prefix}spritebounty Meowstic 1`\n" \ - f"`{prefix}spritebounty Meowstic 5`\n" \ - f"`{prefix}spritebounty Meowstic Shiny 1`\n" \ - f"`{prefix}spritebounty Meowstic Female 1`\n" \ - f"`{prefix}spritebounty Meowstic Shiny Female 1`\n" \ - f"`{prefix}spritebounty Diancie Mega 1`\n" \ - f"`{prefix}spritebounty Diancie Mega Shiny 1`" + f"`{prefix}spritebounty [Form Name] [Shiny] [Gender] `\n" \ + "Places a bounty on a missing or incomplete sprite, using your Guild Points.\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, for those with gender differences\n" \ + "`Points` - The number of guild points you wish to donate\n" \ + "**Examples**\n" \ + f"`{prefix}spritebounty Meowstic 1`\n" \ + f"`{prefix}spritebounty Meowstic 5`\n" \ + f"`{prefix}spritebounty Meowstic Shiny 1`\n" \ + f"`{prefix}spritebounty Meowstic Female 1`\n" \ + f"`{prefix}spritebounty Meowstic Shiny Female 1`\n" \ + f"`{prefix}spritebounty Diancie Mega 1`\n" \ + f"`{prefix}spritebounty Diancie Mega Shiny 1`" else: return_msg = MESSAGE_BOUNTIES_DISABLED elif base_arg == "portraitbounty": if use_bounties: return_msg = "**Command Help**\n" \ - f"`{prefix}portraitbounty [Form Name] [Shiny] [Gender] `\n" \ - "Places a bounty on a missing or incomplete portrait, using your Guild Points.\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 portrait or not\n" \ - "`Gender` - [Optional] Specifies the gender of the Pokemon, for those with gender differences\n" \ - "`Points` - The number of guild points you wish to donate\n" \ - "**Examples**\n" \ - f"`{prefix}portraitbounty Meowstic 1`\n" \ - f"`{prefix}portraitbounty Meowstic 5`\n" \ - f"`{prefix}portraitbounty Meowstic Shiny 1`\n" \ - f"`{prefix}portraitbounty Meowstic Female 1`\n" \ - f"`{prefix}portraitbounty Meowstic Shiny Female 1`\n" \ - f"`{prefix}portraitbounty Diancie Mega 1`\n" \ - f"`{prefix}portraitbounty Diancie Mega Shiny 1`" + f"`{prefix}portraitbounty [Form Name] [Shiny] [Gender] `\n" \ + "Places a bounty on a missing or incomplete portrait, using your Guild Points.\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 portrait or not\n" \ + "`Gender` - [Optional] Specifies the gender of the Pokemon, for those with gender differences\n" \ + "`Points` - The number of guild points you wish to donate\n" \ + "**Examples**\n" \ + f"`{prefix}portraitbounty Meowstic 1`\n" \ + f"`{prefix}portraitbounty Meowstic 5`\n" \ + f"`{prefix}portraitbounty Meowstic Shiny 1`\n" \ + f"`{prefix}portraitbounty Meowstic Female 1`\n" \ + f"`{prefix}portraitbounty Meowstic Shiny Female 1`\n" \ + f"`{prefix}portraitbounty Diancie Mega 1`\n" \ + f"`{prefix}portraitbounty Diancie Mega Shiny 1`" else: return_msg = MESSAGE_BOUNTIES_DISABLED + elif base_arg == "clonesprite": + return_msg = "**Command Help**\n" \ + f"`{prefix}clonesprite [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ + "Clones the contents of one sprite to another. " \ + "Good for copying alternates.\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}clonesprite Escavalier -> Accelgor`\n" \ + f"`{prefix}clonesprite Zoroark Alternate -> Zoroark`\n" \ + f"`{prefix}clonesprite Missingno_ Kleavor -> Kleavor`\n" \ + f"`{prefix}clonesprite Minior Blue -> Minior Indigo`" + elif base_arg == "cloneportrait": + return_msg = "**Command Help**\n" \ + f"`{prefix}cloneportrait [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ + "Clones the contents of one portrait to another. " \ + "Good for copying alternates.\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}cloneportrait Escavalier -> Accelgor`\n" \ + f"`{prefix}cloneportrait Zoroark Alternate -> Zoroark`\n" \ + f"`{prefix}cloneportrait Missingno_ Kleavor -> Kleavor`\n" \ + f"`{prefix}cloneportrait Minior Blue -> Minior Indigo`" + elif base_arg == "showcase": + return_msg = "**Command Help**\n" \ + f"`{prefix}showcase [Form Name] [Shiny] [Gender] `\n" \ + "Showcases the sprite or portrait to social media.\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" \ + "`Anim` - The animation to showcase. This will cause the sprite to be shown instead of the portrait.\n" \ + "**Examples**\n" \ + f"`{prefix}showcase Lucario Mega`\n" \ + f"`{prefix}showcase Calyrex Sleep`" elif base_arg == "modreward": return_msg = "**Command Help**\n" \ f"`{prefix}modreward [Form Name]`\n" \ @@ -2070,6 +2331,7 @@ async def help(self, msg, args, permission_level: Optional[PermissionLevel]): 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') @@ -2092,11 +2354,20 @@ async def on_message(msg: discord.Message): return content = msg.content + + mentioned = False + for mention in msg.mentions: + if mention.id == sprite_bot.client.user.id: + mentioned = True + + cmd_args = content.split()[1:] + # only respond to the proper author - if msg.author.id == sprite_bot.config.root and content.startswith("!init"): - args = content[len("!"):].split(' ') - await sprite_bot.initServer(msg, args[1:]) - return + if msg.author.id == sprite_bot.config.root and mentioned and len(cmd_args) > 0: + base_arg = cmd_args[0].lower() + if base_arg == "init": + await sprite_bot.initServer(msg, cmd_args[1:]) + return # only respond to the proper guilds guild_id_str = str(msg.guild.id) @@ -2104,48 +2375,55 @@ async def on_message(msg: discord.Message): return server = sprite_bot.config.servers[guild_id_str] - prefix = server.prefix if msg.channel.id == server.chat: - if not content.startswith(prefix): + prefix = server.prefix + if content.startswith('!'): + await msg.channel.send(msg.author.mention + " Bot commands no longer use the `!` prefix. Instead, mention me at the beginning of the message.") + return + + base_arg = cmd_args[0].lower() + + if not mentioned: return - args = content[len(prefix):].split() 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(): #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:]) + await command.executeCommand(msg, cmd_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())) + await msg.channel.send("{} Not authorized (Permission level `{}` needed.)".format(msg.author.mention, command.getRequiredPermission().displayname())) return if base_arg == "help": - await sprite_bot.help(msg, args[1:], None) - # legacy link to help staff + await sprite_bot.help(msg, cmd_args[1:], PermissionLevel.EVERYONE) elif base_arg == "staffhelp": - await sprite_bot.help(msg, args[1:], PermissionLevel.STAFF) + await sprite_bot.help(msg, cmd_args[1:], PermissionLevel.STAFF) # primary commands elif base_arg == "spritebounty": - await sprite_bot.placeBounty(msg, args[1:], "sprite") + await sprite_bot.placeBounty(msg, cmd_args[1:], "sprite") elif base_arg == "portraitbounty": - await sprite_bot.placeBounty(msg, args[1:], "portrait") + await sprite_bot.placeBounty(msg, cmd_args[1:], "portrait") elif base_arg == "unregister": - await sprite_bot.deleteProfile(msg, args[1:]) + await sprite_bot.deleteProfile(msg, cmd_args[1:]) # authorized commands + elif base_arg == "clonesprite" and msg.author.id == sprite_bot.config.root: + await sprite_bot.cloneSlot(msg, cmd_args[1:], "sprite") + elif base_arg == "cloneportrait" and msg.author.id == sprite_bot.config.root: + await sprite_bot.cloneSlot(msg, cmd_args[1:], "portrait") elif base_arg == "modreward" and authorized: - await sprite_bot.modSpeciesForm(msg, args[1:]) + await sprite_bot.modSpeciesForm(msg, cmd_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 == "showcase" and authorized: + await sprite_bot.showcase(msg, cmd_args[1:]) elif base_arg in ["gr", "tr", "checkr"]: pass else: - await msg.channel.send(msg.author.mention + " Unknown Command.") + await msg.channel.send(msg.author.mention + " Unknown Command. Run \"" + sprite_bot.client.user.mention + " help\" for commands.") elif msg.channel.id == sprite_bot.config.servers[guild_id_str].submit: changed_tracker = await sprite_bot.pollSubmission(msg) @@ -2220,12 +2498,13 @@ async def periodic_update_status(): try: # info updates every 1 hour - if updates % 360 == 360: + if updates % 360 == 0: sprite_bot.writeLog("Performing Post Update") if sprite_bot.changed or updates == 0: sprite_bot.changed = False for server_id in sprite_bot.config.servers: await sprite_bot.updatePost(sprite_bot.config.servers[server_id]) + sprite_bot.writeLog("Post Updated for {0}".format(server_id)) sprite_bot.writeLog("Post Update Complete") except Exception as e: diff --git a/SpriteUtils.py b/SpriteUtils.py index 4279d9e..6b6af9e 100644 --- a/SpriteUtils.py +++ b/SpriteUtils.py @@ -230,10 +230,10 @@ def getLinkData(url): req = urllib.request.Request(url, None, RETRIEVE_HEADERS) with urllib.request.urlopen(req) as response: - zip_data = BytesIO() - zip_data.write(response.read()) - zip_data.seek(0) - return zip_data, file + file_data = BytesIO() + file_data.write(response.read()) + file_data.seek(0) + return file_data, file def getLinkImg(url): clean_url = sanitizeLink(url) @@ -916,6 +916,9 @@ def verifySpriteLock(dict, chosen_path, precolor_zip, wan_zip, recolor): cmp_zip = precolor_zip wan_zip = removePalette(wan_zip) + if precolor_zip is None: + raise Exception("The file was submitted in recolor format, but no base file was supplied.") + with zipfile.ZipFile(precolor_zip, 'r') as opened_zip: frames, frame_mapping = getFramesAndMappings(opened_zip, True) frame_size = getFrameSizeFromFrames(frames) @@ -1170,6 +1173,12 @@ def verifyPortraitLock(dict, chosen_path, img, recolor): if recolor: img = removePalette(img) + if img.size[0] % Constants.PORTRAIT_SIZE != 0 or img.size[1] % Constants.PORTRAIT_SIZE != 0: + if recolor: + raise SpriteVerifyError("After palette bar removal, portrait has an invalid size of {0}, Not divisble by {1}x{1}".format(str(img.size), Constants.PORTRAIT_SIZE)) + else: + raise SpriteVerifyError("Portrait has an invalid size of {0}, Not divisble by {1}x{1}".format(str(img.size), Constants.PORTRAIT_SIZE)) + in_data = img.getdata() changed_files = [] for xx in range(Constants.PORTRAIT_TILE_X): diff --git a/TrackerUtils.py b/TrackerUtils.py index 1530f05..4f6406c 100644 --- a/TrackerUtils.py +++ b/TrackerUtils.py @@ -76,30 +76,37 @@ def hasExistingCredits(cur_credits, orig_author, diff): def getFileCredits(path): id_list = [] - if os.path.exists(os.path.join(path, Constants.CREDIT_TXT)): - with open(os.path.join(path, Constants.CREDIT_TXT), 'r', encoding='utf-8') as txt: + credit_path = os.path.join(path, Constants.CREDIT_TXT) + if os.path.exists(credit_path): + with open(credit_path, '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])) + if len(credit) >= 5: + id_list.append(CreditEvent(credit[0], credit[1], credit[2], credit[3], credit[4])) + else: + raise BaseException("Invalid credit line “{}” at {}".format(line, credit_path)) return id_list -def appendCredits(path, id, diff): +def appendCredits(path, id, diff, is_old): if diff == '': diff = '"' + status = "CUR" + if is_old: + status = "OLD" with open(os.path.join(path, Constants.CREDIT_TXT), 'a+', encoding='utf-8') as txt: - txt.write("{0}\t{1}\tCUR\t{2}\t{3}\n".format(str(datetime.datetime.utcnow()), id, CURRENT_LICENSE, diff)) + txt.write("{0}\t{1}\t{2}\t{3}\t{4}\n".format(str(datetime.datetime.utcnow()), id, status, CURRENT_LICENSE, diff)) def mergeCredits(path_from, path_to): id_list = [] with open(path_to, 'r', encoding='utf-8') as txt: for line in txt: - splited = line.strip().split('\t') - id_list.append(CreditEvent(splited[0], splited[1], splited[2], splited[3], splited[4])) + credit = line.strip().split('\t') + id_list.append(CreditEvent(credit[0], credit[1], credit[2], credit[3], credit[4])) with open(path_from, 'r', encoding='utf-8') as txt: for line in txt: - splited = line.strip().split('\t') - id_list.append(CreditEvent(splited[0], splited[1], splited[2], splited[3], splited[4])) + credit = line.strip().split('\t') + id_list.append(CreditEvent(credit[0], credit[1], credit[2], credit[3], credit[4])) id_list = sorted(id_list, key=lambda x: x.datetime) @@ -301,7 +308,7 @@ def getCurrentCompletion(orig_dict, dict, prefix): def updateFiles(dict, species_path, prefix): - file_list = [] + file_list = {} if prefix == "sprite": if os.path.exists(os.path.join(species_path, Constants.MULTI_SHEET_XML)): tree = ET.parse(os.path.join(species_path, Constants.MULTI_SHEET_XML)) @@ -309,17 +316,28 @@ def updateFiles(dict, species_path, prefix): anims_node = root.find('Anims') for anim_node in anims_node.iter('Anim'): # type: ignore name = anim_node.find('Name').text # type: ignore - file_list.append(name) + file_list[name] = True else: for inFile in os.listdir(species_path): if inFile.endswith(".png"): file, _ = os.path.splitext(inFile) - file_list.append(file) + file_list[file] = True for file in file_list: if file not in dict.__dict__[prefix + "_files"]: dict.__dict__[prefix + "_files"][file] = False + to_remove = [] + for file in dict.__dict__[prefix + "_files"]: + if file not in file_list: + to_remove.append(file) + + for file in to_remove: + if dict.__dict__[prefix + "_files"][file]: + print("Locked file no longer exists: {0} {1}".format(species_path, file)) + else: + del dict.__dict__[prefix + "_files"][file] + def updateCreditFromEntries(credit_data, credit_entries): # updates just the total count and the secondary # the primary author is user-defined @@ -381,15 +399,16 @@ def fileSystemToJson(dict, species_path, prefix, tier): if updated: dict.__dict__[prefix + "_link"] = "" -def isDataPopulated(sub_dict): - if sub_dict.sprite_credit.primary != "": +def isDataPopulated(sub_dict, check_sprite = True, check_portrait = True, recursive = True): + if check_sprite and sub_dict.sprite_credit.primary != "": return True - if sub_dict.portrait_credit.primary != "": + if check_portrait and sub_dict.portrait_credit.primary != "": return True - for sub_idx in sub_dict.subgroups: - if isDataPopulated(sub_dict.subgroups[sub_idx]): - return True + if recursive: + for sub_idx in sub_dict.subgroups: + if isDataPopulated(sub_dict.subgroups[sub_idx], check_sprite, check_portrait, recursive): + return True return False def deleteData(tracker_dict, portrait_path, sprite_path, idx): @@ -653,7 +672,7 @@ def clearSubmissions(node): def updateCreditCompilation(name_path, credit_dict): with open(name_path, 'w+', encoding='utf-8') as txt: - txt.write("All custom graphics not originating from official PMD games are licensed under Attribution-NonCommercial 4.0 International http://creativecommons.org/licenses/by/4.0/.\n") + txt.write("All custom graphics not originating from official PMD games are licensed under Attribution-NonCommercial 4.0 International http://creativecommons.org/licenses/by-nc/4.0/.\n") txt.write("All graphics referred to in this file can be found in http://sprites.pmdcollab.org/\n\n") for handle in credit_dict: if len(credit_dict[handle].sprite) > 0 or len(credit_dict[handle].portrait) > 0: @@ -778,7 +797,7 @@ def renameFileCredits(species_path, old_name, new_name): entry[1] = new_name with open(fullPath, 'w', encoding='utf-8') as txt: for entry in id_list: - txt.write(entry[0] + "\t" + entry[1] + "\t" + entry[2] + "\t" + entry[3] + "\n") + txt.write(entry[0] + "\t" + entry[1] + "\t" + entry[2] + "\t" + entry[3] + "\t" + entry[4] + "\n") def getDirFromIdx(base_path, asset_type, full_idx): full_arr = [base_path, asset_type] + full_idx @@ -798,6 +817,13 @@ def moveNodeFiles(dir_from, dir_to, merge_credit, is_dir): elif os.path.isdir(full_base_path) == is_dir: shutil.move(full_base_path, os.path.join(dir_to, file)) +def copyNodeFiles(dir_from, dir_to, is_dir): + cur_files = os.listdir(dir_from) + for file in cur_files: + full_base_path = os.path.join(dir_from, file) + if os.path.isdir(full_base_path) == is_dir: + shutil.copy(full_base_path, os.path.join(dir_to, file)) + def deleteNodeFiles(dir_to, include_credit): cur_files = os.listdir(dir_to) for file in cur_files: @@ -820,6 +846,15 @@ def clearCache(chosen_node, recursive): for subgroup in chosen_node.subgroups: clearCache(chosen_node.subgroups[subgroup], recursive) +def setNodeModReward(chosen_node, modreward, recursive): + if chosen_node.name != "": + chosen_node.modreward = modreward + + if recursive: + for subgroup in chosen_node.subgroups: + setNodeModReward(chosen_node.subgroups[subgroup], modreward, recursive) + + def swapNodeMiscFeatures(node_from, node_to): for key in node_from.__dict__: if key.startswith("sprite"): @@ -840,6 +875,34 @@ def swapNodeAssetFeatures(node_from, node_to, asset_type): node_to.__dict__[key] = node_from.__dict__[key] node_from.__dict__[key] = tmp +def copyNodeAssetFeatures(node_from, node_to, asset_type): + for key in node_from.__dict__: + if key.startswith(asset_type): + node_to.__dict__[key] = node_from.__dict__[key] + +def replaceNodeAssetFeatures(node_from, node_to, asset_type): + node_new = initSubNode("", False) + for key in node_from.__dict__: + if key.startswith(asset_type): + if key == asset_type + "_files": + # pass in keys and set to false, only if new keys are introduced + for file_key in node_from.__dict__[key]: + if file_key not in node_to.__dict__[key]: + node_to.__dict__[key][file_key] = False + elif key == asset_type + "_talk": + # do not overwrite + pass + elif key == asset_type + "_bounty": + for status_key in node_from.__dict__[key]: + if status_key not in node_to.__dict__[key]: + node_to.__dict__[key][status_key] = 0 + node_to.__dict__[key][status_key] = node_to.__dict__[key][status_key] + node_from.__dict__[key][status_key] + elif key == "canon" or key == "modreward": + # do not overwrite canon + pass + else: + node_to.__dict__[key] = node_from.__dict__[key] + node_from.__dict__[key] = node_new.__dict__[key] def swapFolderPaths(base_path, tracker, asset_type, full_idx_from, full_idx_to): @@ -872,27 +935,24 @@ def swapFolderPaths(base_path, tracker, asset_type, full_idx_from, full_idx_to): moveNodeFiles(gen_path_tmp, gen_path_to, False, False) shutil.rmtree(gen_path_tmp) +def copyFolderPaths(base_path, tracker, asset_type, full_idx_from, full_idx_to): + + # swap the nodes in tracker, don't do it recursively + chosen_node_from = getNodeFromIdx(tracker, full_idx_from, 0) + chosen_node_to = getNodeFromIdx(tracker, full_idx_to, 0) + + copyNodeAssetFeatures(chosen_node_from, chosen_node_to, asset_type) + + # prepare to copy textures + gen_path_from = getDirFromIdx(base_path, asset_type, full_idx_from) + gen_path_to = getDirFromIdx(base_path, asset_type, full_idx_to) + + if not os.path.exists(gen_path_to): + os.makedirs(gen_path_to, exist_ok=True) + + # copy the folders + copyNodeFiles(gen_path_from, gen_path_to, False) -def replaceNodeAssetFeatures(node_from, node_to, asset_type): - node_new = initSubNode("", False) - for key in node_from.__dict__: - if key.startswith(asset_type): - if key == asset_type + "_files": - # pass in keys and set to false, only if new keys are introduced - for file_key in node_from.__dict__[key]: - if file_key not in node_to.__dict__[key]: - node_to.__dict__[key][file_key] = False - elif key == asset_type + "_talk": - # do not overwrite - pass - elif key == asset_type + "_bounty": - for status_key in node_from.__dict__[key]: - if status_key not in node_to.__dict__[key]: - node_to.__dict__[key][status_key] = 0 - node_to.__dict__[key][status_key] = node_to.__dict__[key][status_key] + node_from.__dict__[key][status_key] - else: - node_to.__dict__[key] = node_from.__dict__[key] - node_from.__dict__[key] = node_new.__dict__[key] def replaceFolderPaths(base_path, tracker, asset_type, full_idx_from, full_idx_to): @@ -991,6 +1051,33 @@ def setCanon(dict, canon): for subgroup in dict.subgroups: setCanon(dict.subgroups[subgroup], canon) +def canonCheck(species_name, form_name): + if species_name == "Missingno_": + return False + if re.search(r"(_|\b)Beta\d*(_|\b)", form_name): + return False + if re.search(r"(_|\b)Skytemple\d*(_|\b)", form_name): + return False + if re.search(r"(_|\b)Cutscene\d*(_|\b)", form_name): + return False + if re.search(r"(_|\b)Alternate\d*(_|\b)", form_name): + return False + if re.search(r"(_|\b)Altcolor\d*(_|\b)", form_name): + return False + if re.search(r"(_|\b)Temp\d*(_|\b)", form_name): + return False + return True + +def reportableCheck(name_arr): + if len(name_arr) < 2: + return True + form_name = name_arr[1] + if re.search(r"(_|\b)Skytemple\d*(_|\b)", form_name): + return False + if re.search(r"(_|\b)Temp\d*(_|\b)", form_name): + return False + return True + """ String operations """ diff --git a/commands/AddNode.py b/commands/AddNode.py index aafbb8b..1262229 100644 --- a/commands/AddNode.py +++ b/commands/AddNode.py @@ -41,11 +41,11 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): await msg.channel.send(msg.author.mention + " {0} already exists!".format(species_name)) return - count = len(self.spritebot.tracker) - new_idx = "{:04d}".format(count) + new_id_int = max([int(i) for i in self.spritebot.tracker.keys()]) + 1 + new_idx = "{:04d}".format(new_id_int) self.spritebot.tracker[new_idx] = TrackerUtils.createSpeciesNode(species_name) - await msg.channel.send(msg.author.mention + " Added #{0:03d}: {1}!".format(count, species_name)) + await msg.channel.send(msg.author.mention + " Added #{0:03d}: {1}!".format(new_id_int, 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)) @@ -63,21 +63,11 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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 + canon = TrackerUtils.canonCheck(species_name, form_name) - count = len(species_dict.subgroups) - new_count = "{:04d}".format(count) - species_dict.subgroups[new_count] = TrackerUtils.createFormNode(form_name, canon) + new_id_int = max([int(i) for i in species_dict.subgroups.keys()]) + 1 + new_idx = "{:04d}".format(new_id_int) + species_dict.subgroups[new_idx] = 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)) diff --git a/commands/AddRessourceCredit.py b/commands/AddResourceCredit.py similarity index 62% rename from commands/AddRessourceCredit.py rename to commands/AddResourceCredit.py index 05821f3..1a3ad47 100644 --- a/commands/AddRessourceCredit.py +++ b/commands/AddResourceCredit.py @@ -8,41 +8,43 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class AddRessourceCredit(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str): +class AddResourceCredit(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type def getRequiredPermission(self) -> PermissionLevel: return PermissionLevel.STAFF def getCommand(self) -> str: - return f"add{self.ressource_type}credit" + return f"add{self.resource_type}credit" def getSingleLineHelp(self, server_config: "BotServer") -> str: - return f"Adds a new author to the credits of the {self.ressource_type}" + return f"Adds a new author to the credits of the {self.resource_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}. " \ + return f"`{server_config.prefix}{self.getCommand()} [Form Name] [Shiny] [Gender] `\n" \ + f"Adds the specified author to the credits of the {self.resource_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" + "`Files` - A comma-separated list of the files to change, or \" to represent the last applied credit\n" \ + + self.generateMultiLineExample(server_config.prefix, [ + "@Audino Unown Shiny \"", + "<@!117780585635643396> Unown Shiny \"", + "`{prefix}addspritecredit @Audino Unown Shiny Idle,Rotate,Sleep`", + "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.") + if len(args) < 3: + await msg.channel.send(msg.author.mention + " Specify a user ID, file list, and Pokemon.") return wanted_author = self.spritebot.getFormattedCredit(args[0]) @@ -54,8 +56,8 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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)) + if chosen_node.__dict__[self.resource_type + "_credit"].primary == "": + await msg.channel.send(msg.author.mention + " This command only works on filled {0}.".format(self.resource_type)) return if wanted_author not in self.spritebot.names: @@ -63,6 +65,18 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): return assert(msg.guild is not None) + + submit_args = "--addauthor" + file_names = args[-1] + + if file_names != "\"": + final_file_names, failed_file_names = self.spritebot.parseFileNames(chosen_node, self.resource_type, file_names) + if len(failed_file_names) > 0: + await msg.channel.send(msg.author.mention + " Could not find the emotion/animations:\n{0}.".format(",".join(failed_file_names))) + return False + + submit_args = submit_args + " --files " + ",".join(final_file_names) + 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.") @@ -71,9 +85,9 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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_link = await self.spritebot.retrieveLinkMsg(full_idx, chosen_node, self.resource_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, + await self.spritebot.postStagedSubmission(submit_channel, submit_args, "", full_idx, chosen_node, self.resource_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/AutoRecolorResource.py similarity index 82% rename from commands/AutoRecolorRessource.py rename to commands/AutoRecolorResource.py index 7508f4a..7c7f78e 100644 --- a/commands/AutoRecolorRessource.py +++ b/commands/AutoRecolorResource.py @@ -9,27 +9,27 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class AutoRecolorRessource(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str): +class AutoRecolorResource(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type def getRequiredPermission(self) -> PermissionLevel: return PermissionLevel.EVERYONE def getCommand(self) -> str: - return f"autocolor{self.ressource_type}" + return f"autocolor{self.resource_type}" def getSingleLineHelp(self, server_config: "BotServer") -> str: - if self.ressource_type == "portrait": + if self.resource_type == "portrait": return "Generates an automatic recolor of the Pokemon's portrait sheet" - elif self.ressource_type == "sprite": + elif self.resource_type == "sprite": return "Generates an automatic recolor of the Pokemon's sprite sheet" else: raise NotImplementedError() def getMultiLineHelp(self, server_config: "BotServer") -> str: - if self.ressource_type == "portrait": + if self.resource_type == "portrait": return f"`{server_config.prefix}autocolor [Form Name] [Gender]`\n" \ "Generates an automatic shiny of a Pokemon's portrait sheet, in recolor form. " \ "Meant to be used as a starting point to assist in manual recoloring. " \ @@ -38,7 +38,7 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: "`Form Name` - [Optional] Form name of the Pokemon\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon, for those with gender differences\n" \ + self.generateMultiLineExample(server_config.prefix, ["Pikachu", "Pikachu Female", "Shaymin Sky"]) - elif self.ressource_type == "sprite": + elif self.resource_type == "sprite": return f"`{server_config.prefix}autocolor [Form Name] [Gender]`\n" \ "Generates an automatic shiny of a Pokemon's sprite sheet, in recolor form. " \ "Meant to be used as a starting point to assist in manual recoloring. " \ @@ -68,27 +68,27 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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 + " Can't recolor a Pokemon that doesn't have a {0}.".format(self.ressource_type)) + if chosen_node.__dict__[self.resource_type + "_credit"].primary == "": + await msg.channel.send(msg.author.mention + " Can't recolor a Pokemon that doesn't have a {0}.".format(self.resource_type)) return shiny_idx = TrackerUtils.createShinyIdx(full_idx, True) shiny_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, shiny_idx, 0) - if shiny_node.__dict__[self.ressource_type + "_credit"].primary == "": - await msg.channel.send(msg.author.mention + " Can't recolor a Pokemon that doesn't have a shiny {0}.".format(self.ressource_type)) + if shiny_node.__dict__[self.resource_type + "_credit"].primary == "": + await msg.channel.send(msg.author.mention + " Can't recolor a Pokemon that doesn't have a shiny {0}.".format(self.resource_type)) return - base_link = await self.spritebot.retrieveLinkMsg(full_idx, chosen_node, self.ressource_type, False) - cur_recolor_file, _ = SpriteUtils.getLinkFile(base_link, self.ressource_type) - base_recolor_link = await self.spritebot.retrieveLinkMsg(full_idx, chosen_node, self.ressource_type, True) + base_link = await self.spritebot.retrieveLinkMsg(full_idx, chosen_node, self.resource_type, False) + cur_recolor_file, _ = SpriteUtils.getLinkFile(base_link, self.resource_type) + base_recolor_link = await self.spritebot.retrieveLinkMsg(full_idx, chosen_node, self.resource_type, True) cur_recolor_img = SpriteUtils.getLinkImg(base_recolor_link) - base_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.ressource_type, full_idx) + base_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.resource_type, full_idx) # auto-generate the shiny recolor image, in file form - shiny_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.ressource_type, shiny_idx) - auto_recolor_img, cmd_str, content = SpriteUtils.autoRecolor(cur_recolor_file, base_path, shiny_path, self.ressource_type) + shiny_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.resource_type, shiny_idx) + auto_recolor_img, cmd_str, content = SpriteUtils.autoRecolor(cur_recolor_file, base_path, shiny_path, self.resource_type) # post it as a staged submission - return_name = "{0}-{1}{2}".format(self.ressource_type + "_recolor", "-".join(shiny_idx), ".png") + return_name = "{0}-{1}{2}".format(self.resource_type + "_recolor", "-".join(shiny_idx), ".png") auto_recolor_file = io.BytesIO() auto_recolor_img.save(auto_recolor_file, format='PNG') diff --git a/commands/DeleteGender.py b/commands/DeleteGender.py index 9bba3b7..e755d93 100644 --- a/commands/DeleteGender.py +++ b/commands/DeleteGender.py @@ -15,7 +15,7 @@ def getCommand(self) -> str: return "deletegender" def getSingleLineHelp(self, server_config: "BotServer") -> str: - return "Removes the female sprite/portrait from the Pokemon" + return "Removes the male/female slot from the Pokemon's sprite/portrait" def getMultiLineHelp(self, server_config: "BotServer") -> str: return f"`{server_config.prefix}deletegender [Pokemon Form]`\n" \ diff --git a/commands/DeleteRessourceCredit.py b/commands/DeleteResourceCredit.py similarity index 85% rename from commands/DeleteRessourceCredit.py rename to commands/DeleteResourceCredit.py index fbea3b1..6ccf8bb 100644 --- a/commands/DeleteRessourceCredit.py +++ b/commands/DeleteResourceCredit.py @@ -8,19 +8,19 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class DeleteRessourceCredit(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str): +class DeleteResourceCredit(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type def getRequiredPermission(self) -> PermissionLevel: return PermissionLevel.EVERYONE def getCommand(self) -> str: - return f"delete{self.ressource_type}credit" + return f"delete{self.resource_type}credit" def getSingleLineHelp(self, server_config: "BotServer") -> str: - return f"Removes an author to the credits of the {self.ressource_type}" + return f"Removes an author to the credits of the {self.resource_type}" def getMultiLineHelp(self, server_config: "BotServer") -> str: example_args = [ @@ -30,23 +30,23 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: "@Audino Calyrex Shiny", "@Audino Jellicent Shiny Female" ] - if self.ressource_type == "portrait": + if self.resource_type == "portrait": return f"`{server_config.prefix}deleteportraitcredit [Form Name] [Shiny] [Gender]`\n" \ "Deletes the specified author from the credits of the portrait. " \ "This makes a post in the submissions channel, asking other approvers to sign off." \ "The post must be approved by the author being removed.\n" \ - "`Author ID` - The discord ID of the author to set as primary\n" \ + "`Author ID` - The discord ID of the author to remove\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, example_args) - elif self.ressource_type == "sprite": + elif self.resource_type == "sprite": return f"`{server_config.prefix}deletespritecredit [Form Name] [Shiny] [Gender]`\n" \ "Deletes the specified author from the credits of the sprite. " \ "This makes a post in the submissions channel, asking other approvers to sign off." \ "The post must be approved by the author being removed.\n" \ - "`Author ID` - The discord ID of the author to set as primary\n" \ + "`Author ID` - The discord ID of the author to remove\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" \ @@ -80,11 +80,11 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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)) + if chosen_node.__dict__[self.resource_type + "_credit"].primary == "": + await msg.channel.send(msg.author.mention + " This command only works on filled {0}.".format(self.resource_type)) return - gen_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.ressource_type, full_idx) + gen_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.resource_type, full_idx) credit_entries = TrackerUtils.getFileCredits(gen_path) has_credit = False latest_credit = False @@ -99,7 +99,7 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): latest_credit = False if not has_credit: - await msg.channel.send(msg.author.mention + " The author must be in the credits list for this {0}.".format(self.ressource_type)) + await msg.channel.send(msg.author.mention + " The author must be in the credits list for this {0}.".format(self.resource_type)) return if latest_credit: @@ -118,9 +118,9 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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_link = await self.spritebot.retrieveLinkMsg(full_idx, chosen_node, self.resource_type, False) base_file, base_name = SpriteUtils.getLinkData(base_link) # stage a post in submissions - await self.spritebot.postStagedSubmission(submit_channel, "--deleteauthor", "", full_idx, chosen_node, self.ressource_type, author + "/" + wanted_author, + await self.spritebot.postStagedSubmission(submit_channel, "--deleteauthor", "", full_idx, chosen_node, self.resource_type, author + "/" + wanted_author, False, None, base_file, base_name, None) \ No newline at end of file diff --git a/commands/ListRessource.py b/commands/ListResource.py similarity index 81% rename from commands/ListRessource.py rename to commands/ListResource.py index fff5ec6..b5504c5 100644 --- a/commands/ListRessource.py +++ b/commands/ListResource.py @@ -7,32 +7,32 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class ListRessource(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str): +class ListResource(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type def getRequiredPermission(self) -> PermissionLevel: return PermissionLevel.EVERYONE def getCommand(self) -> str: - return f"list{self.ressource_type}" + return f"list{self.resource_type}" def getSingleLineHelp(self, server_config: "BotServer") -> str: - if self.ressource_type == "portrait": + if self.resource_type == "portrait": return "List all portraits related to a Pokemon" - elif self.ressource_type == "sprite": + elif self.resource_type == "sprite": return "List all sprites related to a Pokemon" else: raise NotImplementedError() def getMultiLineHelp(self, server_config: "BotServer") -> str: - if self.ressource_type == "portrait": + if self.resource_type == "portrait": return f"`{server_config.prefix}listportrait `\n" \ "List all portraits related to a Pokemon. This includes all forms, gender, and shiny variants.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ + self.generateMultiLineExample(server_config.prefix, ["Pikachu"]) - elif self.ressource_type == "sprite": + elif self.resource_type == "sprite": return f"`{server_config.prefix}listsprite `\n" \ "List all sprites related to a Pokemon. This includes all forms, gender, and shiny variants.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ @@ -56,5 +56,5 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): posts: List[str] = [] over_dict = TrackerUtils.initSubNode("", True) over_dict.subgroups = { full_idx[0] : chosen_node } - self.spritebot.getPostsFromDict(self.ressource_type == 'sprite', self.ressource_type == 'portrait', False, over_dict, posts, []) + self.spritebot.getPostsFromDict(self.resource_type == 'sprite', self.resource_type == 'portrait', False, over_dict, posts, []) msgs_used, changed = await self.spritebot.sendInfoPosts(msg.channel, posts, [], 0) \ No newline at end of file diff --git a/commands/MoveNode.py b/commands/MoveNode.py index adedee4..666bc2f 100644 --- a/commands/MoveNode.py +++ b/commands/MoveNode.py @@ -81,33 +81,38 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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.") + diff_forms_on_same_species = False + if len(full_idx_from) == 2 and len(full_idx_to) == 2 and full_idx_from[0] == full_idx_to[0]: + diff_forms_on_same_species = True + + if not diff_forms_on_same_species: + # 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 - 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.") + + 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) @@ -123,10 +128,11 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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 + server_config = self.spritebot.config.servers[str(msg.guild.id)] 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))) + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_to), server_config.prefix)) 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))) + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_from), server_config.prefix)) self.spritebot.saveTracker() self.spritebot.changed = True diff --git a/commands/MoveRessource.py b/commands/MoveResource.py similarity index 67% rename from commands/MoveRessource.py rename to commands/MoveResource.py index 188bcce..a3e7b01 100644 --- a/commands/MoveRessource.py +++ b/commands/MoveResource.py @@ -8,28 +8,28 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class MoveRessource(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str): +class MoveResource(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type def getRequiredPermission(self) -> PermissionLevel: return PermissionLevel.STAFF def getCommand(self) -> str: - return f"move{self.ressource_type}" + return f"move{self.resource_type}" def getSingleLineHelp(self, server_config: "BotServer") -> str: - return f"Swaps the {self.ressource_type}s for two Pokemon/formes" + return f"Swaps the {self.resource_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. " \ + return f"`{server_config.prefix}move{self.resource_type} [Pokemon Form] [Shiny] [Gender] -> [Pokemon Form 2] [Shiny 2] [Gender 2]`\n" \ + f"Swaps the contents of one {self.resource_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" \ + f"`Shiny` - [Optional] Specifies if you want the shiny {self.resource_type} or not\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ + self.generateMultiLineExample(server_config.prefix, [ "Escavalier -> Accelgor", @@ -67,21 +67,21 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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)) + if not chosen_node_from.__dict__[self.resource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when source {0} is unneeded.".format(self.resource_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)) + if not chosen_node_to.__dict__[self.resource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when destination {0} is unneeded.".format(self.resource_type)) return try: - await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, self.ressource_type) + await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, self.resource_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) + await self.spritebot.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, self.resource_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 @@ -90,17 +90,28 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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) + TrackerUtils.swapFolderPaths(self.spritebot.config.path, self.spritebot.tracker, self.resource_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 + server_config = self.spritebot.config.servers[str(msg.guild.id)] 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))) + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_from), server_config.prefix)) 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))) + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_to), server_config.prefix)) 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 + await self.spritebot.gitCommit("Swapped {0} with {1}".format(" ".join(name_seq_from), " ".join(name_seq_to))) + + if not TrackerUtils.reportableCheck(name_seq_from): + #urls = await self.postSocialMedia(full_idx_to, asset_type, "Showcased", self.createCreditBlock(credit_data, None, True)) + #await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) + pass + + if not TrackerUtils.reportableCheck(name_seq_to): + #urls = await self.postSocialMedia(full_idx_to, asset_type, "Showcased", self.createCreditBlock(credit_data, None, True)) + #await msg.channel.send(msg.author.mention + " {0}".format("\n".join(urls))) + pass \ No newline at end of file diff --git a/commands/QueryRessourceCredit.py b/commands/QueryResourceCredit.py similarity index 87% rename from commands/QueryRessourceCredit.py rename to commands/QueryResourceCredit.py index 5ed8036..9a0e384 100644 --- a/commands/QueryRessourceCredit.py +++ b/commands/QueryResourceCredit.py @@ -8,10 +8,10 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class QueryRessourceCredit(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str, display_history: bool): +class QueryResourceCredit(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str, display_history: bool): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type self.display_history = display_history def getRequiredPermission(self) -> PermissionLevel: @@ -19,20 +19,20 @@ def getRequiredPermission(self) -> PermissionLevel: def getCommand(self) -> str: if self.display_history: - return f"{self.ressource_type}history" + return f"{self.resource_type}history" else: - return f"{self.ressource_type}credit" + return f"{self.resource_type}credit" def getSingleLineHelp(self, server_config: "BotServer") -> str: if self.display_history: - return f"Gets the credit history of the {self.ressource_type}" + return f"Gets the credit history of the {self.resource_type}" else: - return f"Gets the credits of the {self.ressource_type}" + return f"Gets the credits of the {self.resource_type}" def getMultiLineHelp(self, server_config: "BotServer") -> str: sprite_example = ["Pikachu", "Pikachu Shiny", "Pikachu Female", "Pikachu Shiny Female", "Shaymin Sky", "Shaymin Sky Shiny"] portrait_example = ["Wooper", "Wooper Shiny", "Wooper Female", "Wooper Shiny Female", "Shaymin Sky", "Shaymin Sky Shiny"] - if self.display_history and self.ressource_type == "sprite": + if self.display_history and self.resource_type == "sprite": return f"`{server_config.prefix}spritehistory [Form Name] [Shiny] [Gender]`\n" \ "Gets the full credit history for a Pokemon's sprite sheet, including who changed what.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ @@ -40,7 +40,7 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon, for those with gender differences\n" \ + self.generateMultiLineExample(server_config.prefix, sprite_example) - elif self.display_history and self.ressource_type == "portrait": + elif self.display_history and self.resource_type == "portrait": return f"`{server_config.prefix}portraithistory [Form Name] [Shiny] [Gender]`\n" \ "Gets the full credit history for a Pokemon's portraits, including who changed what.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ @@ -48,7 +48,7 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: "`Shiny` - [Optional] Specifies if you want the shiny portrait or not\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon, for those with gender differences\n" \ + self.generateMultiLineExample(server_config.prefix, portrait_example) - elif not self.display_history and self.ressource_type == "sprite": + elif not self.display_history and self.resource_type == "sprite": return f"`{server_config.prefix}spritecredit [Form Name] [Shiny] [Gender]`\n" \ "Gets the full credits for a Pokemon's sprite sheet. Credit them all in your romhacks.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ @@ -56,7 +56,7 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon, for those with gender differences\n" \ + self.generateMultiLineExample(server_config.prefix, sprite_example) - elif not self.display_history and self.ressource_type == "portrait": + elif not self.display_history and self.resource_type == "portrait": return f"`{server_config.prefix}portraitcredit [Form Name] [Shiny] [Gender]`\n" \ "Gets the full credits for a Pokemon's portraits. Credit them all in your romhacks.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ @@ -80,14 +80,14 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) - if chosen_node.__dict__[self.ressource_type + "_credit"].primary == "": + if chosen_node.__dict__[self.resource_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) + gen_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.resource_type, full_idx) response = msg.author.mention + " " - status = TrackerUtils.getStatusEmoji(chosen_node, self.ressource_type) + status = TrackerUtils.getStatusEmoji(chosen_node, self.resource_type) response += "Full credit for {0} #{1:03d}: {2}".format(status, int(full_idx[0]), " ".join(name_seq)) credit_str = "" diff --git a/commands/QueryRessourceStatus.py b/commands/QueryResourceStatus.py similarity index 74% rename from commands/QueryRessourceStatus.py rename to commands/QueryResourceStatus.py index 62a506c..87cb6f9 100644 --- a/commands/QueryRessourceStatus.py +++ b/commands/QueryResourceStatus.py @@ -7,10 +7,10 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class QueryRessourceStatus(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str, is_derivation: bool): +class QueryResourceStatus(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str, is_derivation: bool): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type self.is_derivation = is_derivation def getRequiredPermission(self) -> PermissionLevel: @@ -18,24 +18,24 @@ def getRequiredPermission(self) -> PermissionLevel: def getCommand(self) -> str: if self.is_derivation: - return f"recolor{self.ressource_type}" + return f"recolor{self.resource_type}" else: - return self.ressource_type + return self.resource_type def getSingleLineHelp(self, server_config: "BotServer") -> str: - if self.ressource_type == "portrait" and self.is_derivation: + if self.resource_type == "portrait" and self.is_derivation: return "Get the Pokemon's portrait sheet in a form for easy recoloring" - elif self.ressource_type == "portrait" and not self.is_derivation: + elif self.resource_type == "portrait" and not self.is_derivation: return "Get the Pokemon's portrait sheet" - elif self.ressource_type == "sprite" and self.is_derivation: + elif self.resource_type == "sprite" and self.is_derivation: return "Get the Pokemon's sprite sheet in a form for easy recoloring" - elif self.ressource_type == "sprite" and not self.is_derivation: + elif self.resource_type == "sprite" and not self.is_derivation: return "Get the Pokemon's sprite sheet" else: raise NotImplementedError() def getMultiLineHelp(self, server_config: "BotServer") -> str: - if self.ressource_type == "portrait" and not self.is_derivation: + if self.resource_type == "portrait" and not self.is_derivation: return f"`{server_config.prefix}portrait [Form Name] [Shiny] [Gender]`\n" \ "Gets the portrait sheet for a Pokemon. If there is none, it will return a blank template.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ @@ -43,7 +43,7 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: "`Shiny` - [Optional] Specifies if you want the shiny portrait or not\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon, for those with gender differences\n" \ + self.generateMultiLineExample(server_config.prefix, ["Wooper", "Wooper Shiny", "Wooper Female", "Wooper Shiny Female", "Shaymin Sky", "Shaymin Sky Shiny"]) - elif self.ressource_type == "sprite" and not self.is_derivation: + elif self.resource_type == "sprite" and not self.is_derivation: return f"`{server_config.prefix}sprite [Form Name] [Shiny] [Gender]`\n" \ "Gets the sprite sheet for a Pokemon. If there is none, it will return a blank template.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ @@ -51,14 +51,14 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: "`Shiny` - [Optional] Specifies if you want the shiny sprite or not\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon, for those with gender differences\n" \ + self.generateMultiLineExample(server_config.prefix, ["Pikachu", "Pikachu Shiny", "Pikachu Female", "Pikachu Shiny Female", "Shaymin Sky", "Shaymin Sky Shiny"]) - elif self.ressource_type == "portrait" and self.is_derivation: + elif self.resource_type == "portrait" and self.is_derivation: return f"`{server_config.prefix}recolorportrait [Form Name] [Gender]`\n" \ "Gets the portrait sheet for a Pokemon in a form that is easy to recolor.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ "`Form Name` - [Optional] Form name of the Pokemon\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon, for those with gender differences\n" \ + self.generateMultiLineExample(server_config.prefix, ["Pikachu", "Pikachu Female", "Shaymin Sky"]) - elif self.ressource_type == "sprite" and self.is_derivation: + elif self.resource_type == "sprite" and self.is_derivation: return f"`{server_config.prefix}recolorsprite [Form Name] [Gender]`\n" \ "Gets the sprite sheet for a Pokemon in a form that is easy to recolor.\n" \ "`Pokemon Name` - Name of the Pokemon\n" \ @@ -88,39 +88,39 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): assert chosen_node is not None # post the statuses response = msg.author.mention + " " - status = TrackerUtils.getStatusEmoji(chosen_node, self.ressource_type) + status = TrackerUtils.getStatusEmoji(chosen_node, self.resource_type) response += "{0} #{1:03d}: {2}".format(status, int(full_idx[0]), " ".join(name_seq)) - if chosen_node.__dict__[self.ressource_type + "_required"]: - file_exists = chosen_node.__dict__[self.ressource_type + "_credit"].primary != "" + if chosen_node.__dict__[self.resource_type + "_required"]: + file_exists = chosen_node.__dict__[self.resource_type + "_credit"].primary != "" if not file_exists and self.is_derivation: if recolor_shiny: - response += " doesn't have a {0}. Submit it first.".format(self.ressource_type) + response += " doesn't have a {0}. Submit it first.".format(self.resource_type) else: - response += " doesn't have a {0} to recolor. Submit the original first.".format(self.ressource_type) + response += " doesn't have a {0} to recolor. Submit the original first.".format(self.resource_type) else: if not file_exists: - response += "\n [This {0} is missing. If you want to submit, use this file as a template!]".format(self.ressource_type) + response += "\n [This {0} is missing. If you want to submit, use this file as a template!]".format(self.resource_type) else: - credit = chosen_node.__dict__[self.ressource_type + "_credit"] + credit = chosen_node.__dict__[self.resource_type + "_credit"] base_credit = None response += "\n" + self.spritebot.createCreditBlock(credit, base_credit) if len(credit.secondary) + 1 < credit.total: - response += "\nRun `!{0}credit {1}` for full credit.".format(self.ressource_type, " ".join(name_seq)) + response += "\nRun `!{0}credit {1}` for full credit.".format(self.resource_type, " ".join(name_seq)) if self.is_derivation and not recolor_shiny: - response += "\n [Recolor this {0} to its shiny palette and submit it.]".format(self.ressource_type) - chosen_link = await self.spritebot.retrieveLinkMsg(full_idx, chosen_node, self.ressource_type, self.is_derivation) + response += "\n [Recolor this {0} to its shiny palette and submit it.]".format(self.resource_type) + chosen_link = await self.spritebot.retrieveLinkMsg(full_idx, chosen_node, self.resource_type, self.is_derivation) response += "\n" + chosen_link - next_phase = chosen_node.__dict__[self.ressource_type + "_complete"] + 1 + next_phase = chosen_node.__dict__[self.resource_type + "_complete"] + 1 - if str(next_phase) in chosen_node.__dict__[self.ressource_type + "_bounty"]: - bounty = chosen_node.__dict__[self.ressource_type + "_bounty"][str(next_phase)] + if str(next_phase) in chosen_node.__dict__[self.resource_type + "_bounty"]: + bounty = chosen_node.__dict__[self.resource_type + "_bounty"][str(next_phase)] if bounty > 0: - response += "\n This {0} has a bounty of **{1}GP**, paid out when it becomes {2}".format(self.ressource_type, bounty, PHASES[next_phase].title()) - if chosen_node.modreward and chosen_node.__dict__[self.ressource_type + "_complete"] == TrackerUtils.PHASE_INCOMPLETE: - response += "\n The reward for this {0} will be decided by approvers.".format(self.ressource_type) + response += "\n This {0} has a bounty of **{1}GP**, paid out when it becomes {2}".format(self.resource_type, bounty, PHASES[next_phase].title()) + if chosen_node.modreward and chosen_node.__dict__[self.resource_type + "_complete"] == TrackerUtils.PHASE_INCOMPLETE: + response += "\n The reward for this {0} will be decided by approvers.".format(self.resource_type) else: - response += " does not need a {0}.".format(self.ressource_type) + response += " does not need a {0}.".format(self.resource_type) await msg.channel.send(response) \ No newline at end of file diff --git a/commands/ReplaceRessource.py b/commands/ReplaceResource.py similarity index 79% rename from commands/ReplaceRessource.py rename to commands/ReplaceResource.py index 3ea265c..14578b0 100644 --- a/commands/ReplaceRessource.py +++ b/commands/ReplaceResource.py @@ -8,23 +8,23 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class ReplaceRessource(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str): +class ReplaceResource(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type def getRequiredPermission(self) -> PermissionLevel: return PermissionLevel.STAFF def getCommand(self) -> str: - return f"replace{self.ressource_type}" + return f"replace{self.resource_type}" def getSingleLineHelp(self, server_config: "BotServer") -> str: - return f"Replace the content of one {self.ressource_type} with another" + return f"Replace the content of one {self.resource_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. " \ + "Replaces the contents of one {self.resource_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" \ @@ -61,21 +61,21 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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)) + if not chosen_node_from.__dict__[self.resource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when source {0} is unneeded.".format(self.resource_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)) + if not chosen_node_to.__dict__[self.resource_type + "_required"]: + await msg.channel.send(msg.author.mention + " Cannot move when destination {0} is unneeded.".format(self.resource_type)) return try: - await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, self.ressource_type) + await self.spritebot.checkMoveLock(full_idx_from, chosen_node_from, full_idx_to, chosen_node_to, self.resource_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) + await self.spritebot.checkMoveLock(full_idx_to, chosen_node_to, full_idx_from, chosen_node_from, self.resource_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 @@ -84,12 +84,13 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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) + TrackerUtils.replaceFolderPaths(self.spritebot.config.path, self.spritebot.tracker, self.resource_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))) + server_config = self.spritebot.config.servers[str(msg.guild.id)] + await msg.channel.send(msg.author.mention + " {0} is now empty. Use `{1}delete` if it is no longer needed.".format(" ".join(name_seq_from), server_config.prefix)) self.spritebot.saveTracker() self.spritebot.changed = True diff --git a/commands/SetNodeCanon.py b/commands/SetNodeCanon.py index bbbc349..5a08c77 100644 --- a/commands/SetNodeCanon.py +++ b/commands/SetNodeCanon.py @@ -46,6 +46,10 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): if full_idx is None: await msg.channel.send(msg.author.mention + " No such Pokemon.") return + if len(full_idx) != 2: + await msg.channel.send(msg.author.mention + " Must specify Pokemon and form.") + return + chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) TrackerUtils.setCanon(chosen_node, self.canon) diff --git a/commands/SetProfile.py b/commands/SetProfile.py index 52e1314..c99e21b 100644 --- a/commands/SetProfile.py +++ b/commands/SetProfile.py @@ -73,6 +73,7 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): new_credit = TrackerUtils.CreditEntry(args[0], args[1]) else: await msg.channel.send(msg.author.mention + " Invalid amounts of arguments") + return if entry_key in self.spritebot.names: new_credit.sprites = self.spritebot.names[entry_key].sprites diff --git a/commands/SetRessourceCompletion.py b/commands/SetResourceCompletion.py similarity index 81% rename from commands/SetRessourceCompletion.py rename to commands/SetResourceCompletion.py index 7a0f10e..f774fb5 100644 --- a/commands/SetRessourceCompletion.py +++ b/commands/SetResourceCompletion.py @@ -8,11 +8,11 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class SetRessourceCompletion(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str, completion: int): +class SetResourceCompletion(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_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.resource_type = resource_type + #TODO: completion should eventually be replaced by a class or enum once better in-memory resource typing is implemented. self.completion = completion def getRequiredPermission(self): @@ -49,17 +49,17 @@ def getCompletionCommandCode(self) -> str: raise NotImplementedError() def getCommand(self) -> str: - return self.ressource_type + self.getCompletionCommandCode() + return self.resource_type + self.getCompletionCommandCode() def getSingleLineHelp(self, server_config: "BotServer") -> str: - return f"Set the {self.ressource_type} status to {self.getCompletionName()}" + return f"Set the {self.resource_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" \ + f"Manually sets the {self.resource_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" \ + f"`Shiny` - [Optional] Specifies if you want the shiny {self.resource_type} or not\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ + self.generateMultiLineExample( server_config.prefix, @@ -84,16 +84,16 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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) + if chosen_node.__dict__[self.resource_type + "_credit"].primary == "" and self.completion > TrackerUtils.PHASE_INCOMPLETE: + status = TrackerUtils.getStatusEmoji(chosen_node, self.resource_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 + chosen_node.__dict__[self.resource_type + "_complete"] = self.completion - status = TrackerUtils.getStatusEmoji(chosen_node, self.ressource_type) + status = TrackerUtils.getStatusEmoji(chosen_node, self.resource_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() diff --git a/commands/SetRessourceCredit.py b/commands/SetResourceCredit.py similarity index 75% rename from commands/SetRessourceCredit.py rename to commands/SetResourceCredit.py index 6367668..7797471 100644 --- a/commands/SetRessourceCredit.py +++ b/commands/SetResourceCredit.py @@ -7,28 +7,28 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class SetRessourceCredit(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str): +class SetResourceCredit(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type def getRequiredPermission(self) -> PermissionLevel: return PermissionLevel.STAFF def getCommand(self) -> str: - return "set{}credit".format(self.ressource_type) + return "set{}credit".format(self.resource_type) def getSingleLineHelp(self, server_config: "BotServer") -> str: - return "Sets the primary author of the {}".format(self.ressource_type) + return "Sets the primary author of the {}".format(self.resource_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" \ + return f"`{server_config.prefix}set{self.resource_type}credit [Form Name] [Shiny] [Gender]`\n" \ + f"Manually sets the primary author of a {self.resource_type} to the specified author. " \ + f"The specified author must already exist in the credits for the {self.resource_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" \ + f"`Shiny` - [Optional] Specifies if you want the shiny {self.resource_type} or not\n" \ "`Gender` - [Optional] Specifies the gender of the Pokemon\n" \ + self.generateMultiLineExample(server_config.prefix, [ "@Audino Unown Shiny", @@ -54,27 +54,27 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): chosen_node = TrackerUtils.getNodeFromIdx(self.spritebot.tracker, full_idx, 0) - if chosen_node.__dict__[self.ressource_type + "_credit"].primary == "": + if chosen_node.__dict__[self.resource_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) + gen_path = TrackerUtils.getDirFromIdx(self.spritebot.config.path, self.resource_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)) + await msg.channel.send(msg.author.mention + " Could not find ID `{0}` in credits for {1}.".format(wanted_author, self.resource_type)) return # make the credit array into the most current author by itself - credit_data = chosen_node.__dict__[self.ressource_type + "_credit"] + credit_data = chosen_node.__dict__[self.resource_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)) + await msg.channel.send(msg.author.mention + " Cannot reset credit for a CHUNSOFT {0}.".format(self.resource_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))) + await msg.channel.send(msg.author.mention + " Credit display has been reset for {0} {1}:\n{2}".format(self.resource_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/SetResourceLock.py similarity index 65% rename from commands/SetRessourceLock.py rename to commands/SetResourceLock.py index 64dc487..22cd8d3 100644 --- a/commands/SetRessourceLock.py +++ b/commands/SetResourceLock.py @@ -7,10 +7,10 @@ if TYPE_CHECKING: from SpriteBot import SpriteBot, BotServer -class SetRessourceLock(BaseCommand): - def __init__(self, spritebot: "SpriteBot", ressource_type: str, lock: bool): +class SetResourceLock(BaseCommand): + def __init__(self, spritebot: "SpriteBot", resource_type: str, lock: bool): super().__init__(spritebot) - self.ressource_type = ressource_type + self.resource_type = resource_type self.lock = lock def getRequiredPermission(self): @@ -18,12 +18,12 @@ def getRequiredPermission(self): def getCommand(self) -> str: if self.lock: - return "lock" + self.ressource_type + return "lock" + self.resource_type else: - return "unlock" + self.ressource_type + return "unlock" + self.resource_type def getEmotionOrActionText(self) -> str: - if self.ressource_type == "sprite": + if self.resource_type == "sprite": return "action" else: return "emotion" @@ -31,9 +31,9 @@ def getEmotionOrActionText(self) -> str: def getSingleLineHelp(self, server_config: "BotServer") -> str: if self.lock: - return f"Mark a {self.ressource_type} {self.getEmotionOrActionText()}as locked" + return f"Mark a {self.resource_type} {self.getEmotionOrActionText()}as locked" else: - return f"Mark a {self.ressource_type} {self.getEmotionOrActionText()} as unlocked" + return f"Mark a {self.resource_type} {self.getEmotionOrActionText()} as unlocked" def getMultiLineHelp(self, server_config: "BotServer") -> str: if self.lock: @@ -43,7 +43,7 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: action = "unlock" description = "so it can be modified again" - if self.ressource_type == "sprite": + if self.resource_type == "sprite": example = [ "Pikachu pose", "Pikachu Female wake" @@ -55,7 +55,7 @@ def getMultiLineHelp(self, server_config: "BotServer") -> str: ] return f"`{server_config.prefix}{self.getCommand()} [Pokemon Form] [Shiny] [Gender] `\n" \ - f"Set a {self.ressource_type} {self.getEmotionOrActionText()} {description}. \n" \ + f"Set a {self.resource_type} {self.getEmotionOrActionText()} {description}. \n" \ + self.generateMultiLineExample(server_config.prefix, example) async def executeCommand(self, msg: discord.Message, args: List[str]): @@ -66,24 +66,22 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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 + final_file_names, failed_file_names = self.spritebot.parseFileNames(chosen_node, self.resource_type, args[-1]) - 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.") + if len(failed_file_names) > 0: + await msg.channel.send(msg.author.mention + " Could not find the emotion/animations:\n{0}.".format(",".join(failed_file_names))) return - chosen_node.__dict__[self.ressource_type + "_files"][file_name] = self.lock - status = TrackerUtils.getStatusEmoji(chosen_node, self.ressource_type) + for file_name in final_file_names: + chosen_node.__dict__[self.resource_type + "_files"][file_name] = self.lock + + status = TrackerUtils.getStatusEmoji(chosen_node, self.resource_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)) + await msg.channel.send(msg.author.mention + " {0} #{1:03d}: {2} {3} is now {4}.".format(status, int(full_idx[0]), " ".join(name_seq), ",".join(final_file_names), 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 index 016fd65..1915e85 100644 --- a/commands/Shutdown.py +++ b/commands/Shutdown.py @@ -30,4 +30,4 @@ async def executeCommand(self, msg: discord.Message, args: List[str]): 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 + await self.spritebot.client.close() diff --git a/utils.py b/utils.py index 6e2ac5e..0105fe5 100644 --- a/utils.py +++ b/utils.py @@ -48,7 +48,7 @@ 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, loc2, sub: bool = False): +def addLoc(loc1: Tuple[int, int], loc2: Tuple[int, int], sub: bool = False): mult = 1 if sub: mult = -1