diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/package-info.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/package-info.java new file mode 100644 index 0000000..dcccc86 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/package-info.java @@ -0,0 +1,4 @@ +/** + * Support for legacy parsing. + */ +package org.incendo.cloud.discord.legacy; diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordChannelParser.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordChannelParser.java new file mode 100644 index 0000000..3fc39eb --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordChannelParser.java @@ -0,0 +1,173 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.legacy.parser; + +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.context.CommandInput; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; + +/** + * Parser for Discord channels. + * + * @param command sender type + * @param guild type + * @param channel type + * @since 1.0.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +public abstract class DiscordChannelParser extends MentionableDiscordParser { + + protected DiscordChannelParser(final @NonNull Set modes) { + super(modes); + } + + @Override + public final @NonNull ArgumentParseResult parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final CommandInput commandInput + ) { + final ArgumentParseResult preProcessed = this.preProcess(commandContext); + if (preProcessed != null) { + return preProcessed; + } + + final String input = commandInput.readString(); + final DiscordRepository repository = this.repository(commandContext); + + Exception exception = null; + + if (this.modes().contains(DiscordParserMode.MENTION)) { + if (input.startsWith("<#") && input.endsWith(">")) { + final String id = input.substring(2, input.length() - 1); + + try { + final T result = repository.getById(id); + if (result != null) { + return ArgumentParseResult.success(result); + } + } catch (final ChannelNotFoundParseException | NumberFormatException e) { + exception = e; + } + } else { + exception = new IllegalArgumentException(String.format("Input '%s' is not a channel mention.", input)); + } + } + + if (this.modes().contains(DiscordParserMode.ID)) { + try { + final T result = repository.getById(input); + if (result != null) { + return ArgumentParseResult.success(result); + } + } catch (final ChannelNotFoundParseException | NumberFormatException e) { + exception = e; + } + } + + if (this.modes().contains(DiscordParserMode.NAME)) { + final Collection channels = repository.getByName(input); + + if (channels.isEmpty()) { + exception = new ChannelNotFoundParseException(input); + } else if (channels.size() > 1) { + exception = new TooManyChannelsFoundParseException(input); + } else { + return ArgumentParseResult.success(channels.stream().findFirst().get()); + } + } + + return ArgumentParseResult.failure(Objects.requireNonNull(exception, "exception")); + } + + + public static class ChannelParseException extends IllegalArgumentException { + + private static final long serialVersionUID = 2724288304060572202L; + private final String input; + + /** + * Construct a new channel parse exception + * + * @param input String input + */ + public ChannelParseException(final @NonNull String input) { + this.input = input; + } + + /** + * Get the users input + * + * @return users input + */ + public final @NonNull String input() { + return this.input; + } + } + + + public static final class TooManyChannelsFoundParseException extends ChannelParseException { + + private static final long serialVersionUID = -507783063742841507L; + + /** + * Construct a new channel parse exception + * + * @param input String input + */ + public TooManyChannelsFoundParseException(final @NonNull String input) { + super(input); + } + + @Override + public @NonNull String getMessage() { + return String.format("Too many channels found for '%s'.", input()); + } + } + + + public static final class ChannelNotFoundParseException extends ChannelParseException { + + private static final long serialVersionUID = -8299458048947528494L; + + /** + * Construct a new channel parse exception + * + * @param input String input + */ + public ChannelNotFoundParseException(final @NonNull String input) { + super(input); + } + + @Override + public @NonNull String getMessage() { + return String.format("Channel not found for '%s'.", input()); + } + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordMemberParser.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordMemberParser.java new file mode 100644 index 0000000..0619d64 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordMemberParser.java @@ -0,0 +1,184 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.legacy.parser; + +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.context.CommandInput; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; + +/** + * Parser for Discord members. + * + * @param command sender type + * @param guild type + * @param member type + * @since 1.0.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +public abstract class DiscordMemberParser extends MentionableDiscordParser { + + protected DiscordMemberParser(final @NonNull Set<@NonNull DiscordParserMode> modes) { + super(modes); + } + + @Override + public final @NonNull ArgumentParseResult<@NonNull T> parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final CommandInput commandInput + ) { + final ArgumentParseResult preProcessed = this.preProcess(commandContext); + if (preProcessed != null) { + return preProcessed; + } + + final String input = commandInput.readString(); + final DiscordRepository repository = this.repository(commandContext); + + Exception exception = null; + + if (this.modes().contains(DiscordParserMode.MENTION)) { + if (input.startsWith("<@") && input.endsWith(">")) { + final String id; + if (input.startsWith("<@!")) { + id = input.substring(3, input.length() - 1); + } else { + id = input.substring(2, input.length() - 1); + } + + try { + final T result = repository.getById(id); + if (result != null) { + return ArgumentParseResult.success(result); + } + } catch (final MemberNotFoundParseException | NumberFormatException e) { + exception = e; + } + } else { + exception = new IllegalArgumentException(String.format("Input '%s' is not a member mention.", input)); + } + } + + if (this.modes().contains(DiscordParserMode.ID)) { + try { + final T result = repository.getById(input); + if (result != null) { + return ArgumentParseResult.success(result); + } + } catch (final MemberNotFoundParseException | NumberFormatException e) { + exception = e; + } + } + + if (this.modes().contains(DiscordParserMode.NAME)) { + final Collection members = repository.getByName(input); + + if (members.isEmpty()) { + exception = new MemberNotFoundParseException(input); + } else if (members.size() > 1) { + exception = new TooManyMembersFoundParseException(input); + } else { + return ArgumentParseResult.success(members.stream().findFirst().get()); + } + } + + return ArgumentParseResult.failure(Objects.requireNonNull(exception, "exception")); + } + + + public static class MemberParseException extends IllegalArgumentException { + + private final String input; + + /** + * Constructs a new member parse exception. + * + * @param input string input + */ + public MemberParseException(final @NonNull String input) { + this.input = input; + } + + /** + * Returns the user's input. + * + * @return user's input + */ + public final @NonNull String input() { + return this.input; + } + } + + + public static final class TooManyMembersFoundParseException extends MemberParseException { + + /** + * Constructs a new member parse exception. + * + * @param input string input + */ + public TooManyMembersFoundParseException(final @NonNull String input) { + super(input); + } + + @Override + public String getMessage() { + return String.format("Too many users found for '%s'.", this.input()); + } + } + + + public static final class MemberNotFoundParseException extends MemberParseException { + + /** + * Constructs a new member parse exception. + * + * @param input string input + */ + public MemberNotFoundParseException(final @NonNull String input) { + super(input); + } + + @Override + public String getMessage() { + return String.format("User not found for '%s'.", this.input()); + } + } + + + public static final class CommandNotFromGuildException extends IllegalArgumentException { + + /** + * Constructs a new command not from guild exception. + */ + public CommandNotFromGuildException() { + super("Command must be executed in a guild."); + } + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordParser.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordParser.java new file mode 100644 index 0000000..9d9f5cb --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordParser.java @@ -0,0 +1,54 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.legacy.parser; + +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.context.CommandContext; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; + +/** + * Parser for Discord objects. + * + * @param command sender type + * @param guild type + * @param parsed value type + * @since 1.0.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +abstract class DiscordParser implements ArgumentParser { + + /** + * Retrieves the repository from the given {@code context}. + * + * @param context command context + * @return the repository + */ + protected abstract @NonNull DiscordRepository repository(@NonNull CommandContext context); + + protected abstract @Nullable ArgumentParseResult preProcess(@NonNull CommandContext context); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordParserMode.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordParserMode.java new file mode 100644 index 0000000..5be5436 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordParserMode.java @@ -0,0 +1,33 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.legacy.parser; + +import org.apiguardian.api.API; + +@API(status = API.Status.STABLE, since = "1.0.0") +public enum DiscordParserMode { + ID, + MENTION, + NAME +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordRoleParser.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordRoleParser.java new file mode 100644 index 0000000..37c2b81 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordRoleParser.java @@ -0,0 +1,173 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.legacy.parser; + +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.context.CommandInput; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; + +/** + * Parser for Discord roles. + * + * @param command sender type + * @param guild type + * @param role type + * @since 1.0.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +public abstract class DiscordRoleParser extends MentionableDiscordParser { + + protected DiscordRoleParser(final @NonNull Set<@NonNull DiscordParserMode> modes) { + super(modes); + } + + @Override + public final @NonNull ArgumentParseResult<@NonNull T> parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final CommandInput commandInput + ) { + final ArgumentParseResult preProcessed = this.preProcess(commandContext); + if (preProcessed != null) { + return preProcessed; + } + + final String input = commandInput.readString(); + final DiscordRepository repository = this.repository(commandContext); + + Exception exception = null; + + if (this.modes().contains(DiscordParserMode.MENTION)) { + if (input.startsWith("<@") && input.endsWith(">")) { + final String id = input.substring(3, input.length() - 1); + + try { + final T result = repository.getById(id); + if (result != null) { + return ArgumentParseResult.success(result); + } + } catch (final RoleNotFoundParseException | NumberFormatException e) { + exception = e; + } + } else { + exception = new IllegalArgumentException(String.format("Input '%s' is not a role mention.", input)); + } + } + + if (this.modes().contains(DiscordParserMode.ID)) { + try { + final T result = repository.getById(input); + if (result != null) { + return ArgumentParseResult.success(result); + } + } catch (final RoleNotFoundParseException | NumberFormatException e) { + exception = e; + } + } + + if (this.modes().contains(DiscordParserMode.NAME)) { + final Collection users = repository.getByName(input); + + if (users.isEmpty()) { + exception = new RoleNotFoundParseException(input); + } else if (users.size() > 1) { + exception = new TooManyRolesFoundParseException(input); + } else { + return ArgumentParseResult.success(users.stream().findFirst().get()); + } + } + + return ArgumentParseResult.failure(Objects.requireNonNull(exception, "exception")); + } + + + public static class RoleParseException extends IllegalArgumentException { + + private static final long serialVersionUID = -2451548379508062135L; + private final String input; + + /** + * Construct a new role parse exception + * + * @param input String input + */ + public RoleParseException(final @NonNull String input) { + this.input = input; + } + + /** + * Get the users input + * + * @return users input + */ + public final @NonNull String input() { + return this.input; + } + } + + + public static final class TooManyRolesFoundParseException extends RoleParseException { + + private static final long serialVersionUID = -8604082973199995006L; + + /** + * Construct a new role parse exception + * + * @param input String input + */ + public TooManyRolesFoundParseException(final @NonNull String input) { + super(input); + } + + @Override + public @NonNull String getMessage() { + return String.format("Too many roles found for '%s'.", input()); + } + } + + + public static final class RoleNotFoundParseException extends RoleParseException { + + private static final long serialVersionUID = 7931804739792920510L; + + /** + * Construct a new role parse exception + * + * @param input String input + */ + public RoleNotFoundParseException(final @NonNull String input) { + super(input); + } + + @Override + public @NonNull String getMessage() { + return String.format("Role not found for '%s'.", input()); + } + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordUserParser.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordUserParser.java new file mode 100644 index 0000000..9c22973 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/DiscordUserParser.java @@ -0,0 +1,196 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.legacy.parser; + +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.context.CommandInput; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; + +/** + * Parser for Discord users. + * + * @param command sender type + * @param guild type + * @param user type + * @since 1.0.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +public abstract class DiscordUserParser extends MentionableDiscordParser { + + private final Isolation isolation; + + protected DiscordUserParser( + final @NonNull Set<@NonNull DiscordParserMode> modes, + final @NonNull Isolation isolation + ) { + super(modes); + this.isolation = Objects.requireNonNull(isolation, "isolation"); + } + + /** + * Returns the isolation. + * + * @return the isolation + */ + public @NonNull Isolation isolation() { + return this.isolation; + } + + @Override + public final @NonNull ArgumentParseResult<@NonNull T> parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final CommandInput commandInput + ) { + final ArgumentParseResult preProcessed = this.preProcess(commandContext); + if (preProcessed != null) { + return preProcessed; + } + + final String input = commandInput.readString(); + final DiscordRepository repository = this.repository(commandContext); + + Exception exception = null; + + if (this.modes().contains(DiscordParserMode.MENTION)) { + if (input.startsWith("<@") && input.endsWith(">")) { + final String id; + if (input.startsWith("<@!")) { + id = input.substring(3, input.length() - 1); + } else { + id = input.substring(2, input.length() - 1); + } + + try { + final T result = repository.getById(id); + if (result != null) { + return ArgumentParseResult.success(result); + } + } catch (final UserNotFoundParseException | NumberFormatException e) { + exception = e; + } + } else { + exception = new IllegalArgumentException(String.format("Input '%s' is not a User mention.", input)); + } + } + + if (this.modes().contains(DiscordParserMode.ID)) { + try { + final T result = repository.getById(input); + if (result != null) { + return ArgumentParseResult.success(result); + } + } catch (final UserNotFoundParseException | NumberFormatException e) { + exception = e; + } + } + + if (this.modes().contains(DiscordParserMode.NAME)) { + final Collection users = repository.getByName(input); + + if (users.isEmpty()) { + exception = new UserNotFoundParseException(input); + } else if (users.size() > 1) { + exception = new TooManyUsersFoundParseException(input); + } else { + return ArgumentParseResult.success(users.stream().findFirst().get()); + } + } + + return ArgumentParseResult.failure(Objects.requireNonNull(exception, "exception")); + } + + + public enum Isolation { + GLOBAL, + GUILD + } + + + public static class UserParseException extends IllegalArgumentException { + + private final String input; + + /** + * Construct a new user parse exception + * + * @param input String input + */ + public UserParseException(final @NonNull String input) { + this.input = input; + } + + /** + * Get the users input + * + * @return Users input + */ + public final @NonNull String input() { + return this.input; + } + } + + + public static final class TooManyUsersFoundParseException extends UserParseException { + + + /** + * Construct a new user parse exception + * + * @param input String input + */ + public TooManyUsersFoundParseException(final @NonNull String input) { + super(input); + } + + @Override + public @NonNull String getMessage() { + return String.format("Too many users found for '%s'.", input()); + } + } + + + public static final class UserNotFoundParseException extends UserParseException { + + + /** + * Construct a new user parse exception + * + * @param input String input + */ + public UserNotFoundParseException(final @NonNull String input) { + super(input); + } + + @Override + public @NonNull String getMessage() { + return String.format("User not found for '%s'.", input()); + } + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/MentionableDiscordParser.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/MentionableDiscordParser.java new file mode 100644 index 0000000..8108035 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/MentionableDiscordParser.java @@ -0,0 +1,53 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.legacy.parser; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +@API(status = API.Status.INTERNAL, since = "1.0.0") +public abstract class MentionableDiscordParser extends DiscordParser { + + private final Set modes; + + protected MentionableDiscordParser(final @NonNull Set modes) { + Objects.requireNonNull(modes, "modes"); + if (modes.isEmpty()) { + throw new IllegalArgumentException("At least one parsing mode is required"); + } + this.modes = modes; + } + + /** + * Returns the enabled modes for this parser. + * + * @return the modes + */ + public @NonNull Set modes() { + return Collections.unmodifiableSet(this.modes); + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/package-info.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/package-info.java new file mode 100644 index 0000000..b6db4d7 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/parser/package-info.java @@ -0,0 +1,4 @@ +/** + * Discord parsers. + */ +package org.incendo.cloud.discord.legacy.parser; diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/repository/DiscordRepository.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/repository/DiscordRepository.java new file mode 100644 index 0000000..1d3bb2a --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/repository/DiscordRepository.java @@ -0,0 +1,67 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.legacy.repository; + +import java.util.Collection; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A repository for Discord objects. + * + * @param guild type + * @param value type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface DiscordRepository { + + /** + * Returns the object by its {@code id}. + * + * @param id id to retrieve object by + * @return result, or {@code null} + */ + @Nullable T getById(long id); + + /** + * Returns the object by its {@code id}. + * + * @param id id to retrieve object by + * @return result, or {@code null} + * @throws NumberFormatException if the given {@code id} is invalid + */ + default @Nullable T getById(final @NonNull String id) throws NumberFormatException { + return this.getById(Long.parseLong(id)); + } + + /** + * Returns all objects with the given {@code name}. + * + * @param name name to retrieve objects by + * @return the objects + */ + @NonNull Collection getByName(@NonNull String name); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/repository/package-info.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/repository/package-info.java new file mode 100644 index 0000000..5b779b3 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/legacy/repository/package-info.java @@ -0,0 +1,4 @@ +/** + * Repositories for Discord objects. + */ +package org.incendo.cloud.discord.legacy.repository; diff --git a/cloud-jda/build.gradle.kts b/cloud-jda/build.gradle.kts index 2acbd58..cab71a4 100644 --- a/cloud-jda/build.gradle.kts +++ b/cloud-jda/build.gradle.kts @@ -7,6 +7,8 @@ version = "2.0.0-SNAPSHOT" dependencies { api(libs.cloud.core) + api(projects.cloudDiscordCommon) + compileOnly(libs.cloud.annotations) compileOnly(libs.jda) } diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/JDACommandManager.java b/cloud-jda/src/main/java/cloud/commandframework/jda/JDACommandManager.java index 0a1d49f..5ccd0fa 100644 --- a/cloud-jda/src/main/java/cloud/commandframework/jda/JDACommandManager.java +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/JDACommandManager.java @@ -60,6 +60,7 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.discord.legacy.parser.DiscordParserMode; /** * Command manager for use with JDA @@ -167,20 +168,20 @@ public JDACommandManager( /* Register JDA Parsers */ this.parserRegistry().registerParserSupplier(TypeToken.get(User.class), parserParameters -> new UserParser<>( - EnumSet.allOf(UserParser.ParserMode.class), + EnumSet.allOf(DiscordParserMode.class), UserParser.Isolation.GLOBAL )); this.parserRegistry().registerParserSupplier(TypeToken.get(Member.class), parserParameters -> new MemberParser<>( - EnumSet.allOf(MemberParser.ParserMode.class) + EnumSet.allOf(DiscordParserMode.class) )); this.parserRegistry().registerParserSupplier(TypeToken.get(MessageChannel.class), parserParameters -> new ChannelParser<>( - EnumSet.allOf(ChannelParser.ParserMode.class) + EnumSet.allOf(DiscordParserMode.class) )); this.parserRegistry().registerParserSupplier(TypeToken.get(Role.class), parserParameters -> new RoleParser<>( - EnumSet.allOf(RoleParser.ParserMode.class) + EnumSet.allOf(DiscordParserMode.class) )); // No "native" command system means that we can delete commands just fine. diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/ChannelParser.java b/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/ChannelParser.java index ae5e952..89645a3 100644 --- a/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/ChannelParser.java +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/ChannelParser.java @@ -25,32 +25,31 @@ import cloud.commandframework.CommandComponent; import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.parser.ParserDescriptor; import cloud.commandframework.context.CommandContext; -import cloud.commandframework.context.CommandInput; -import java.util.List; +import cloud.commandframework.jda.repository.JDAChannelRepository; import java.util.Set; -import java.util.concurrent.CompletionException; +import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.MessageChannel; -import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.exceptions.ErrorResponseException; -import net.dv8tion.jda.api.requests.ErrorResponse; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; -import org.jetbrains.annotations.NotNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.discord.legacy.parser.DiscordChannelParser; +import org.incendo.cloud.discord.legacy.parser.DiscordParserMode; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; /** - * Command Argument for {@link MessageChannel} + * Parser {@link MessageChannel} * - * @param Command sender type + * @param command sender type */ @SuppressWarnings("unused") -public final class ChannelParser implements ArgumentParser { +@API(status = API.Status.STABLE, since = "2.0.0") +public final class ChannelParser extends DiscordChannelParser { /** - * Creates a new server parser. + * Creates a new channel parser. * * @param command sender type * @param modes parser modes to use @@ -58,7 +57,7 @@ public final class ChannelParser implements ArgumentParser * @since 2.0.0 */ @API(status = API.Status.STABLE, since = "2.0.0") - public static @NonNull ParserDescriptor channelParser(final @NonNull Set modes) { + public static @NonNull ParserDescriptor channelParser(final @NonNull Set modes) { return ParserDescriptor.of(new ChannelParser<>(modes), MessageChannel.class); } @@ -71,192 +70,40 @@ public final class ChannelParser implements ArgumentParser * @since 2.0.0 */ @API(status = API.Status.STABLE, since = "2.0.0") - public static CommandComponent.@NonNull Builder channelComponent(final @NonNull Set modes) { + public static CommandComponent.@NonNull Builder channelComponent( + final @NonNull Set modes + ) { return CommandComponent.builder().parser(channelParser(modes)); } - private final Set modes; - /** * Construct a new channel parser. * * @param modes parser modes to use */ - public ChannelParser(final @NonNull Set modes) { - if (modes.isEmpty()) { - throw new IllegalArgumentException("At least one parsing mode is required"); - } - - this.modes = modes; - } - - /** - * Get the modes enabled on the parser - * - * @return Set of Modes - */ - public @NotNull Set getModes() { - return this.modes; - } - - - public enum ParserMode { - MENTION, - ID, - NAME + public ChannelParser(final @NonNull Set modes) { + super(modes); } @Override - public @NonNull ArgumentParseResult parse( - final @NonNull CommandContext commandContext, - final @NonNull CommandInput commandInput - ) { - final String input = commandInput.peekString(); - - if (!commandContext.contains("MessageReceivedEvent")) { + protected @Nullable ArgumentParseResult preProcess(final @NonNull CommandContext context) { + if (!context.contains("MessageReceivedEvent")) { return ArgumentParseResult.failure(new IllegalStateException( "MessageReceivedEvent was not in the command context." )); } - final MessageReceivedEvent event = commandContext.get("MessageReceivedEvent"); - Exception exception = null; - + final MessageReceivedEvent event = context.get("MessageReceivedEvent"); if (!event.isFromGuild()) { return ArgumentParseResult.failure(new IllegalArgumentException("Channel arguments can only be parsed in guilds")); } - if (this.modes.contains(ParserMode.MENTION)) { - if (input.startsWith("<#") && input.endsWith(">")) { - final String id = input.substring(2, input.length() - 1); - - try { - final ArgumentParseResult channel = this.channelFromId(event, input, id); - commandInput.readString(); - return channel; - } catch (final ChannelNotFoundParseException | NumberFormatException e) { - exception = e; - } - } else { - exception = new IllegalArgumentException( - String.format("Input '%s' is not a channel mention.", input) - ); - } - } - - if (this.modes.contains(ParserMode.ID)) { - try { - final ArgumentParseResult result = this.channelFromId(event, input, input); - commandInput.readString(); - return result; - } catch (final ChannelNotFoundParseException | NumberFormatException e) { - exception = e; - } - } - - if (this.modes.contains(ParserMode.NAME)) { - final List channels = event.getGuild().getTextChannelsByName(input, true); - - if (channels.isEmpty()) { - exception = new ChannelNotFoundParseException(input); - } else if (channels.size() > 1) { - exception = new TooManyChannelsFoundParseException(input); - } else { - commandInput.readString(); - return ArgumentParseResult.success(channels.get(0)); - } - } - - assert exception != null; - return ArgumentParseResult.failure(exception); + return null; } - private @NonNull ArgumentParseResult channelFromId( - final @NonNull MessageReceivedEvent event, - final @NonNull String input, - final @NonNull String id - ) - throws ChannelNotFoundParseException, NumberFormatException { - try { - final MessageChannel channel = event.getGuild().getTextChannelById(id); - - if (channel == null) { - throw new ChannelNotFoundParseException(input); - } - - return ArgumentParseResult.success(channel); - } catch (final CompletionException e) { - if (e.getCause().getClass().equals(ErrorResponseException.class) - && ((ErrorResponseException) e.getCause()).getErrorResponse() == ErrorResponse.UNKNOWN_CHANNEL) { - //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException - throw new ChannelNotFoundParseException(input); - } - throw e; - } - } - - - public static class ChannelParseException extends IllegalArgumentException { - - private static final long serialVersionUID = 2724288304060572202L; - private final String input; - - /** - * Construct a new channel parse exception - * - * @param input String input - */ - public ChannelParseException(final @NonNull String input) { - this.input = input; - } - - /** - * Get the users input - * - * @return users input - */ - public final @NonNull String input() { - return this.input; - } - } - - - public static final class TooManyChannelsFoundParseException extends ChannelParseException { - - private static final long serialVersionUID = -507783063742841507L; - - /** - * Construct a new channel parse exception - * - * @param input String input - */ - public TooManyChannelsFoundParseException(final @NonNull String input) { - super(input); - } - - @Override - public @NonNull String getMessage() { - return String.format("Too many channels found for '%s'.", input()); - } - } - - - public static final class ChannelNotFoundParseException extends ChannelParseException { - - private static final long serialVersionUID = -8299458048947528494L; - - /** - * Construct a new channel parse exception - * - * @param input String input - */ - public ChannelNotFoundParseException(final @NonNull String input) { - super(input); - } - - @Override - public @NonNull String getMessage() { - return String.format("Channel not found for '%s'.", input()); - } + @Override + protected @NonNull DiscordRepository repository(final @NonNull CommandContext context) { + final MessageReceivedEvent event = context.get("MessageReceivedEvent"); + return new JDAChannelRepository(event.getGuild()); } } diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/MemberParser.java b/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/MemberParser.java index 8f1cfb8..36d1ddc 100644 --- a/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/MemberParser.java +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/MemberParser.java @@ -23,21 +23,21 @@ // package cloud.commandframework.jda.parsers; +import cloud.commandframework.CommandComponent; import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.arguments.parser.ParserDescriptor; import cloud.commandframework.context.CommandContext; -import cloud.commandframework.context.CommandInput; -import java.util.Collections; -import java.util.List; +import cloud.commandframework.jda.repository.JDAMemberRepository; import java.util.Set; -import java.util.concurrent.CompletionException; -import java.util.stream.Collectors; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.exceptions.ErrorResponseException; -import net.dv8tion.jda.api.requests.ErrorResponse; +import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.discord.legacy.parser.DiscordMemberParser; +import org.incendo.cloud.discord.legacy.parser.DiscordParserMode; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; /** * Parser for {@link Member}. @@ -46,9 +46,34 @@ * @since 2.0.0 */ @SuppressWarnings("unused") -public final class MemberParser implements ArgumentParser { +@API(status = API.Status.STABLE, since = "2.0.0") +public final class MemberParser extends DiscordMemberParser { - private final Set modes; + /** + * Creates a new member parser. + * + * @param command sender type + * @param modes parser modes to use + * @return the created parser + * @since 2.0.0 + */ + @API(status = API.Status.STABLE, since = "2.0.0") + public static @NonNull ParserDescriptor memberParser(final @NonNull Set modes) { + return ParserDescriptor.of(new MemberParser<>(modes), Member.class); + } + + /** + * Returns a {@link CommandComponent.Builder} using {@link #memberParser} as the parser. + * + * @param the command sender type + * @param modes parser modes to use + * @return the component builder + * @since 2.0.0 + */ + @API(status = API.Status.STABLE, since = "2.0.0") + public static CommandComponent.@NonNull Builder roleComponent(final @NonNull Set modes) { + return CommandComponent.builder().parser(memberParser(modes)); + } /** * Constructs a new parser for {@link Member}. @@ -56,198 +81,29 @@ public final class MemberParser implements ArgumentParser { * @param modes parsing modes to use when parsing * @throws IllegalArgumentException if no parsing modes were provided */ - public MemberParser(final @NonNull Set modes) { - if (modes.isEmpty()) { - throw new IllegalArgumentException("At least one parsing mode is required"); - } - this.modes = modes; + public MemberParser(final @NonNull Set modes) { + super(modes); } @Override - public @NonNull ArgumentParseResult<@NonNull Member> parse( - final @NonNull CommandContext<@NonNull C> commandContext, - final @NonNull CommandInput commandInput - ) { - if (!commandContext.contains("MessageReceivedEvent")) { + protected @NonNull DiscordRepository repository(final @NonNull CommandContext context) { + final MessageReceivedEvent event = context.get("MessageReceivedEvent"); + return new JDAMemberRepository(event.getGuild()); + } + + @Override + protected @Nullable ArgumentParseResult preProcess(final @NonNull CommandContext context) { + if (!context.contains("MessageReceivedEvent")) { return ArgumentParseResult.failure(new IllegalStateException( "MessageReceivedEvent was not in the command context." )); } - final MessageReceivedEvent event = commandContext.get("MessageReceivedEvent"); - + final MessageReceivedEvent event = context.get("MessageReceivedEvent"); if (!event.isFromGuild()) { return ArgumentParseResult.failure(new CommandNotFromGuildException()); } - Exception exception = null; - final String input = commandInput.readString(); - - if (this.modes.contains(ParserMode.MENTION)) { - if (input.startsWith("<@") && input.endsWith(">")) { - final String id; - if (input.startsWith("<@!")) { - id = input.substring(3, input.length() - 1); - } else { - id = input.substring(2, input.length() - 1); - } - - try { - return this.memberFromId(event, input, Long.parseLong(id)); - } catch (final MemberNotFoundParseException | NumberFormatException e) { - exception = e; - } - } else { - exception = new IllegalArgumentException( - String.format("Input '%s' is not a member mention.", input) - ); - } - } - - if (this.modes.contains(ParserMode.ID)) { - try { - return this.memberFromId(event, input, Long.parseLong(input)); - } catch (final MemberNotFoundParseException | NumberFormatException e) { - exception = e; - } - } - - if (this.modes.contains(ParserMode.NAME)) { - final List members; - - if (event.getAuthor().getName().equalsIgnoreCase(input)) { - members = Collections.singletonList(event.getMember()); - } else { - members = event.getGuild().getMembers() - .stream() - .filter(member -> member.getEffectiveName().toLowerCase().startsWith(input)) - .collect(Collectors.toList()); - } - - if (members.isEmpty()) { - exception = new MemberNotFoundParseException(input); - } else if (members.size() > 1) { - exception = new TooManyMembersFoundParseException(input); - } else { - return ArgumentParseResult.success(members.get(0)); - } - } - - assert exception != null; - return ArgumentParseResult.failure(exception); - } - - private @NonNull ArgumentParseResult memberFromId( - final @NonNull MessageReceivedEvent event, - final @NonNull String input, - final @NonNull Long id - ) - throws MemberNotFoundParseException, NumberFormatException { - try { - final Guild guild = event.getGuild(); - - final Member member; - if (event.getAuthor().getIdLong() == id) { - member = event.getMember(); - } else { - Member guildMember = guild.getMemberById(id); - - if (guildMember == null) { // fallback if member is not cached - guildMember = guild.retrieveMemberById(id).complete(); - } - member = guildMember; - } - - if (member == null) { - throw new MemberNotFoundParseException(input); - } else { - return ArgumentParseResult.success(member); - } - } catch (final CompletionException e) { - if (e.getCause().getClass().equals(ErrorResponseException.class) - && ((ErrorResponseException) e.getCause()).getErrorResponse() == ErrorResponse.UNKNOWN_MEMBER) { - //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException - throw new MemberNotFoundParseException(input); - } - throw e; - } - } - - - public enum ParserMode { - MENTION, - ID, - NAME - } - - - public static class MemberParseException extends IllegalArgumentException { - - private final String input; - - /** - * Constructs a new member parse exception. - * - * @param input string input - */ - public MemberParseException(final @NonNull String input) { - this.input = input; - } - - /** - * Returns the user's input. - * - * @return user's input - */ - public final @NonNull String input() { - return this.input; - } - } - - - public static final class TooManyMembersFoundParseException extends MemberParseException { - - /** - * Constructs a new member parse exception. - * - * @param input string input - */ - public TooManyMembersFoundParseException(final @NonNull String input) { - super(input); - } - - @Override - public String getMessage() { - return String.format("Too many users found for '%s'.", this.input()); - } - } - - - public static final class MemberNotFoundParseException extends MemberParseException { - - /** - * Constructs a new member parse exception. - * - * @param input string input - */ - public MemberNotFoundParseException(final @NonNull String input) { - super(input); - } - - @Override - public String getMessage() { - return String.format("User not found for '%s'.", this.input()); - } - } - - - public static final class CommandNotFromGuildException extends IllegalArgumentException { - - /** - * Constructs a new command not from guild exception. - */ - public CommandNotFromGuildException() { - super("Command must be executed in a guild."); - } + return null; } } diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/RoleParser.java b/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/RoleParser.java index 225360e..6f58af5 100644 --- a/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/RoleParser.java +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/RoleParser.java @@ -25,31 +25,31 @@ import cloud.commandframework.CommandComponent; import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.parser.ParserDescriptor; import cloud.commandframework.context.CommandContext; -import cloud.commandframework.context.CommandInput; -import java.util.List; +import cloud.commandframework.jda.repository.JDARoleRepository; import java.util.Set; -import java.util.concurrent.CompletionException; +import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.exceptions.ErrorResponseException; -import net.dv8tion.jda.api.requests.ErrorResponse; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; -import org.jetbrains.annotations.NotNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.discord.legacy.parser.DiscordParserMode; +import org.incendo.cloud.discord.legacy.parser.DiscordRoleParser; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; /** - * Command Argument for {@link net.dv8tion.jda.api.entities.Role} + * Command Argument for {@link Role} * * @param Command sender type */ @SuppressWarnings("unused") -public final class RoleParser implements ArgumentParser { +@API(status = API.Status.STABLE, since = "2.0.0") +public final class RoleParser extends DiscordRoleParser { /** - * Creates a new server parser. + * Creates a new role parser. * * @param command sender type * @param modes parser modes to use @@ -57,7 +57,7 @@ public final class RoleParser implements ArgumentParser { * @since 2.0.0 */ @API(status = API.Status.STABLE, since = "2.0.0") - public static @NonNull ParserDescriptor roleParser(final @NonNull Set modes) { + public static @NonNull ParserDescriptor roleParser(final @NonNull Set modes) { return ParserDescriptor.of(new RoleParser<>(modes), Role.class); } @@ -70,188 +70,36 @@ public final class RoleParser implements ArgumentParser { * @since 2.0.0 */ @API(status = API.Status.STABLE, since = "2.0.0") - public static CommandComponent.@NonNull Builder roleComponent(final @NonNull Set modes) { + public static CommandComponent.@NonNull Builder roleComponent(final @NonNull Set modes) { return CommandComponent.builder().parser(roleParser(modes)); } - private final Set modes; - /** * Construct a new role parser. * * @param modes parser modules to use */ - public RoleParser(final @NonNull Set modes) { - this.modes = modes; + public RoleParser(final @NonNull Set modes) { + super(modes); } - /** - * Get the modes enabled on the parser - * - * @return Set of Modes - */ - public @NotNull Set getModes() { - return this.modes; - } - - - public enum ParserMode { - MENTION, - ID, - NAME - } - - @Override - public @NonNull ArgumentParseResult parse( - final @NonNull CommandContext commandContext, - final @NonNull CommandInput commandInput - ) { - final String input = commandInput.peekString(); - - if (!commandContext.contains("MessageReceivedEvent")) { + protected @Nullable ArgumentParseResult preProcess(@NonNull final CommandContext context) { + if (!context.contains("MessageReceivedEvent")) { return ArgumentParseResult.failure(new IllegalStateException( "MessageReceivedEvent was not in the command context." )); } - - final MessageReceivedEvent event = commandContext.get("MessageReceivedEvent"); - Exception exception = null; - + final MessageReceivedEvent event = context.get("MessageReceivedEvent"); if (!event.isFromGuild()) { return ArgumentParseResult.failure(new IllegalArgumentException("Role arguments can only be parsed in guilds")); } - - if (this.modes.contains(ParserMode.MENTION)) { - if (input.startsWith("<@&") && input.endsWith(">")) { - final String id = input.substring(3, input.length() - 1); - - try { - final ArgumentParseResult role = this.roleFromId(event, input, id); - commandInput.readString(); - return role; - } catch (final RoleNotFoundParseException | NumberFormatException e) { - exception = e; - } - } else { - exception = new IllegalArgumentException( - String.format("Input '%s' is not a role mention.", input) - ); - } - } - - if (this.modes.contains(ParserMode.ID)) { - try { - final ArgumentParseResult result = this.roleFromId(event, input, input); - commandInput.readString(); - return result; - } catch (final RoleNotFoundParseException | NumberFormatException e) { - exception = e; - } - } - - if (this.modes.contains(ParserMode.NAME)) { - final List roles = event.getGuild().getRolesByName(input, true); - - if (roles.isEmpty()) { - exception = new RoleNotFoundParseException(input); - } else if (roles.size() > 1) { - exception = new TooManyRolesFoundParseException(input); - } else { - commandInput.readString(); - return ArgumentParseResult.success(roles.get(0)); - } - } - - assert exception != null; - return ArgumentParseResult.failure(exception); + return null; } - private @NonNull ArgumentParseResult roleFromId( - final @NonNull MessageReceivedEvent event, - final @NonNull String input, - final @NonNull String id - ) - throws RoleNotFoundParseException, NumberFormatException { - try { - final Role role = event.getGuild().getRoleById(id); - - if (role == null) { - throw new RoleNotFoundParseException(input); - } - - return ArgumentParseResult.success(role); - } catch (final CompletionException e) { - if (e.getCause().getClass().equals(ErrorResponseException.class) - && ((ErrorResponseException) e.getCause()).getErrorResponse() == ErrorResponse.UNKNOWN_ROLE) { - //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException - throw new RoleNotFoundParseException(input); - } - throw e; - } - } - - public static class RoleParseException extends IllegalArgumentException { - - private static final long serialVersionUID = -2451548379508062135L; - private final String input; - - /** - * Construct a new role parse exception - * - * @param input String input - */ - public RoleParseException(final @NonNull String input) { - this.input = input; - } - - /** - * Get the users input - * - * @return users input - */ - public final @NonNull String input() { - return this.input; - } - } - - - public static final class TooManyRolesFoundParseException extends RoleParseException { - - private static final long serialVersionUID = -8604082973199995006L; - - /** - * Construct a new role parse exception - * - * @param input String input - */ - public TooManyRolesFoundParseException(final @NonNull String input) { - super(input); - } - - @Override - public @NonNull String getMessage() { - return String.format("Too many roles found for '%s'.", input()); - } - } - - - public static final class RoleNotFoundParseException extends RoleParseException { - - private static final long serialVersionUID = 7931804739792920510L; - - /** - * Construct a new role parse exception - * - * @param input String input - */ - public RoleNotFoundParseException(final @NonNull String input) { - super(input); - } - - @Override - public @NonNull String getMessage() { - return String.format("Role not found for '%s'.", input()); - } + @Override + protected @NonNull DiscordRepository repository(@NonNull final CommandContext context) { + final MessageReceivedEvent event = context.get("MessageReceivedEvent"); + return new JDARoleRepository(event.getGuild()); } } diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/UserParser.java b/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/UserParser.java index 30e4728..c51701b 100644 --- a/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/UserParser.java +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/parsers/UserParser.java @@ -25,33 +25,29 @@ import cloud.commandframework.CommandComponent; import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.parser.ParserDescriptor; import cloud.commandframework.context.CommandContext; -import cloud.commandframework.context.CommandInput; -import java.util.Collections; -import java.util.List; +import cloud.commandframework.jda.repository.JDAUserRepository; import java.util.Set; -import java.util.concurrent.CompletionException; -import java.util.stream.Collectors; import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.exceptions.ErrorResponseException; -import net.dv8tion.jda.api.requests.ErrorResponse; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; -import org.jetbrains.annotations.NotNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.discord.legacy.parser.DiscordParserMode; +import org.incendo.cloud.discord.legacy.parser.DiscordUserParser; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; /** - * Command Argument for {@link User} + * Parser for {@link User}. * - * @param Command sender type + * @param command sender type * @since 2.0.0 */ @SuppressWarnings("unused") -public final class UserParser implements ArgumentParser { +@API(status = API.Status.STABLE, since = "2.0.0") +public final class UserParser extends DiscordUserParser { /** * Creates a new server parser. @@ -64,7 +60,7 @@ public final class UserParser implements ArgumentParser { */ @API(status = API.Status.STABLE, since = "2.0.0") public static @NonNull ParserDescriptor userParser( - final @NonNull Set modes, + final @NonNull Set modes, final @NonNull Isolation isolationLevel ) { return ParserDescriptor.of(new UserParser<>(modes, isolationLevel), User.class); @@ -81,15 +77,12 @@ public final class UserParser implements ArgumentParser { */ @API(status = API.Status.STABLE, since = "2.0.0") public static CommandComponent.@NonNull Builder userComponent( - final @NonNull Set modes, + final @NonNull Set modes, final @NonNull Isolation isolationLevel ) { return CommandComponent.builder().parser(userParser(modes, isolationLevel)); } - private final Set modes; - private final Isolation isolationLevel; - /** * Construct a new user parser. * @@ -97,219 +90,25 @@ public final class UserParser implements ArgumentParser { * @param isolationLevel isolation level to allow */ public UserParser( - final @NonNull Set modes, + final @NonNull Set modes, final @NonNull Isolation isolationLevel ) { - this.modes = modes; - this.isolationLevel = isolationLevel; - } - - /** - * Get the modes enabled on the parser - * - * @return Set of Modes - */ - public @NotNull Set getModes() { - return this.modes; - } - - - public enum ParserMode { - MENTION, - ID, - NAME + super(modes, isolationLevel); } - public enum Isolation { - GLOBAL, - GUILD + @Override + protected @NonNull DiscordRepository repository(final @NonNull CommandContext context) { + final MessageReceivedEvent event = context.get("MessageReceivedEvent"); + return new JDAUserRepository(event.getGuild(), this.isolation()); } @Override - public @NonNull ArgumentParseResult parse( - final @NonNull CommandContext commandContext, - final @NonNull CommandInput commandInput - ) { - final String input = commandInput.peekString(); - - if (!commandContext.contains("MessageReceivedEvent")) { + protected @Nullable ArgumentParseResult preProcess(@NonNull final CommandContext context) { + if (!context.contains("MessageReceivedEvent")) { return ArgumentParseResult.failure(new IllegalStateException( "MessageReceivedEvent was not in the command context." )); } - - final MessageReceivedEvent event = commandContext.get("MessageReceivedEvent"); - Exception exception = null; - - if (this.modes.contains(ParserMode.MENTION)) { - if (input.startsWith("<@") && input.endsWith(">")) { - final String id; - if (input.startsWith("<@!")) { - id = input.substring(3, input.length() - 1); - } else { - id = input.substring(2, input.length() - 1); - } - - try { - final ArgumentParseResult result = this.userFromId(event, input, Long.parseLong(id)); - commandInput.readString(); - return result; - } catch (final UserNotFoundParseException | NumberFormatException e) { - exception = e; - } - } else { - exception = new IllegalArgumentException( - String.format("Input '%s' is not a user mention.", input) - ); - } - } - - if (this.modes.contains(ParserMode.ID)) { - try { - final ArgumentParseResult result = this.userFromId(event, input, Long.parseLong(input)); - commandInput.readString(); - return result; - } catch (final UserNotFoundParseException | NumberFormatException e) { - exception = e; - } - } - - if (this.modes.contains(ParserMode.NAME)) { - final List users; - - if (this.isolationLevel == Isolation.GLOBAL) { - users = event.getJDA().getUsersByName(input, true); - } else if (event.isFromGuild()) { - users = event.getGuild().getMembers() - .stream() - .filter(member -> member.getEffectiveName().toLowerCase().startsWith(input)) - .map(Member::getUser) - .collect(Collectors.toList()); - } else if (event.getAuthor().getName().equalsIgnoreCase(input)) { - users = Collections.singletonList(event.getAuthor()); - } else { - users = Collections.emptyList(); - } - - if (users.isEmpty()) { - exception = new UserNotFoundParseException(input); - } else if (users.size() > 1) { - exception = new TooManyUsersFoundParseException(input); - } else { - commandInput.readString(); - return ArgumentParseResult.success(users.get(0)); - } - } - - assert exception != null; - return ArgumentParseResult.failure(exception); - } - - private @NonNull ArgumentParseResult userFromId( - final @NonNull MessageReceivedEvent event, - final @NonNull String input, - final @NonNull Long id - ) - throws UserNotFoundParseException, NumberFormatException { - final User user; - if (this.isolationLevel == Isolation.GLOBAL) { - User globalUser = event.getJDA().getUserById(id); - - if (globalUser == null) { // fallback if user is not cached - globalUser = event.getJDA().retrieveUserById(id).complete(); - } - - user = globalUser; - } else if (event.isFromGuild()) { - final Guild guild = event.getGuild(); - Member member = event.getGuild().getMemberById(id); - - try { - if (member == null) { // fallback if user is not cached - member = guild.retrieveMemberById(id).complete(); - } - - user = member.getUser(); - } catch (final CompletionException e) { - if (e.getCause().getClass().equals(ErrorResponseException.class) - && ((ErrorResponseException) e.getCause()).getErrorResponse() == ErrorResponse.UNKNOWN_USER) { - //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException - throw new UserNotFoundParseException(input); - } - throw e; - } - } else if (event.getAuthor().getIdLong() == id) { - user = event.getAuthor(); - } else { - user = null; - } - - if (user == null) { - throw new UserNotFoundParseException(input); - } else { - return ArgumentParseResult.success(user); - } - } - - - public static class UserParseException extends IllegalArgumentException { - - private final String input; - - /** - * Construct a new user parse exception - * - * @param input String input - */ - public UserParseException(final @NonNull String input) { - this.input = input; - } - - /** - * Get the users input - * - * @return Users input - */ - public final @NonNull String input() { - return this.input; - } - } - - - public static final class TooManyUsersFoundParseException extends UserParseException { - - - /** - * Construct a new user parse exception - * - * @param input String input - */ - public TooManyUsersFoundParseException(final @NonNull String input) { - super(input); - } - - @Override - public @NonNull String getMessage() { - return String.format("Too many users found for '%s'.", input()); - } - } - - - public static final class UserNotFoundParseException extends UserParseException { - - - /** - * Construct a new user parse exception - * - * @param input String input - */ - public UserNotFoundParseException(final @NonNull String input) { - super(input); - } - - @Override - public @NonNull String getMessage() { - return String.format("User not found for '%s'.", input()); - } + return null; } } diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDAChannelRepository.java b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDAChannelRepository.java new file mode 100644 index 0000000..f92eacc --- /dev/null +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDAChannelRepository.java @@ -0,0 +1,80 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.jda.repository; + +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.CompletionException; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.ErrorResponse; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.legacy.parser.DiscordChannelParser; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; + +/** + * Repository for JDA {@link MessageChannel message channels}. + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +public final class JDAChannelRepository implements DiscordRepository { + + private final Guild guild; + + /** + * Creates a new channel repository. + * + * @param guild guild to retrieve channels from + */ + public JDAChannelRepository(final @NonNull Guild guild) { + this.guild = Objects.requireNonNull(guild, "guild"); + } + + @Override + public @NonNull MessageChannel getById(final long id) { + try { + final MessageChannel channel = this.guild.getTextChannelById(id); + + if (channel == null) { + throw new DiscordChannelParser.ChannelNotFoundParseException(Long.toString(id)); + } + + return channel; + } catch (final CompletionException e) { + if (e.getCause().getClass().equals(ErrorResponseException.class) + && ((ErrorResponseException) e.getCause()).getErrorResponse() == ErrorResponse.UNKNOWN_CHANNEL) { + //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException + throw new DiscordChannelParser.ChannelNotFoundParseException(Long.toString(id)); + } + throw e; + } + } + + @Override + public @NonNull Collection<@NonNull TextChannel> getByName(final @NonNull String name) { + return this.guild.getTextChannelsByName(name, true); + } +} diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDAMemberRepository.java b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDAMemberRepository.java new file mode 100644 index 0000000..f7b25ff --- /dev/null +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDAMemberRepository.java @@ -0,0 +1,87 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.jda.repository; + +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.ErrorResponse; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.legacy.parser.DiscordMemberParser; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; + +/** + * Repository for JDA {@link Member members}. + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +public final class JDAMemberRepository implements DiscordRepository { + + private final Guild guild; + + /** + * Creates a new member repository. + * + * @param guild guild to retrieve members from + */ + public JDAMemberRepository(final @NonNull Guild guild) { + this.guild = Objects.requireNonNull(guild, "guild"); + } + + @Override + public @NonNull Member getById(final long id) { + try { + Member guildMember = this.guild.getMemberById(id); + + if (guildMember == null) { // fallback if member is not cached + guildMember = this.guild.retrieveMemberById(id).complete(); + } + + if (guildMember == null) { + throw new DiscordMemberParser.MemberNotFoundParseException(Long.toString(id)); + } else { + return guildMember; + } + } catch (final CompletionException e) { + if (e.getCause().getClass().equals(ErrorResponseException.class) + && ((ErrorResponseException) e.getCause()).getErrorResponse() == ErrorResponse.UNKNOWN_MEMBER) { + //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException + throw new DiscordMemberParser.MemberNotFoundParseException(Long.toString(id)); + } + throw e; + } + } + + @Override + public @NonNull Collection getByName(@NonNull final String name) { + return this.guild.getMembers() + .stream() + .filter(member -> member.getEffectiveName().toLowerCase().startsWith(name)) + .collect(Collectors.toList()); + } +} diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDARoleRepository.java b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDARoleRepository.java new file mode 100644 index 0000000..af9ddd0 --- /dev/null +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDARoleRepository.java @@ -0,0 +1,79 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.jda.repository; + +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.CompletionException; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.ErrorResponse; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.legacy.parser.DiscordRoleParser; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; + +/** + * Repository for JDA {@link Role roles}. + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +public final class JDARoleRepository implements DiscordRepository { + + private final Guild guild; + + /** + * Creates a new role repository. + * + * @param guild guild to retrieve roles from + */ + public JDARoleRepository(final @NonNull Guild guild) { + this.guild = Objects.requireNonNull(guild, "guild"); + } + + @Override + public @NonNull Role getById(final long id) { + try { + final Role role = this.guild.getRoleById(id); + + if (role == null) { + throw new DiscordRoleParser.RoleNotFoundParseException(Long.toString(id)); + } + + return role; + } catch (final CompletionException e) { + if (e.getCause().getClass().equals(ErrorResponseException.class) + && ((ErrorResponseException) e.getCause()).getErrorResponse() == ErrorResponse.UNKNOWN_ROLE) { + //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException + throw new DiscordRoleParser.RoleNotFoundParseException(Long.toString(id)); + } + throw e; + } + } + + @Override + public @NonNull Collection getByName(@NonNull final String name) { + return this.guild.getRolesByName(name, true); + } +} diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDAUserRepository.java b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDAUserRepository.java new file mode 100644 index 0000000..d284326 --- /dev/null +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/JDAUserRepository.java @@ -0,0 +1,112 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.jda.repository; + +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.ErrorResponse; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.legacy.parser.DiscordUserParser; +import org.incendo.cloud.discord.legacy.repository.DiscordRepository; + +/** + * Repository for JDA {@link User users}. + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +public final class JDAUserRepository implements DiscordRepository { + + private final Guild guild; + private final DiscordUserParser.Isolation isolation; + + /** + * Creates a new User repository. + * + * @param guild guild to retrieve users from + * @param isolation isolation + */ + public JDAUserRepository( + final @NonNull Guild guild, + final DiscordUserParser.@NonNull Isolation isolation + ) { + this.guild = Objects.requireNonNull(guild, "guild"); + this.isolation = Objects.requireNonNull(isolation, "isolation"); + } + + @Override + public @NonNull User getById(final long id) { + User user = null; + + if (this.isolation == DiscordUserParser.Isolation.GLOBAL) { + User globalUser = this.guild.getJDA().getUserById(id); + + if (globalUser == null) { // fallback if User is not cached + globalUser = this.guild.getJDA().retrieveUserById(id).complete(); + } + + user = globalUser; + } else { + Member member = this.guild.getMemberById(id); + + try { + if (member == null) { // fallback if member is not cached + member = this.guild.retrieveMemberById(id).complete(); + } + + user = member.getUser(); + } catch (final CompletionException e) { + if (e.getCause().getClass().equals(ErrorResponseException.class) + && ((ErrorResponseException) e.getCause()).getErrorResponse() == ErrorResponse.UNKNOWN_USER) { + //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException + throw new DiscordUserParser.UserNotFoundParseException(Long.toString(id)); + } + throw e; + } + } + + if (user == null) { + throw new DiscordUserParser.UserNotFoundParseException(Long.toString(id)); + } + return user; + } + + @Override + public @NonNull Collection getByName(@NonNull final String name) { + if (this.isolation == DiscordUserParser.Isolation.GLOBAL) { + return this.guild.getJDA().getUsersByName(name, true); + } + + return this.guild.getMembers() + .stream() + .filter(member -> member.getEffectiveName().toLowerCase().startsWith(name)) + .map(Member::getUser) + .collect(Collectors.toList()); + } +} diff --git a/cloud-jda/src/main/java/cloud/commandframework/jda/repository/package-info.java b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/package-info.java new file mode 100644 index 0000000..7ea261e --- /dev/null +++ b/cloud-jda/src/main/java/cloud/commandframework/jda/repository/package-info.java @@ -0,0 +1,4 @@ +/** + * JDA implementations of the Discord parsers. + */ +package cloud.commandframework.jda.repository;