From 979d1079c6978bab24027fbf5ce829d89239d6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Thu, 22 Oct 2020 10:51:33 +0200 Subject: [PATCH] :sparkles: Add a PircBotX implementation of cloud --- CHANGELOG.md | 1 + README.md | 7 +- .../pircbotx/CloudListenerAdapter.java | 99 ++++++++++ .../pircbotx/PircBotXCommandManager.java | 141 ++++++++++++++ .../pircbotx/arguments/UserArgument.java | 178 ++++++++++++++++++ .../pircbotx/arguments/package-info.java | 28 +++ .../pircbotx/package-info.java | 28 +++ scripts/dependencies.gradle | 2 + settings.gradle | 3 + 9 files changed, 484 insertions(+), 3 deletions(-) create mode 100644 cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/CloudListenerAdapter.java create mode 100644 cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/PircBotXCommandManager.java create mode 100644 cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/arguments/UserArgument.java create mode 100644 cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/arguments/package-info.java create mode 100644 cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/package-info.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f9fccb..22ec6b86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added CaptionKeys to cloud-bungee - Added BungeeCommandPreprocessor to cloud-bungee - Added named suggestion providers + - Added a PircBotX implementation ### Changed - Allow for combined presence flags, such that `-a -b -c` is equivalent to `-abc` diff --git a/README.md b/README.md index dc3cf547..e5a186c5 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,9 @@ command completion and suggestions. If using the annotation parsing system, you can create custom annotation mappers to use your own annotation bindings for command parsing, preprocessing, etc. -Cloud by default ships with implementations and mappings for the most common Minecraft server platforms and JDA and javacord for -Discord bots. The core -module allows you to use Cloud anywhere, simply by implementing the CommandManager for the platform of your choice. +Cloud by default ships with implementations and mappings for the most common Minecraft server platforms, JDA and javacord for +Discord bots and PircBotX for IRC. +The core module allows you to use Cloud anywhere, simply by implementing the CommandManager for the platform of your choice. The code is based on a (W.I.P) paper that can be found [here](https://github.com/Sauilitired/Sauilitired/blob/master/AS_2020_09_Commands.pdf). @@ -89,6 +89,7 @@ The code is based on a (W.I.P) paper that can be found [here](https://github.com - **cloud-minecraft/cloud-minecraft-extras**: Opinionated Extra Features for cloud-minecraft - **cloud-discord/cloud-jda**: JDA v4.2.0_209+ implementation of cloud - **cloud-discord/cloud-javacord**: Javacord v3.1.1+ implementation of cloud +- **cloud-irc/cloud-pircbotx**: PircBotX 2.0+ implementation of cloud ## links diff --git a/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/CloudListenerAdapter.java b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/CloudListenerAdapter.java new file mode 100644 index 00000000..b20aa876 --- /dev/null +++ b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/CloudListenerAdapter.java @@ -0,0 +1,99 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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.pircbotx; + +import cloud.commandframework.exceptions.ArgumentParseException; +import cloud.commandframework.exceptions.InvalidCommandSenderException; +import cloud.commandframework.exceptions.InvalidSyntaxException; +import cloud.commandframework.exceptions.NoPermissionException; +import cloud.commandframework.exceptions.NoSuchCommandException; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.pircbotx.hooks.ListenerAdapter; +import org.pircbotx.hooks.types.GenericMessageEvent; + +final class CloudListenerAdapter extends ListenerAdapter { + + private static final String MESSAGE_INVALID_SYNTAX = "Invalid Command Syntax. Correct command syntax is: "; + private static final String MESSAGE_NO_PERMS = "I'm sorry, but you do not have permission to perform this command. " + + "Please contact the server administrators if you believe that this is in error."; + private static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + + private final PircBotXCommandManager manager; + + CloudListenerAdapter(final @NonNull PircBotXCommandManager manager) { + this.manager = manager; + } + + @Override + public void onGenericMessage(final @NonNull GenericMessageEvent event) { + final String message = event.getMessage(); + if (!message.startsWith(this.manager.getCommandPrefix())) { + return; + } + final C sender = this.manager.getUserMapper().apply(event.getUser()); + manager.executeCommand(sender, message.substring(this.manager.getCommandPrefix().length())) + .whenComplete((commandResult, throwable) -> { + if (throwable == null) { + return; + } + + if (throwable instanceof InvalidSyntaxException) { + this.manager.handleException(sender, + InvalidSyntaxException.class, + (InvalidSyntaxException) throwable, (c, e) -> event.respondWith( + MESSAGE_INVALID_SYNTAX + this.manager.getCommandPrefix() + + ((InvalidSyntaxException) throwable).getCorrectSyntax() + ) + ); + } else if (throwable instanceof InvalidCommandSenderException) { + this.manager.handleException(sender, + InvalidCommandSenderException.class, + (InvalidCommandSenderException) throwable, (c, e) -> + event.respondWith(throwable.getMessage()) + ); + } else if (throwable instanceof NoPermissionException) { + this.manager.handleException(sender, + NoPermissionException.class, + (NoPermissionException) throwable, (c, e) -> + event.respondWith(MESSAGE_NO_PERMS) + ); + } else if (throwable instanceof NoSuchCommandException) { + this.manager.handleException(sender, + NoSuchCommandException.class, + (NoSuchCommandException) throwable, (c, e) -> + event.respondWith(MESSAGE_UNKNOWN_COMMAND) + ); + } else if (throwable instanceof ArgumentParseException) { + this.manager.handleException(sender, ArgumentParseException.class, + (ArgumentParseException) throwable, (c, e) -> event.respondWith( + "Invalid Command Argument: " + throwable.getCause().getMessage() + ) + ); + } else { + event.respondWith(throwable.getMessage()); + } + }); + } + +} diff --git a/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/PircBotXCommandManager.java b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/PircBotXCommandManager.java new file mode 100644 index 00000000..d9f244c0 --- /dev/null +++ b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/PircBotXCommandManager.java @@ -0,0 +1,141 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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.pircbotx; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.CommandTree; +import cloud.commandframework.captions.Caption; +import cloud.commandframework.captions.FactoryDelegatingCaptionRegistry; +import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator; +import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.internal.CommandRegistrationHandler; +import cloud.commandframework.meta.CommandMeta; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.pircbotx.PircBotX; +import org.pircbotx.User; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Command manager implementation for PircBotX 2.0 + * + * @param Command sender type + * @since 1.1.0 + */ +public class PircBotXCommandManager extends CommandManager { + + /** + * Meta key for accessing the {@link org.pircbotx.PircBotX} instance from a + * {@link cloud.commandframework.context.CommandContext} instance + */ + public static final String PIRCBOTX_META_KEY = "__internal_pircbotx__"; + /** + * Variables: {input} + */ + public static final Caption ARGUMENT_PARSE_FAILURE_USER_KEY = Caption.of("argument.parse.failure.use"); + /** + * Default caption for {@link #ARGUMENT_PARSE_FAILURE_USER_KEY} + */ + public static final String ARGUMENT_PARSE_FAILURE_USER = "'{input}' is not a valid user"; + + private final String commandPrefix; + private final BiFunction permissionFunction; + private final Function userMapper; + private final PircBotX pircBotX; + + /** + * Create a new command manager instance + * + * @param pircBotX PircBotX instance. This is used to register the + * {@link org.pircbotx.hooks.ListenerAdapter} that will forward commands to the + * command dispatcher + * @param commandExecutionCoordinator Execution coordinator instance. The coordinator is in charge of executing incoming + * commands. Some considerations must be made when picking a suitable execution coordinator + * for your platform. For example, an entirely asynchronous coordinator is not suitable + * when the parsers used in that particular platform are not thread safe. If you have + * commands that perform blocking operations, however, it might not be a good idea to + * use a synchronous execution coordinator. In most cases you will want to pick between + * {@link CommandExecutionCoordinator#simpleCoordinator()} and + * {@link AsynchronousCommandExecutionCoordinator} + * @param commandRegistrationHandler Command registration handler. This will get called every time a new command is + * registered to the command manager. This may be used to forward command registration + * @param permissionFunction Function used to determine whether or not a sender is permitted to use a certain + * command. The first input is the sender of the command, and the second parameter is + * the the command permission string. The return value should be {@code true} if the + * sender is permitted to use the command, else {@code false} + * @param userMapper Function that maps {@link User users} to the custom command sender type + * @param commandPrefix The prefix that must be applied to all commands for the command to be valid + */ + public PircBotXCommandManager( + final @NonNull PircBotX pircBotX, + final @NonNull Function<@NonNull CommandTree, @NonNull CommandExecutionCoordinator> commandExecutionCoordinator, + final @NonNull CommandRegistrationHandler commandRegistrationHandler, + final @NonNull BiFunction permissionFunction, + final @NonNull Function userMapper, + final @NonNull String commandPrefix + ) { + super(commandExecutionCoordinator, commandRegistrationHandler); + this.pircBotX = pircBotX; + this.permissionFunction = permissionFunction; + this.commandPrefix = commandPrefix; + this.userMapper = userMapper; + this.pircBotX.getConfiguration().getListenerManager().addListener(new CloudListenerAdapter<>(this)); + if (this.getCaptionRegistry() instanceof FactoryDelegatingCaptionRegistry) { + ((FactoryDelegatingCaptionRegistry) this.getCaptionRegistry()).registerMessageFactory( + ARGUMENT_PARSE_FAILURE_USER_KEY, + (caption, user) -> ARGUMENT_PARSE_FAILURE_USER + ); + } + this.registerCommandPreProcessor(context -> context.getCommandContext().store(PIRCBOTX_META_KEY, pircBotX)); + } + + @Override + public final boolean hasPermission( + @NonNull final C sender, + @NonNull final String permission + ) { + return this.permissionFunction.apply(sender, permission); + } + + @Override + public final @NonNull CommandMeta createDefaultCommandMeta() { + return CommandMeta.simple().build(); + } + + /** + * Get the command prefix. A message should be classed as a command if, and only if, it is prefixed + * with this prefix + * + * @return Command prefix + */ + public final @NonNull String getCommandPrefix() { + return this.commandPrefix; + } + + @NonNull final Function getUserMapper() { + return this.userMapper; + } + +} diff --git a/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/arguments/UserArgument.java b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/arguments/UserArgument.java new file mode 100644 index 00000000..c3c92ddc --- /dev/null +++ b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/arguments/UserArgument.java @@ -0,0 +1,178 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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.pircbotx.arguments; + +import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.captions.CaptionVariable; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.exceptions.parsing.NoInputProvidedException; +import cloud.commandframework.exceptions.parsing.ParserException; +import cloud.commandframework.pircbotx.PircBotXCommandManager; +import io.leangen.geantyref.TypeToken; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.pircbotx.PircBotX; +import org.pircbotx.User; +import org.pircbotx.exception.DaoException; + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.function.BiFunction; + +/** + * {@link CommandArgument Command argument} that parses PircBotX {@link User users} + * + * @param Command sender type + * @since 1.1.0 + */ +public final class UserArgument extends CommandArgument { + + private UserArgument( + final boolean required, + final @NonNull String name, + final @Nullable BiFunction, String, List> suggestionsProvider + ) { + super( + required, + name, + new UserArgumentParser<>(), + "", + TypeToken.get(User.class), + suggestionsProvider, + new LinkedList<>() + ); + } + + /** + * Create a new user argument builder + * + * @param name Argument name + * @param Command sender type + * @return Builder instance + */ + public static @NonNull Builder newBuilder(final @NonNull String name) { + return new Builder<>(name); + } + + /** + * Create a new required user argument + * + * @param name Argument name + * @param Command sender type + * @return Argument instance + */ + public static @NonNull CommandArgument of(final @NonNull String name) { + return UserArgument.newBuilder(name).asRequired().build(); + } + + /** + * Create a optional user argument + * + * @param name Argument name + * @param Command sender type + * @return Argument instance + */ + public static @NonNull CommandArgument optional(final @NonNull String name) { + return UserArgument.newBuilder(name).asOptional().build(); + } + + + public static final class Builder extends CommandArgument.Builder { + + private Builder( + final @NonNull String name + ) { + super( + TypeToken.get(User.class), + name + ); + } + + @Override + public @NonNull CommandArgument<@NonNull C, @NonNull User> build() { + return new UserArgument<>( + this.isRequired(), + this.getName(), + this.getSuggestionsProvider() + ); + } + + } + + + public static final class UserArgumentParser implements ArgumentParser { + + @Override + public @NonNull ArgumentParseResult<@NonNull User> parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final Queue<@NonNull String> inputQueue + ) { + final String input = inputQueue.peek(); + if (input == null) { + return ArgumentParseResult.failure(new NoInputProvidedException( + UserArgumentParser.class, + commandContext + )); + } + final PircBotX pircBotX = commandContext.get(PircBotXCommandManager.PIRCBOTX_META_KEY); + final User user; + try { + user = pircBotX.getUserChannelDao().getUser(input); + } catch (final DaoException exception) { + return ArgumentParseResult.failure( + new UserParseException( + commandContext, + input + ) + ); + } + return ArgumentParseResult.success(user); + } + + } + + + public static final class UserParseException extends ParserException { + + private UserParseException( + final @NonNull CommandContext context, + final @NonNull String input + ) { + super( + UserArgumentParser.class, + context, + PircBotXCommandManager.ARGUMENT_PARSE_FAILURE_USER_KEY, + CaptionVariable.of( + "input", + input + ) + ); + } + + } + +} diff --git a/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/arguments/package-info.java b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/arguments/package-info.java new file mode 100644 index 00000000..165d3c03 --- /dev/null +++ b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/arguments/package-info.java @@ -0,0 +1,28 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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. +// + +/** + * PircBotX specific arguments + */ +package cloud.commandframework.pircbotx.arguments; diff --git a/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/package-info.java b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/package-info.java new file mode 100644 index 00000000..bea9be86 --- /dev/null +++ b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/package-info.java @@ -0,0 +1,28 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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. +// + +/** + * PircBotX 2.0 implementation of Cloud + */ +package cloud.commandframework.pircbotx; diff --git a/scripts/dependencies.gradle b/scripts/dependencies.gradle index 847f57b8..2b3feddb 100644 --- a/scripts/dependencies.gradle +++ b/scripts/dependencies.gradle @@ -14,6 +14,8 @@ ext { 'adventure-api' : '4.1.1', 'paper-api' : '1.15.2-R0.1-SNAPSHOT', 'velocity-api' : '1.1.0-SNAPSHOT', + // IRC DEPENDENCIES + 'pircbotx' : '2.1', // TEST DEPENDENCIES 'jupiter-engine': '5.7.0', 'jhm' : '1.25.2' diff --git a/settings.gradle b/settings.gradle index 5d7c5b46..da2585bb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,9 @@ include(':cloud-sponge') project(':cloud-sponge').projectDir = file('cloud-minecraft/cloud-sponge') include(':cloud-velocity') project(':cloud-velocity').projectDir = file('cloud-minecraft/cloud-velocity') +// IRC Modules +include(':cloud-pircbotx') +project(':cloud-pircbotx').projectDir = file('cloud-irc/cloud-pircbotx') // // Example Modules //