diff --git a/.gitignore b/.gitignore index ccab28b..6cbf983 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ !src/**/build/ gradle.properties +/server/ +/template/ +/plugins/ + # Ignore Gradle GUI configuration gradle-app.setting @@ -30,3 +34,11 @@ dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties .mvn/wrapper/maven-wrapper.jar + +# Bot internal generated data +/config/ +/logs/ +/out/ +*.db +*.sqlite +/wait.sh diff --git a/README.md b/README.md index 282cf76..92b3084 100644 --- a/README.md +++ b/README.md @@ -1 +1,19 @@ -Bot and spigot plugin used for the dev cord plugin jam +Bot and spigot plugin used for the dev cord plugin jamCreator + +## Start Arguments for paper +`-Dpluginjam.port` + +Port of the velocity server api + +`-Dpluginjam.name` + +Name of the server + +`-Djavalin.port` + +Port of the javalin api of the server + +## Start Arguments for velocity +`-Djavalin.port` + +Port of the javalin api of the server diff --git a/api/build.gradle.kts b/api/build.gradle.kts deleted file mode 100644 index aa757a5..0000000 --- a/api/build.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -plugins { - java -} - -group = "de.chojo" -version = "1.0" - -repositories { - mavenCentral() -} - -dependencies { - testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") -} - -tasks.getByName("test") { - useJUnitPlatform() -} \ No newline at end of file diff --git a/bot/build.gradle.kts b/bot/build.gradle.kts index aa757a5..5fa234d 100644 --- a/bot/build.gradle.kts +++ b/bot/build.gradle.kts @@ -1,4 +1,5 @@ plugins { + id("com.github.johnrengelman.shadow") version "7.1.2" java } @@ -6,14 +7,60 @@ group = "de.chojo" version = "1.0" repositories { + maven("https://eldonexus.de/repository/maven-public") + maven("https://eldonexus.de/repository/maven-proxies") + maven("https://m2.dv8tion.net/releases") mavenCentral() } dependencies { - testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + implementation(project(":plugin-api")) + + // discord + implementation("de.chojo", "cjda-util", "2.7.8+alpha.22-DEV") { + exclude(module = "opus-java") + } + implementation("io.javalin", "javalin-bundle", "4.4.0") + implementation("net.lingala.zip4j", "zip4j", "2.11.2") + + // database + implementation("de.chojo.sadu", "sadu-queries", "1.2.0") + implementation("de.chojo.sadu", "sadu-updater", "1.2.0") + implementation("de.chojo.sadu", "sadu-datasource", "1.2.0") + implementation("de.chojo.sadu", "sadu-postgresql", "1.2.0") + implementation("org.postgresql", "postgresql", "42.3.3") + + // Logging + implementation("org.slf4j", "slf4j-api", "2.0.3") + implementation("org.apache.logging.log4j", "log4j-core", "2.19.0") + implementation("org.apache.logging.log4j", "log4j-slf4j2-impl", "2.19.0") + implementation("club.minnced", "discord-webhooks", "0.7.5") + + // unit testing + testImplementation(platform("org.junit:junit-bom:5.9.0")) + testImplementation("org.junit.jupiter", "junit-jupiter") } -tasks.getByName("test") { - useJUnitPlatform() -} \ No newline at end of file +tasks { + processResources { + from(sourceSets.main.get().resources.srcDirs) { + filesMatching("version") { + expand( + "version" to project.version + ) + } + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + } + + build { + dependsOn(shadowJar) + } + + shadowJar { + mergeServiceFiles() + manifest { + attributes(mapOf("Main-Class" to "de.chojo.gamejam.Bot")) + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/Bot.java b/bot/src/main/java/de/chojo/gamejam/Bot.java new file mode 100644 index 0000000..412ca5a --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/Bot.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam; + +import de.chojo.gamejam.api.Api; +import de.chojo.gamejam.commands.jamadmin.JamAdmin; +import de.chojo.gamejam.commands.register.Register; +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.commands.serveradmin.ServerAdmin; +import de.chojo.gamejam.commands.settings.Settings; +import de.chojo.gamejam.commands.team.Team; +import de.chojo.gamejam.commands.unregister.Unregister; +import de.chojo.gamejam.commands.vote.Votes; +import de.chojo.gamejam.configuration.Configuration; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.access.Teams; +import de.chojo.gamejam.server.ServerService; +import de.chojo.gamejam.util.LogNotify; +import de.chojo.jdautil.interactions.dispatching.InteractionHub; +import de.chojo.jdautil.localization.ILocalizer; +import de.chojo.jdautil.localization.Localizer; +import de.chojo.sadu.databases.PostgreSql; +import de.chojo.sadu.datasource.DataSourceCreator; +import de.chojo.sadu.exceptions.ExceptionTransformer; +import de.chojo.sadu.mapper.PostgresqlMapper; +import de.chojo.sadu.mapper.RowMapperRegistry; +import de.chojo.sadu.updater.QueryReplacement; +import de.chojo.sadu.updater.SqlUpdater; +import de.chojo.sadu.wrapper.QueryBuilderConfig; +import net.dv8tion.jda.api.interactions.DiscordLocale; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder; +import net.dv8tion.jda.api.sharding.ShardManager; +import net.dv8tion.jda.api.utils.MemberCachePolicy; +import org.slf4j.Logger; + +import javax.security.auth.login.LoginException; +import javax.sql.DataSource; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.sql.SQLException; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Bot { + private static final Logger log = getLogger(Bot.class); + private static final Thread.UncaughtExceptionHandler EXCEPTION_HANDLER = + (t, e) -> log.error(LogNotify.NOTIFY_ADMIN, "An uncaught exception occured in " + t.getName() + "-" + t.getId() + ".", e); + + private static final Bot instance; + + static { + instance = new Bot(); + } + + private Configuration configuration; + private DataSource dataSource; + private ILocalizer localizer; + private ShardManager shardManager; + private Guilds guilds; + private ServerService serverService; + + private static ThreadFactory createThreadFactory(String name) { + return createThreadFactory(new ThreadGroup(name)); + } + + private static ThreadFactory createThreadFactory(ThreadGroup group) { + return r -> { + var thread = new Thread(group, r, group.getName()); + thread.setUncaughtExceptionHandler(EXCEPTION_HANDLER); + return thread; + }; + } + + public static void main(String[] args) { + try { + instance.start(); + } catch (Exception e) { + log.error("Critical error occured during bot startup", e); + } + } + + private ExecutorService createExecutor(String name) { + return Executors.newCachedThreadPool(createThreadFactory(name)); + } + + private ScheduledExecutorService createScheduledExecutor(String name, int size) { + return Executors.newScheduledThreadPool(10, createThreadFactory(name)); + } + + private ExecutorService createExecutor(int threads, String name) { + return Executors.newFixedThreadPool(threads, createThreadFactory(name)); + } + + public void start() throws IOException, SQLException, LoginException { + configuration = Configuration.create(); + + initDb(); + + initServer(); + + initBot(); + + buildLocale(); + + buildCommands(); + + Api.create(configuration, shardManager, guilds); + + } + + private void buildLocale() { + localizer = Localizer.builder(DiscordLocale.ENGLISH_US) + .addLanguage(DiscordLocale.GERMAN) + .withLanguageProvider(guild -> Optional.ofNullable(guilds.guild(guild).settings().locale()) + .map(DiscordLocale::from)) + .build(); + } + + private void buildCommands() { + InteractionHub.builder(shardManager) + .withLocalizer(localizer) + .withCommands(new JamAdmin(guilds), + new Register(guilds), + new Settings(guilds), + new Team(guilds), + new Unregister(guilds), + new Votes(guilds), + new Server(guilds, serverService, configuration), + new ServerAdmin(guilds, serverService)) + .withPagination(builder -> builder.withLocalizer(localizer) + .withCache(cache -> cache.expireAfterAccess(30, TimeUnit.MINUTES))) + .withMenuService(builder -> builder.withLocalizer(localizer) + .withCache(cache -> cache.expireAfterAccess(30, TimeUnit.MINUTES))) + .withModalService(builder -> builder.withLocalizer(localizer)) + .build(); + } + + private void initBot() { + shardManager = DefaultShardManagerBuilder.createDefault(configuration.baseSettings().token()) + .enableIntents( + GatewayIntent.GUILD_MEMBERS, + GatewayIntent.DIRECT_MESSAGES, + GatewayIntent.GUILD_MESSAGES) + .setMemberCachePolicy(MemberCachePolicy.DEFAULT) + .setEventPool(Executors.newScheduledThreadPool(5, createThreadFactory("Event Worker"))) + .build(); + RestAction.setDefaultFailure(throwable -> log.error("Unhandled exception occured: ", throwable)); + serverService.inject(new Teams(dataSource, guilds, shardManager)); + serverService.syncVelocity(); + } + + private void initDb() throws IOException, SQLException { + var mapperRegistry = new RowMapperRegistry(); + mapperRegistry.register(PostgresqlMapper.getDefaultMapper()); + QueryBuilderConfig.setDefault(QueryBuilderConfig.builder() + .withExceptionHandler(err -> log.error(ExceptionTransformer.prettyException(err), err)) + .withExecutor(createExecutor("DataWorker")) + .rowMappers(mapperRegistry) + .build()); + + dataSource = DataSourceCreator.create(PostgreSql.get()) + .configure(config -> { + config.host(configuration.database().host()) + .port(configuration.database().port()) + .database(configuration.database().database()) + .user(configuration.database().user()) + .password(configuration.database().password()); + }) + .create() + .forSchema(configuration.database().schema()) + .build(); + + SqlUpdater.builder(dataSource, PostgreSql.get()) + .setReplacements(new QueryReplacement("gamejam", configuration.database().schema())) + .setSchemas(configuration.database().schema()) + .execute(); + + guilds = new Guilds(dataSource); + } + + private void initServer() throws IOException { + serverService = ServerService.create(createScheduledExecutor("Server ping", 1), configuration); + + var templateDir = Path.of(configuration.serverTemplate().templateDir()); + var serverDir = Path.of(configuration.serverManagement().serverDir()); + var pluginDir = Path.of(configuration.plugins().pluginDir()); + + Files.createDirectories(templateDir); + Files.createDirectories(serverDir); + Files.createDirectories(pluginDir); + + Path wait = Path.of("wait.sh"); + Files.copy(getClass().getClassLoader().getResourceAsStream("wait.sh"), + wait, StandardCopyOption.REPLACE_EXISTING); + try { + Files.setPosixFilePermissions(wait, Set.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); + } catch (UnsupportedOperationException e) { + log.error("Use linux..."); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/api/Api.java b/bot/src/main/java/de/chojo/gamejam/api/Api.java new file mode 100644 index 0000000..103f2f1 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/api/Api.java @@ -0,0 +1,109 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.api; + +import de.chojo.gamejam.api.exception.InterruptException; +import de.chojo.gamejam.api.v1.Teams; +import de.chojo.gamejam.api.v1.Users; +import de.chojo.gamejam.configuration.Configuration; +import de.chojo.gamejam.data.access.Guilds; +import io.javalin.Javalin; +import io.javalin.http.Context; +import io.javalin.plugin.openapi.OpenApiOptions; +import io.javalin.plugin.openapi.OpenApiPlugin; +import io.javalin.plugin.openapi.ui.ReDocOptions; +import io.javalin.plugin.openapi.ui.SwaggerOptions; +import net.dv8tion.jda.api.sharding.ShardManager; +import org.slf4j.Logger; + +import javax.servlet.http.HttpServletResponse; +import java.util.stream.Collectors; + +import static io.javalin.apibuilder.ApiBuilder.path; +import static org.slf4j.LoggerFactory.getLogger; + +public class Api { + private static final Logger log = getLogger(Api.class); + private Configuration configuration; + private final ShardManager shardManager; + private final Guilds guilds; + private Javalin app; + + public static Api create(Configuration configuration, ShardManager shardManager, Guilds guilds) { + var api = new Api(configuration, shardManager, guilds); + api.build(); + return api; + } + + public Api(Configuration configuration, ShardManager shardManager, Guilds guilds) { + this.configuration = configuration; + this.shardManager = shardManager; + this.guilds = guilds; + } + + private void build() { + app = Javalin.create(config -> { + config.registerPlugin(getConfiguredOpenApiPlugin()); + config.accessManager((handler, ctx, routeRoles) -> { + if(ctx.path().startsWith("/swagger") || ctx.path().startsWith("/redoc")){ + handler.handle(ctx); + return; + } + + var token = ctx.req.getHeader("authorization"); + if (token == null) { + ctx.status(HttpServletResponse.SC_UNAUTHORIZED).result("Please provde a valid token in the authorization header."); + } else if (!token.equals(configuration.api().token())) { + ctx.status(HttpServletResponse.SC_UNAUTHORIZED).result("Unauthorized"); + } else { + handler.handle(ctx); + } + }); + config.requestLogger((ctx, executionTimeMs) -> { + log.debug("{}: {} in {}ms\nHeaders:\n{}\nBody:\n{}", + ctx.method(), ctx.path(), executionTimeMs, + headers(ctx), + ctx.body().substring(0, Math.min(100, ctx.body().length()))); + }); + }).start(configuration.api().host(), configuration.api().port()); + + app.exception(InterruptException.class, (exception, ctx) -> { + ctx.status(exception.status()).result(exception.getMessage()); + }); + + app.routes(() -> { + path("api/v1", () -> { + var users = new Users(shardManager, guilds); + users.routes(); + var teams = new Teams(shardManager, guilds); + teams.routes(); + }); + }); + } + + private String headers(Context context) { + return context.headerMap() + .entrySet() + .stream() + .map(e -> String.format("%s=%s", e.getKey(), e.getValue())) + .collect(Collectors.joining("\n ")); + } + + private static OpenApiPlugin getConfiguredOpenApiPlugin() { + var info = new io.swagger.v3.oas.models.info.Info().version("1.0").description("User API"); + OpenApiOptions options = new OpenApiOptions(info) + .activateAnnotationScanningFor("io.javalin.example.java") + .path("/swagger-docs") // endpoint for OpenAPI json + .swagger(new SwaggerOptions("/swagger-ui")) // endpoint for swagger-ui + .reDoc(new ReDocOptions("/redoc")) // endpoint for redoc + .defaultDocumentation(doc -> { + doc.header("authorization", String.class) + .result("401"); + }); + return new OpenApiPlugin(options); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/api/exception/Interrupt.java b/bot/src/main/java/de/chojo/gamejam/api/exception/Interrupt.java new file mode 100644 index 0000000..db1820d --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/api/exception/Interrupt.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.api.exception; + +import io.javalin.http.HttpCode; +import org.jetbrains.annotations.Contract; + +public final class Interrupt { + private Interrupt() { + } + + public static InterruptException create(String message, HttpCode httpCode) { + return new InterruptException(message, httpCode); + } + + @Contract("_ -> fail") + public static InterruptException notFound(String entity) { + return create(String.format("%s not found.", entity), HttpCode.NOT_FOUND); + } + @Contract(" -> fail") + public static InterruptException forbidden() { + return create("Endpoint forbidden", HttpCode.FORBIDDEN); + } + + @Contract("null,_ -> fail") + public static void assertNotFound(Object object, String entity) throws InterruptException { + assertNotFound(object == null, entity); + } + + @Contract("true,_ -> fail") + public static void assertNotFound(boolean failed, String entity) throws InterruptException { + if (failed) throw notFound(entity); + } + + @Contract("false -> fail") + public static void assertForbidden(boolean success) throws InterruptException { + if (!success) throw forbidden(); + } + + @Contract(" -> fail") + public static InterruptException noJam() { + return create("No current or upcoming jam", HttpCode.NOT_FOUND); + } + + @Contract("true -> fail") + public static void assertNoJam(boolean failed) throws InterruptException { + if (failed) { + throw noJam(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/api/exception/InterruptException.java b/bot/src/main/java/de/chojo/gamejam/api/exception/InterruptException.java new file mode 100644 index 0000000..4a41d71 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/api/exception/InterruptException.java @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.api.exception; + +import io.javalin.http.HttpCode; + +public class InterruptException extends Exception { + private final HttpCode status; + + public InterruptException(String message, HttpCode status) { + super(message); + this.status = status; + } + + public InterruptException(HttpCode status) { + super(status.getMessage()); + this.status = status; + } + + public int status() { + return status.getStatus(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/api/v1/Teams.java b/bot/src/main/java/de/chojo/gamejam/api/v1/Teams.java new file mode 100644 index 0000000..7a0ff9a --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/api/v1/Teams.java @@ -0,0 +1,219 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.api.v1; + +import de.chojo.gamejam.api.exception.Interrupt; +import de.chojo.gamejam.api.exception.InterruptException; +import de.chojo.gamejam.api.v1.wrapper.LeaderToken; +import de.chojo.gamejam.api.v1.wrapper.TeamProfile; +import de.chojo.gamejam.api.v1.wrapper.UserProfile; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import io.javalin.http.Context; +import io.javalin.http.HttpCode; +import io.javalin.plugin.openapi.annotations.OpenApi; +import io.javalin.plugin.openapi.annotations.OpenApiContent; +import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.dsl.OpenApiBuilder; +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.requests.RestAction; +import net.dv8tion.jda.api.sharding.ShardManager; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +import static io.javalin.apibuilder.ApiBuilder.get; +import static io.javalin.apibuilder.ApiBuilder.path; +import static io.javalin.apibuilder.ApiBuilder.put; + +public class Teams { + private final ShardManager shardManager; + private final Guilds guilds; + + public Teams(ShardManager shardManager, Guilds guilds) { + this.shardManager = shardManager; + this.guilds = guilds; + } + + private static void putName(Context ctx) { +// change name + } + + private static void putGithub(Context ctx) { +// change github link + } + + private static void putLeader(Context ctx) { +// change leader + } + + private static void putDescription(Context ctx) { +// change description + } + + private void authCheck(Context ctx) throws InterruptException { + String token = ctx.header("leader-authorization"); + if (token == null) { + throw Interrupt.create("Set the \"leader-authorization\" header to get access.", HttpCode.UNAUTHORIZED); + } + + Interrupt.assertForbidden("123456".equals(token)); + new LeaderToken(0, token, true); + ctx.status(HttpCode.OK).result("Valid"); +// returns ok if token is valid + } + + private void authorize(Context ctx) { + ctx.status(HttpCode.OK).result("123456"); +// returns a modification token or forbidden if id is not leader + } + + public void routes() { + path("teams", () -> { + get("{guild-id}", OpenApiBuilder.documented(OpenApiBuilder.document() + .operation(operation -> { + operation.summary("Get all teams on a guild"); + }).json("200", TeamProfile[].class), + this::getGuildTeams)); + + path("{guild-id}/{team-id}", () -> { + get("member", OpenApiBuilder.documented(OpenApiBuilder.document() + .operation(operation -> { + operation.summary("Get the member list of a team on a guild"); + }).json("200", UserProfile[].class), + this::getGuildTeamMembers)); + get("profile", OpenApiBuilder.documented(OpenApiBuilder.document() + .operation(operation -> { + operation.summary("Get the profile of a team on a guild"); + }).json("200", TeamProfile.class), + this::getTeamProfile)); + + path("leader", () -> { + get("auth/{user-id}", OpenApiBuilder.documented(OpenApiBuilder.document() + .operation(operation -> { + operation.summary("Get the leader token when the user id matched the leader id") + .description(""" + The returned token stays valid until the leader of the team changes. + A new request will always yield the same token as long as the leader stays. + The token has to be send via the "leader-authorization" header.""".stripIndent()); + }) + .json("200", LeaderToken.class) + .result("403"), + this::authorize)); + + get("auth/check", OpenApiBuilder.documented(OpenApiBuilder.document() + .header("leader-authorization", String.class) + .operation(operation -> { + operation.summary("Checks if the provided authorization header is still valid"); + }).json("200", LeaderToken.class) + .result("403"), this::authCheck)); + + put("name", OpenApiBuilder.documented(OpenApiBuilder.document() + .header("leader-authorization", String.class) + .operation(operation -> { + operation.summary("Changes the name of the team."); + }).json("202", TeamProfile.class) + .result("403") + .result("409"), Teams::putName)); + + put("projecturl", OpenApiBuilder.documented(OpenApiBuilder.document() + .header("leader-authorization", String.class) + .operation(operation -> { + operation.summary("Changes the github link of the team."); + }).json("202", TeamProfile.class) + .result("403"), Teams::putGithub)); + + put("leader", OpenApiBuilder.documented(OpenApiBuilder.document() + .header("leader-authorization", String.class) + .operation(operation -> { + operation.summary("Changes the leader of the team."); + }).json("202", TeamProfile.class) + .result("403"), Teams::putLeader)); + + put("description", OpenApiBuilder.documented(OpenApiBuilder.document() + .header("leader-authorization", String.class) + .operation(operation -> { + operation.summary("Changes the description of the team."); + }).json("202", TeamProfile.class) + .result("403"), Teams::putDescription)); + }); + }); + }); + } + + private LeaderPath resolveTeamPath(Context ctx) throws InterruptException { + var guild = shardManager.getGuildById(ctx.pathParamAsClass("guild-id", Long.class).get()); + Interrupt.assertNotFound(guild, "Guild"); + var team = guilds.guild(guild).teams().byId(ctx.pathParamAsClass("team-id", Integer.class).get()); + Interrupt.assertNotFound(team.isEmpty(), "Team"); + return new LeaderPath(team.get(), guild); + } + + private Guild resolveGuild(Context ctx) throws InterruptException { + var guild = shardManager.getGuildById(ctx.pathParamAsClass("guild-id", Long.class).get()); + Interrupt.assertNotFound(guild, "Guild"); + return guild; + } + + @NotNull + private Member getMember(Guild guild, long userId) throws InterruptException { + return completeEntity(guild.retrieveMemberById(userId), "User"); + } + + private User getUser(long userId) throws InterruptException { + return completeEntity(shardManager.retrieveUserById(userId), "User"); + } + + @NotNull + private T completeEntity(RestAction action, String entity) throws InterruptException { + try { + var member = action.complete(); + Interrupt.assertNotFound(member, entity); + return member; + } catch (RuntimeException ignored) { + throw Interrupt.notFound(entity); + } + } + + @OpenApi(responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = TeamProfile[].class)) + }) + private void getGuildTeams(Context ctx) throws InterruptException { + var guild = resolveGuild(ctx); + var jamGuild = guilds.guild(guild); + var jam = jamGuild.jams().nextOrCurrent(); + Interrupt.assertNoJam(jam.isEmpty()); + ctx.status(HttpCode.OK).json(jam.get().teams().teams().stream().map(TeamProfile::build).toList()); + } + + @OpenApi(responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = UserProfile[].class)) + }) + private void getGuildTeamMembers(Context ctx) throws InterruptException { + var teamPath = resolveTeamPath(ctx); + var teamMember = teamPath.team.member(); + List members = new ArrayList<>(); + for (var member : teamMember) { + members.add(getMember(teamPath.guild(), member.member().getIdLong())); + } + ctx.status(HttpCode.OK).json(members.stream().map(UserProfile::build).toList()); + } + + @OpenApi(responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = TeamProfile.class)) + }) + private void getTeamProfile(Context ctx) throws InterruptException { + var teamPath = resolveTeamPath(ctx); + ctx.status(HttpCode.OK).json(TeamProfile.build(teamPath.team())); + } + + private record LeaderPath(Team team, Guild guild) { + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/api/v1/Users.java b/bot/src/main/java/de/chojo/gamejam/api/v1/Users.java new file mode 100644 index 0000000..05e1ebb --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/api/v1/Users.java @@ -0,0 +1,133 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.api.v1; + +import de.chojo.gamejam.api.exception.Interrupt; +import de.chojo.gamejam.api.exception.InterruptException; +import de.chojo.gamejam.api.v1.wrapper.GuildProfile; +import de.chojo.gamejam.api.v1.wrapper.TeamProfile; +import de.chojo.gamejam.api.v1.wrapper.UserProfile; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.JamGuild; +import io.javalin.http.Context; +import io.javalin.http.HttpCode; +import io.javalin.plugin.openapi.annotations.HttpMethod; +import io.javalin.plugin.openapi.annotations.OpenApi; +import io.javalin.plugin.openapi.annotations.OpenApiContent; +import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.dsl.OpenApiBuilder; +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.requests.RestAction; +import net.dv8tion.jda.api.sharding.ShardManager; +import org.jetbrains.annotations.NotNull; + +import static io.javalin.apibuilder.ApiBuilder.get; +import static io.javalin.apibuilder.ApiBuilder.path; + +public class Users { + private final ShardManager shardManager; + private final Guilds guilds; + + public Users(ShardManager shardManager, Guilds guilds) { + this.shardManager = shardManager; + this.guilds = guilds; + } + + public void routes() { + path("users", () -> { + path("{user-id}", () -> { + path("guilds", () -> { + get(OpenApiBuilder.documented(OpenApiBuilder.document() + .operation(operation -> { + operation.summary("Get the mututal guilds with the user"); + }).json("200", GuildProfile[].class), + this::getMututalGuilds)); + }); + path("{guild-id}", () -> { + get("team", OpenApiBuilder.documented(OpenApiBuilder.document() + .operation(operation -> { + operation.summary("Get the team of a user on a guild"); + }).json("200", TeamProfile.class), + this::getUserTeam)); + + get("profile", OpenApiBuilder.documented(OpenApiBuilder.document() + .operation(operation -> { + operation.summary("Get the user profile of a user on a guild"); + }).json("200", UserProfile.class), + this::getUserProfile)); + }); + }); + }); + } + + private GuildPath resolveGuildPath(Context ctx) throws InterruptException { + var guild = shardManager.getGuildById(ctx.pathParamAsClass("guild-id", Long.class).get()); + Interrupt.assertNotFound(guild, "Guild"); + var member = getMember(guild, ctx.pathParamAsClass("user-id", Long.class).get()); + return new GuildPath(member, guild); + } + + @NotNull + private Member getMember(Guild guild, long userId) throws InterruptException { + return completeEntity(guild.retrieveMemberById(userId), "User"); + } + + private User getUser(long userId) throws InterruptException { + return completeEntity(shardManager.retrieveUserById(userId), "User"); + } + + @NotNull + private T completeEntity(RestAction action, String entity) throws InterruptException { + try { + var member = action.complete(); + Interrupt.assertNotFound(member, entity); + return member; + } catch (RuntimeException ignored) { + throw Interrupt.notFound(entity); + } + } + + private void getMututalGuilds(Context ctx) throws InterruptException { + var user = getUser(ctx.pathParamAsClass("user-id", Long.class).get()); + + var guildProfiles = shardManager.getMutualGuilds(user).stream().map(GuildProfile::build).toList(); + ctx.status(HttpCode.OK).json(guildProfiles); + } + + @OpenApi(path = "/api/v1/users/{user-id}/{guild-id}/team", + method = HttpMethod.GET, + responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = TeamProfile.class)) + }) + private void getUserTeam(Context ctx) throws InterruptException { + var guildPath = resolveGuildPath(ctx); + var guild = guilds.guild(guildPath.guild()); + + var jam = guild.jams().nextOrCurrent(); + Interrupt.assertNoJam(jam.isEmpty()); + + var team = jam.get().teams().byMember(guildPath.member()); + + Interrupt.assertNotFound(team.isEmpty(), "Team"); + + ctx.status(HttpCode.OK).json(TeamProfile.build(team.get())); + } + + @OpenApi(path = "/api/v1/users/{user-id}/{guild-id}/profile", + method = HttpMethod.GET, + responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = UserProfile.class)) + }) + private void getUserProfile(Context ctx) throws InterruptException { + ctx.status(HttpCode.OK).json(UserProfile.build(resolveGuildPath(ctx).member())); + } + + private record GuildPath(Member member, Guild guild) { + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/GuildProfile.java b/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/GuildProfile.java new file mode 100644 index 0000000..d0d9237 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/GuildProfile.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.api.v1.wrapper; + +import net.dv8tion.jda.api.entities.Guild; + +public record GuildProfile(String name, String iconUrl, long idLong) { + public static GuildProfile build(Guild guild) { + var name = guild.getName(); + var iconUrl = guild.getIconUrl(); + var idLong = guild.getIdLong(); + return new GuildProfile(name, iconUrl, idLong); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/LeaderToken.java b/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/LeaderToken.java new file mode 100644 index 0000000..5e71df1 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/LeaderToken.java @@ -0,0 +1,10 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.api.v1.wrapper; + +public record LeaderToken(int teamId, String token, boolean valid) { +} diff --git a/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/TeamProfile.java b/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/TeamProfile.java new file mode 100644 index 0000000..c42e47c --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/TeamProfile.java @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.api.v1.wrapper; + +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; + +public record TeamProfile(int id, String name, String description, long leaderId, String projectUrl) { + + public static TeamProfile build(Team team) { + return new TeamProfile(team.id(), team.meta().name(), "Cool description", team.meta().leader(), "https://github.com/devcordde/plugin-jam-bot"); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/UserProfile.java b/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/UserProfile.java new file mode 100644 index 0000000..8ea89c5 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/api/v1/wrapper/UserProfile.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.api.v1.wrapper; + +import net.dv8tion.jda.api.entities.Member; + +public record UserProfile(String name, String tag, String iconUrl, long idLong) { + public static UserProfile build(Member guild) { + var name = guild.getEffectiveName(); + var tag = guild.getUser().getAsTag(); + var iconUrl = guild.getEffectiveAvatarUrl(); + var idLong = guild.getIdLong(); + return new UserProfile(name, tag, iconUrl, idLong); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/JamAdmin.java b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/JamAdmin.java new file mode 100644 index 0000000..80be89d --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/JamAdmin.java @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.jamadmin; + +import de.chojo.gamejam.commands.jamadmin.handler.Create; +import de.chojo.gamejam.commands.jamadmin.handler.jam.JamEnd; +import de.chojo.gamejam.commands.jamadmin.handler.jam.JamStart; +import de.chojo.gamejam.commands.jamadmin.handler.votes.ChangeVotes; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.Argument; +import de.chojo.jdautil.interactions.slash.Group; +import de.chojo.jdautil.interactions.slash.Slash; +import de.chojo.jdautil.interactions.slash.SubCommand; +import de.chojo.jdautil.interactions.slash.provider.SlashCommand; + +public class JamAdmin extends SlashCommand { + + + public JamAdmin(Guilds guilds) { + super(Slash.of("jamadmin", "command.jamadmin.description") + .adminCommand() + .subCommand(SubCommand.of("create", "command.jamadmin.create.description") + .handler(new Create(guilds)) + .argument(Argument.text("topic", "command.jamadmin.create.options.topic.description") + .asRequired()) + .argument(Argument.text("tagline", "command.jamadmin.create.options.tagline.description") + .asRequired()) + .argument(Argument.text("timezone", "command.jamadmin.create.options.timezone.description") + .asRequired() + .withAutoComplete()) + .argument(Argument.text("registerstart", "command.jamadmin.create.options.registerstart.description") + .asRequired()) + .argument(Argument.text("registerend", "command.jamadmin.create.options.registerend.description") + .asRequired()) + .argument(Argument.text("jamstart", "command.jamadmin.create.options.jamstart.description") + .asRequired()) + .argument(Argument.text("jamend", "command.jamadmin.create.options.jamend.description") + .asRequired())) + .group(Group.of("jam", "command.jamadmin.jam.description") + .subCommand(SubCommand.of("start", "command.jamadmin.jam.start.description") + .handler(new JamStart(guilds))) + .subCommand(SubCommand.of("end", "command.jamadmin.jam.end.description") + .handler(new JamEnd(guilds)) + .argument(Argument.bool("confirm", "command.jamadmin.jam.end.options.confirm.description")))) + .group(Group.of("votes", "command.jamadmin.votes.description") + .subCommand(SubCommand.of("open", "command.jamadmin.votes.open.description") + .handler(new ChangeVotes(guilds, true, "command.jamadmin.votes.message.opened"))) + .subCommand(SubCommand.of("close", "command.jamadmin.votes.close.description") + .handler(new ChangeVotes(guilds, false, "command.jamadmin.votes.message.closed")))) + .build()); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/Create.java b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/Create.java new file mode 100644 index 0000000..10331ce --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/Create.java @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.jamadmin.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.wrapper.jam.JamCreator; +import de.chojo.gamejam.data.dao.guild.jams.jam.JamTimes; +import de.chojo.gamejam.data.wrapper.jam.TimeFrame; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; + +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.stream.Collectors; + +public class Create implements SlashHandler { + public static final String PATTERN = "yyyy.MM.dd HH:mm"; + private static final DateTimeFormatter DATE_PARSER = DateTimeFormatter.ofPattern(PATTERN); + private final Guilds guilds; + + public Create(Guilds guilds) { + this.guilds = guilds; + } + + private ZonedDateTime parseTime(String time, ZoneId zoneId) throws DateTimeException { + var parsed = LocalDateTime.from(DATE_PARSER.parse(time)); + return ZonedDateTime.ofInstant(parsed, zoneId.getRules().getOffset(parsed), zoneId); + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var topic = String.join("\n", event.getOption("topic").getAsString(), + event.getOption("tagline", "", OptionMapping::getAsString)); + ZoneId timezone; + try { + timezone = ZoneId.of(event.getOption("timezone").getAsString()); + } catch (DateTimeException e) { + event.reply(context.localize("error.invalidtimezone")).setEphemeral(true).queue(); + return; + } + + var jamBuilder = JamCreator.create() + .setTopic(topic); + try { + var registerStart = parseTime(event.getOption("registerstart").getAsString(), timezone); + var registerEnd = parseTime(event.getOption("registerend").getAsString(), timezone); + var jamStart = parseTime(event.getOption("jamstart").getAsString(), timezone); + var jamEnd = parseTime(event.getOption("jamend").getAsString(), timezone); + var times = new JamTimes(timezone, new TimeFrame(registerStart, registerEnd), new TimeFrame(jamStart, jamEnd)); + jamBuilder.setTimes(times); + } catch (DateTimeException e) { + event.reply(context.localize("error.invalidrimeformat", Replacement.create("FORMAT", PATTERN))) + .setEphemeral(true).queue(); + return; + } + + guild.jams().create(jamBuilder.build()); + event.reply(context.localize("command.jamadmin.create.message.created")).setEphemeral(true).queue(); + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + if ("timezone".equals(event.getFocusedOption().getName())) { + var value = event.getFocusedOption().getValue().toLowerCase(Locale.ROOT); + var choices = ZoneId.getAvailableZoneIds().stream() + .filter(zone -> zone.toLowerCase(Locale.ROOT).contains(value)) + .limit(25) + .map(zone -> new Command.Choice(zone, zone)) + .collect(Collectors.toList()); + event.replyChoices(choices).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamEnd.java b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamEnd.java new file mode 100644 index 0000000..6f2aed1 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamEnd.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.jamadmin.handler.jam; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public final class JamEnd implements SlashHandler { + private final Guilds guilds; + + public JamEnd(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + if (!event.getOption("confirm").getAsBoolean()) { + event.reply(context.localize("error.noconfirm")).setEphemeral(true).queue(); + return; + } + + + guilds.guild(event).jams().activeJam() + .ifPresentOrElse( + jam -> { + jam.state().finish(); + event.reply(context.localize("command.jamadmin.jam.end.message.ended")).setEphemeral(true) + .queue(); + }, + () -> event.reply(context.localize("error.noactivejam")).setEphemeral(true).queue()); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamStart.java b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamStart.java new file mode 100644 index 0000000..32296d6 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamStart.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.jamadmin.handler.jam; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public final class JamStart implements SlashHandler { + private final Guilds guilds; + + public JamStart(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var jams = guilds.guild(event).jams(); + var currJam = jams.activeJam(); + if (currJam.isPresent()) { + event.reply(context.localize("error.alreadyActive")).setEphemeral(true).queue(); + return; + } + var next = jams.nextOrCurrent(); + if (next.isPresent()) { + next.get().state().active(true); + event.reply(context.localize("command.start.message.activated")) + .setEphemeral(true) + .queue(); + + } + event.reply(context.localize("error.noupcomingjam")).setEphemeral(true) + .queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/votes/ChangeVotes.java b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/votes/ChangeVotes.java new file mode 100644 index 0000000..f98bc32 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/votes/ChangeVotes.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.jamadmin.handler.votes; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public final class ChangeVotes implements SlashHandler { + private final Guilds guilds; + private final boolean voting; + private final String content; + + public ChangeVotes(Guilds guilds, boolean voting, String content) { + this.guilds = guilds; + this.voting = voting; + this.content = content; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + guilds.guild(event).jams().activeJam() + .ifPresentOrElse( + jam -> { + jam.state().voting(voting); + event.reply(context.localize(content)).queue(); + }, + () -> event.reply(context.localize("error.noactivejam")).setEphemeral(true).queue()); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/register/Register.java b/bot/src/main/java/de/chojo/gamejam/commands/register/Register.java new file mode 100644 index 0000000..8f68e62 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/register/Register.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.register; + +import de.chojo.gamejam.commands.register.handler.Handler; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.Slash; +import de.chojo.jdautil.interactions.slash.provider.SlashCommand; + +public class Register extends SlashCommand { + public Register(Guilds guilds) { + super(Slash.of("register", "command.register.description") + .command(new Handler(guilds))); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/register/handler/Handler.java b/bot/src/main/java/de/chojo/gamejam/commands/register/handler/Handler.java new file mode 100644 index 0000000..f594483 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/register/handler/Handler.java @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.register.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.utils.TimeFormat; + +import java.time.ZonedDateTime; + +public class Handler implements SlashHandler { + private final Guilds guilds; + + public Handler(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var optJam = guild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.noupcomingjam")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + var times = jam.times(); + + if (!times.registration().contains(ZonedDateTime.now())) { + if (times.registration().start().isAfter(ZonedDateTime.now())) { + event.reply(context.localize("command.register.message.notyet", + Replacement.create("TIMESTAMP", TimeFormat.DATE_TIME_LONG.format(times.registration() + .start())))) + .setEphemeral(true) + .queue(); + return; + } + event.reply(context.localize("command.register.message.notanymore")).setEphemeral(true).queue(); + return; + } + + if (jam.registrations().contains(event.getMember().getIdLong())) { + event.reply(context.localize("command.register.message.alreadyregistered")).setEphemeral(true).queue(); + return; + } + + jam.register(event.getMember()); + var settings = guild.jamSettings(); + var role = event.getGuild().getRoleById(settings.jamRole()); + if (role != null) { + event.getGuild().addRoleToMember(event.getMember(), role).queue(); + } + event.reply(context.localize("command.register.message.registered", + Replacement.create("TIMESTAMP", TimeFormat.DATE_TIME_LONG.format(times.jam().start())))) + .setEphemeral(true) + .queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/Server.java b/bot/src/main/java/de/chojo/gamejam/commands/server/Server.java new file mode 100644 index 0000000..7118960 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/Server.java @@ -0,0 +1,142 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server; + +import de.chojo.gamejam.commands.server.configure.MaxPlayers; +import de.chojo.gamejam.commands.server.configure.Message; +import de.chojo.gamejam.commands.server.configure.SpectatorOverflow; +import de.chojo.gamejam.commands.server.configure.Whitelist; +import de.chojo.gamejam.commands.server.download.DownloadPluginData; +import de.chojo.gamejam.commands.server.plugins.Install; +import de.chojo.gamejam.commands.server.plugins.Uninstall; +import de.chojo.gamejam.commands.server.process.Console; +import de.chojo.gamejam.commands.server.process.Log; +import de.chojo.gamejam.commands.server.process.Restart; +import de.chojo.gamejam.commands.server.process.Start; +import de.chojo.gamejam.commands.server.process.Status; +import de.chojo.gamejam.commands.server.process.Stop; +import de.chojo.gamejam.commands.server.system.Delete; +import de.chojo.gamejam.commands.server.system.Setup; +import de.chojo.gamejam.commands.server.upload.Plugin; +import de.chojo.gamejam.commands.server.upload.UploadPluginData; +import de.chojo.gamejam.commands.server.upload.World; +import de.chojo.gamejam.configuration.Configuration; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.Argument; +import de.chojo.jdautil.interactions.slash.Group; +import de.chojo.jdautil.interactions.slash.Slash; +import de.chojo.jdautil.interactions.slash.SubCommand; +import de.chojo.jdautil.interactions.slash.provider.SlashProvider; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Optional; + +public class Server implements SlashProvider { + private final Guilds guilds; + private final ServerService serverService; + private final Configuration configuration; + + public Server(Guilds guilds, ServerService serverService, Configuration configuration) { + this.guilds = guilds; + this.serverService = serverService; + this.configuration = configuration; + } + + @Override + public Slash slash() { + return Slash.of("server", "command.server.description") + .adminCommand() + .group(Group.of("process", "command.server.process.description") + .subCommand(SubCommand.of("start", "command.server.process.start.description") + .handler(new Start(this))) + .subCommand(SubCommand.of("stop", "command.server.process.stop.description") + .handler(new Stop(this))) + .subCommand(SubCommand.of("restart", "command.server.process.restart.description") + .handler(new Restart(this))) + .subCommand(SubCommand.of("console", "command.server.process.console.description") + .handler(new Console(this)) + .argument(Argument.text("command", "command.server.process.console.options.command.description").asRequired())) + .subCommand(SubCommand.of("status", "command.server.process.status.description") + .handler(new Status(this))) + .subCommand(SubCommand.of("log", "command.server.process.log.description") + .handler(new Log(this)))) + .group(Group.of("system", "command.server.system.description") + .subCommand(SubCommand.of("setup", "command.server.system.setup.description") + .handler(new Setup(this))) + .subCommand(SubCommand.of("delete", "command.server.system.delete.description") + .handler(new Delete(this)))) + .group(Group.of("upload", "command.server.upload.description") + .subCommand(SubCommand.of("world", "command.server.upload.world.description") + .handler(new World(this)) + .argument(Argument.text("url", "command.server.upload.world.options.url.description")) + .argument(Argument.attachment("file", "command.server.upload.world.options.file.description"))) + .subCommand(SubCommand.of("plugin", "command.server.upload.plugin.description") + .handler(new Plugin(this)) + .argument(Argument.attachment("file", "command.server.upload.plugin.options.file.description") + .asRequired())) + .subCommand(SubCommand.of("plugindata", "command.server.upload.plugindata.description") + .handler(new UploadPluginData(this, guilds, serverService)) + .argument(Argument.text("path", "command.server.upload.plugindata.options.path.description") + .asRequired() + .withAutoComplete()) + .argument(Argument.attachment("file", "command.server.upload.plugindata.options.file.description") + .asRequired()))) + .group(Group.of("download", "command.server.download.description") + .subCommand(SubCommand.of("plugindata", "command.server.download.plugindata.description") + .handler(new DownloadPluginData(this, guilds, serverService)) + .argument(Argument.text("path", "command.server.download.plugindata.options.path.description") + .asRequired() + .withAutoComplete()))) + .group(Group.of("plugins", "command.server.plugins.description") + .subCommand(SubCommand.of("install", "command.server.plugins.install.description") + .handler(new Install(configuration, this)) + .argument(Argument.text("plugin", "command.server.plugins.install.options.plugin.description") + .asRequired() + .withAutoComplete())) + .subCommand(SubCommand.of("uninstall", "command.server.plugins.uninstall.description") + .handler(new Uninstall(this, configuration, guilds, serverService)) + .argument(Argument.text("plugin", "command.server.plugins.uninstall.options.plugin.description").asRequired() + .withAutoComplete()) + .argument(Argument.bool("deletedata", "command.server.plugins.uninstall.options.deletedata.description") + .asRequired()))) + .group(Group.of("configure", "command.server.configure.description") + .subCommand(SubCommand.of("message", "command.server.configure.message.description") + .handler(new Message(this))) + .subCommand(SubCommand.of("maxplayers", "command.server.configure.maxplayers.description") + .handler(new MaxPlayers(this)) + .argument(Argument.integer("amount", "command.server.configure.maxplayers.options.amount.description").asRequired())) + .subCommand(SubCommand.of("spectatoroverflow", "command.server.configure.spectatoroverflow.description") + .handler(new SpectatorOverflow(this)) + .argument(Argument.bool("state", "command.server.configure.spectatoroverflow.options.state.description").asRequired())) + .subCommand(SubCommand.of("whitelist", "command.server.configure.whitelist.description") + .handler(new Whitelist(this)) + .argument(Argument.bool("state", "command.server.configure.whitelist.options.state.description").asRequired()))) + .build(); + } + + public Optional getServer(SlashCommandInteractionEvent event, EventContext context) { + var optJam = guilds.guild(event).jams().activeJam(); + + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return Optional.empty(); + } + + var jam = optJam.get(); + var optTeam = jam.teams().byMember(event.getUser()); + + if (optTeam.isEmpty()) { + event.reply(context.localize("error.noteam")).setEphemeral(true).queue(); + return Optional.empty(); + } + + return Optional.ofNullable(serverService.get(optTeam.get())); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/MaxPlayers.java b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/MaxPlayers.java new file mode 100644 index 0000000..8008c8f --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/MaxPlayers.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.configure; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.util.Futures; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.slf4j.LoggerFactory.getLogger; + +public class MaxPlayers implements SlashHandler { + private static final Logger log = getLogger(MaxPlayers.class); + private final Server server; + + public MaxPlayers(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + + var teamServer = optServer.get(); + var request = teamServer.requestBuilder("v1/config/maxplayers") + .POST(HttpRequest.BodyPublishers.ofString(String.valueOf(event.getOption("amount").getAsInt()))) + .build(); + + if (!teamServer.running()) { + event.reply(context.localize("error.servernotrunning")).queue(); + return; + } + + teamServer.http().sendAsync(request, HttpResponse.BodyHandlers.discarding()) + .whenComplete(Futures.whenComplete(res -> event.reply("command.server.configure.maxplayers.message.success").queue(), + err -> log.error("Failed to send request.", err)));; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Message.java b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Message.java new file mode 100644 index 0000000..22799a1 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Message.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.configure; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.modals.handler.ModalHandler; +import de.chojo.jdautil.modals.handler.TextInputHandler; +import de.chojo.jdautil.util.Futures; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import org.slf4j.Logger; + +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Message implements SlashHandler { + private static final Logger log = getLogger(Message.class); + private final Server server; + + public Message(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + + var teamServer = optServer.get(); + + if (!teamServer.running()) { + event.reply(context.localize("error.servernotrunning")).queue(); + return; + } + + context.registerModal(ModalHandler.builder(context.localize("command.server.configure.message.message.modal.title")) + .addInput(TextInputHandler.builder("message", context.localize("command.server.configure.message.message.modal.input.message.label"), TextInputStyle.PARAGRAPH) + .withPlaceholder(context.localize("command.server.configure.message.message.modal.input.message.placeholder")) + .build()) + .withHandler(modalEvent -> { + var content = modalEvent.getValues().get(0).getAsString(); + var request = teamServer.requestBuilder("v1/config/message") + .POST(HttpRequest.BodyPublishers.ofString(content)) + .build(); + teamServer.http().sendAsync(request, HttpResponse.BodyHandlers.discarding()) + .whenComplete(Futures.whenComplete(res -> modalEvent.reply(context.localize("command.server.configure.message.message.success")).queue(), + err -> log.error("Failed to send request.", err))); + }) + .build()); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/SpectatorOverflow.java b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/SpectatorOverflow.java new file mode 100644 index 0000000..b7a987f --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/SpectatorOverflow.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.configure; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.util.Futures; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.slf4j.LoggerFactory.getLogger; + +public class SpectatorOverflow implements SlashHandler { + private static final Logger log = getLogger(SpectatorOverflow.class); + private final Server server; + + public SpectatorOverflow(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + + var teamServer = optServer.get(); + + if (!teamServer.running()) { + event.reply(context.localize("error.servernotrunning")).queue(); + return; + } + + var request = teamServer.requestBuilder("v1/config/spectatoroverflow") + .POST(HttpRequest.BodyPublishers.ofString(String.valueOf(event.getOption("state").getAsBoolean()))) + .build(); + teamServer.http().sendAsync(request, HttpResponse.BodyHandlers.discarding()) + .whenComplete(Futures.whenComplete(res -> event.reply(context.localize("command.server.configure.spectatoroverflow.message.success")).queue(), + err -> log.error("Failed to send request.", err)));; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Whitelist.java b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Whitelist.java new file mode 100644 index 0000000..8568d95 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Whitelist.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.configure; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.util.Futures; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Whitelist implements SlashHandler { + private static final Logger log = getLogger(Whitelist.class); + private final Server server; + + public Whitelist(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + + var teamServer = optServer.get(); + + if (!teamServer.running()) { + event.reply(context.localize("error.servernotrunning")).queue(); + return; + } + + var request = teamServer.requestBuilder("v1/config/whitelist") + .POST(HttpRequest.BodyPublishers.ofString(String.valueOf(event.getOption("state").getAsBoolean()))) + .build(); + teamServer.http().sendAsync(request, HttpResponse.BodyHandlers.discarding()) + .whenComplete(Futures.whenComplete(res -> event.reply(context.localize("command.server.configure.whitelist.message.success")).queue(), + err -> log.error("Failed to send request.", err)));; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/download/DownloadPluginData.java b/bot/src/main/java/de/chojo/gamejam/commands/server/download/DownloadPluginData.java new file mode 100644 index 0000000..584c901 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/download/DownloadPluginData.java @@ -0,0 +1,174 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.download; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.guild.jams.Jam; +import de.chojo.gamejam.server.ServerService; +import de.chojo.gamejam.util.TempFile; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.util.Choice; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.ErrorResponse; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.FileUpload; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.exception.ZipException; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.slf4j.LoggerFactory.getLogger; + +public class DownloadPluginData implements SlashHandler { + private static final Logger log = getLogger(DownloadPluginData.class); + private final Server server; + private final Guilds guilds; + private final ServerService serverService; + + public DownloadPluginData(Server server, Guilds guilds, ServerService serverService) { + this.server = server; + this.guilds = guilds; + this.serverService = serverService; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + var teamServer = optServer.get(); + + var path = event.getOption("path").getAsString(); + + if (path.contains("..")) { + event.reply(context.localize("error.invalidpath")).queue(); + return; + } + + var pluginFile = teamServer.plugins().resolve(path); + + if (pluginFile.toFile().isFile()) { + // No download from plugin root + if (pluginFile.getParent().equals(teamServer.plugins())) { + event.reply(context.localize("error.invalidpath")).queue(); + return; + } + event.replyFiles(FileUpload.fromData(pluginFile, pluginFile.toFile().getName())).queue(); + return; + } + + event.reply(context.localize("command.server.download.downloadplugindata.message.zipping")).queue(); + Path tempFile; + try { + tempFile = TempFile.createPath("download", "zip"); + try (var zip = new ZipFile(tempFile.toFile())) { + zip.addFolder(pluginFile.toFile()); + } + tempFile.toFile().deleteOnExit(); + } catch (ZipException e) { + log.error("Failed to zip date", e); + event.getHook().editOriginal(context.localize("command.server.download.downloadplugindata.message.fail.zip")).queue(); + return; + } catch (IOException e) { + log.error("Failed to create zip file", e); + event.getHook().editOriginal(context.localize("command.server.download.downloadplugindata.message.fail.tempfile")).queue(); + return; + } + event.getHook().editOriginal(context.localize("command.server.download.downloadplugindata.message.success")) + .setFiles(FileUpload.fromData(tempFile, pluginFile.toFile().getName() + ".zip")) + .queue(RestAction.getDefaultSuccess(), err -> { + if (err instanceof ErrorResponseException response) { + if (response.getErrorResponse() == ErrorResponse.FILE_UPLOAD_MAX_SIZE_EXCEEDED) { + event.getHook().editOriginal(context.localize("command.server.download.downloadplugindata.message.fail.filetolarge")).queue(); + } + } + }); + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var optServer = guilds.guild(event).jams().activeJam() + .map(Jam::teams) + .flatMap(teams -> teams.byMember(event.getUser())) + .map(serverService::get); + if (optServer.isEmpty()) return; + var server = optServer.get(); + + var option = event.getFocusedOption(); + if ("path".equals(option.getName())) { + var plugins = server.plugins(); + var currPath = option.getValue(); + if (currPath.contains("..")) { + event.replyChoices().queue(); + return; + } + var split = currPath.split("/"); + + // Root dir + if (split.length == 1 && !currPath.endsWith("/")) { + var currValue = split[0].toLowerCase(); + var files = files(plugins).stream() + .filter(File::isDirectory) + .filter(file -> file.getName().toLowerCase().startsWith(currValue)) + .limit(25) + .map(line -> fileName(plugins, line)); + event.replyChoices(Choice.toStringChoice(files)).queue(); + return; + } + + if (!currPath.endsWith("/")) { + split = Arrays.copyOfRange(split, 0, split.length - 1); + } + + var path = plugins; + + for (var s : split) { + path = path.resolve(s); + } + + if (currPath.endsWith("/")) { + var files = files(path).stream() + .limit(25) + .map(line -> fileName(plugins, line)); + event.replyChoices(Choice.toStringChoice(files)).queue(); + return; + } + split = currPath.split("/"); + var currValue = currPath.split("/")[split.length - 1].toLowerCase(); + + var files = files(path).stream() + .filter(file -> file.getName().toLowerCase().startsWith(currValue)) + .limit(24) + .map(line -> fileName(plugins, line)) + .collect(Collectors.toCollection(ArrayList::new)); + + event.replyChoices(Choice.toStringChoice(files)).queue(); + } + } + + private List files(Path path) { + return Arrays.stream(path.toFile().listFiles()).toList(); + } + + private String fileName(Path strip, File file) { + if (file.isDirectory()) { + return (file.toPath() + "/").replace(strip.toString(), "").replaceAll("^/", ""); + } + return file.toPath().toString().replace(strip.toString(), "").replaceAll("^/", ""); + + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/plugins/Install.java b/bot/src/main/java/de/chojo/gamejam/commands/server/plugins/Install.java new file mode 100644 index 0000000..08aa310 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/plugins/Install.java @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.plugins; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.configuration.Configuration; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.util.Choice; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Install implements SlashHandler { + private static final Logger log = getLogger(Install.class); + private final Configuration configuration; + private final Server server; + + public Install(Configuration configuration, Server server) { + this.configuration = configuration; + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + var teamServer = optServer.get(); + + var pluginName = event.getOption("plugin").getAsString(); + var optPlugin = configuration.plugins().byName(pluginName); + if (optPlugin.isEmpty()) { + event.reply(context.localize("error.pluginnotfound")).queue(); + return; + } + var plugin = optPlugin.get(); + + var pluginPath = teamServer.plugins().resolve(plugin.toFile().getName()); + pluginPath.toFile().delete(); + try { + Files.createSymbolicLink(pluginPath, plugin.toAbsolutePath()); + } catch (IOException e) { + event.reply(context.localize("command.server.plugins.install.message.fail")).queue(); + log.error("Could not install plugin", e); + return; + } + event.reply(context.localize("command.server.plugins.install.message.success", Replacement.create("NAME", pluginName))).queue(); + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var option = event.getFocusedOption(); + if ("plugin".equals(option.getName())) { + var currValue = option.getValue().toLowerCase(); + if (currValue.isEmpty()) { + event.replyChoices(Choice.toStringChoice(configuration.plugins().pluginNames().stream().limit(25))) + .queue(); + return; + } + var stream = configuration.plugins().pluginNames().stream() + .filter(name -> name.toLowerCase().startsWith(currValue)) + .limit(25); + event.replyChoices(Choice.toStringChoice(stream)).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/plugins/Uninstall.java b/bot/src/main/java/de/chojo/gamejam/commands/server/plugins/Uninstall.java new file mode 100644 index 0000000..8280872 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/plugins/Uninstall.java @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.plugins; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.configuration.Configuration; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.guild.jams.Jam; +import de.chojo.gamejam.server.ServerService; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.util.Choice; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.io.File; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Uninstall implements SlashHandler { + private final Server server; + private final Configuration configuration; + private final Guilds guilds; + private final ServerService serverService; + + public Uninstall(Server server, Configuration configuration, Guilds guilds, ServerService serverService) { + this.server = server; + this.configuration = configuration; + this.guilds = guilds; + this.serverService = serverService; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + var teamServer = optServer.get(); + + var pluginName = event.getOption("plugin").getAsString(); + var optPlugin = configuration.plugins().byName(pluginName); + if (optPlugin.isEmpty()) { + event.reply(context.localize("error.pluginnotfound")).queue(); + return; + } + var plugin = optPlugin.get(); + + var pluginPath = teamServer.plugins().resolve(plugin.toFile().getName()); + pluginPath.toFile().delete(); + + if (event.getOption("deletedata").getAsBoolean()) { + var pluginDir = teamServer.plugins().resolve(pluginName); + teamServer.deleteDirectory(pluginDir); + event.reply(context.localize("command.server.plugins.uninstall.message.success.pluginanddata")).queue(); + } else { + event.reply(context.localize("command.server.plugins.uninstall.message.success.plugin")).queue(); + } + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var optServer = guilds.guild(event).jams().activeJam() + .map(Jam::teams) + .flatMap(teams -> teams.byMember(event.getUser())) + .map(serverService::get); + if (optServer.isEmpty()) return; + + var option = event.getFocusedOption(); + if ("plugin".equals(option.getName())) { + var allowedPlugins = configuration.plugins().pluginFiles().stream() + .map(File::getName) + .collect(Collectors.toSet()); + + var installedPlugins = Stream.of(optServer.get().plugins().toFile().listFiles(File::isFile)) + .filter(plugin -> allowedPlugins.contains(plugin.getName())) + .map(file -> file.getName().replace(".jar", "")) + .toList(); + + var currValue = option.getValue().toLowerCase(); + + if (currValue.isEmpty()) { + event.replyChoices(Choice.toStringChoice(installedPlugins)) + .queue(); + return; + } + + var stream = installedPlugins.stream() + .filter(name -> name.toLowerCase().startsWith(currValue)) + .limit(25); + event.replyChoices(Choice.toStringChoice(stream)).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Console.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Console.java new file mode 100644 index 0000000..583cf28 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Console.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.process; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class Console implements SlashHandler { + private final Server server; + + public Console(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + var teamServer = optServer.get(); + var command = event.getOption("command").getAsString(); + + if (command.startsWith("stop") || command.startsWith("restart")) { + event.reply(context.localize("command.server.process.console.message.notexecutable")).queue(); + return; + } + teamServer.send(command); + event.reply(context.localize("command.server.process.console.message.executed")).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java new file mode 100644 index 0000000..60d0d89 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.process; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.utils.FileUpload; + +import java.io.IOException; +import java.nio.file.Files; + +public class Log implements SlashHandler { + private final Server server; + + public Log(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if(optServer.isEmpty())return; + var teamServer = optServer.get(); + var logFile = teamServer.logFile(); + String content; + try { + content = Files.readString(logFile); + } catch (IOException e) { + content = ""; + } + content = content.substring(Math.max(content.length() - 1950, 0)); + event.reply("```log%n%s%n```".formatted(content)) + .addFiles(FileUpload.fromData(logFile, "latest.log")) + .queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Restart.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Restart.java new file mode 100644 index 0000000..487b31b --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Restart.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.process; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class Restart implements SlashHandler { + private final Server server; + + public Restart(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if(optServer.isEmpty())return; + var teamServer = optServer.get(); + if (teamServer.exists()) { + teamServer.stop(true) + .thenRun(() -> event.getHook().editOriginal(context.localize("command.server.process.restart.message.restarted")).queue()); + event.reply(context.localize("command.server.process.restart.message.restarting")).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Start.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Start.java new file mode 100644 index 0000000..45f60ed --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Start.java @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.process; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class Start implements SlashHandler { + private final Server server; + + public Start(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if(optServer.isEmpty())return; + var teamServer = optServer.get(); + if (teamServer.exists()) { + if (teamServer.start()) { + event.reply(context.localize("command.server.process.start.message.success")).queue(); + } else { + event.reply(context.localize("command.server.process.start.message.fail")).queue(); + } + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Status.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Status.java new file mode 100644 index 0000000..23cedd3 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Status.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.process; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class Status implements SlashHandler { + private final Server server; + + public Status(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + + event.replyEmbeds(optServer.get().detailStatus(context).join()).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Stop.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Stop.java new file mode 100644 index 0000000..9e28f91 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Stop.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.process; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class Stop implements SlashHandler { + private final Server server; + + public Stop(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if(optServer.isEmpty())return; + var teamServer = optServer.get(); + if (teamServer.exists()) { + event.reply(context.localize("command.server.process.stop.message.stopping")).queue(); + teamServer.stop(false).thenRun(() -> event.getHook().editOriginal( + context.localize("command.server.process.stop.message.stopped")).queue()); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/system/Delete.java b/bot/src/main/java/de/chojo/gamejam/commands/server/system/Delete.java new file mode 100644 index 0000000..698d83f --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/system/Delete.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.system; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import java.io.IOException; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Delete implements SlashHandler { + private static final Logger log = getLogger(Delete.class); + private final Server server; + + public Delete(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if(optServer.isEmpty())return; + var teamServer = optServer.get(); + boolean deleted; + try { + deleted = teamServer.purge(); + } catch (IOException e) { + log.error("Could not purge server", e); + event.reply(context.localize("command.server.system.delete.message.error")).queue(); + return; + } + + if (deleted) { + event.reply(context.localize("command.server.system.delete.message.success")).queue(); + } else { + event.reply(context.localize("command.server.system.delete.message.notsetup")).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/system/Setup.java b/bot/src/main/java/de/chojo/gamejam/commands/server/system/Setup.java new file mode 100644 index 0000000..10fdb03 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/system/Setup.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.system; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import java.io.IOException; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Setup implements SlashHandler { + private static final Logger log = getLogger(Setup.class); + private final Server server; + + public Setup(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if(optServer.isEmpty())return; + var teamServer = optServer.get(); + boolean setup; + try { + setup = teamServer.setup(); + } catch (IOException e) { + log.error("Could not setup server", e); + event.reply(context.localize("command.server.system.setup.message.error")).queue(); + return; + } + + if (setup) { + event.reply(context.localize("command.server.system.setup.message.success")).queue(); + } else { + event.reply(context.localize("command.server.system.setup.message.alreadysetup")).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/upload/Plugin.java b/bot/src/main/java/de/chojo/gamejam/commands/server/upload/Plugin.java new file mode 100644 index 0000000..945e84f --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/upload/Plugin.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.upload; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.commands.server.util.ProgressDownloader; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Plugin implements SlashHandler { + private static final Logger log = getLogger(Plugin.class); + private final Server server; + + public Plugin(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + var teamServer = optServer.get(); + + var downloadUrl = event.getOption("file").getAsAttachment().getProxy().getUrl(); + + var download = ProgressDownloader.download(event, context, downloadUrl); + + if (download.isEmpty()) return; + + var pluginFile = teamServer.plugins().resolve("plugin.jar"); + try { + Files.copy(download.get(), pluginFile, StandardCopyOption.REPLACE_EXISTING); + event.getHook().editOriginal(context.localize("command.server.upload.plugin.message.success")).queue(); + } catch (IOException e) { + event.getHook().editOriginal(context.localize("command.server.upload.plugin.message.fail")).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/upload/UploadPluginData.java b/bot/src/main/java/de/chojo/gamejam/commands/server/upload/UploadPluginData.java new file mode 100644 index 0000000..52e9ce1 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/upload/UploadPluginData.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.upload; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.commands.server.util.ProgressDownloader; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.guild.jams.Jam; +import de.chojo.gamejam.server.ServerService; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.util.Choice; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.slf4j.LoggerFactory.getLogger; + +public class UploadPluginData implements SlashHandler { + private static final Logger log = getLogger(UploadPluginData.class); + private final Server server; + private final Guilds guilds; + private final ServerService serverService; + + public UploadPluginData(Server server, Guilds guilds, ServerService serverService) { + this.server = server; + this.guilds = guilds; + this.serverService = serverService; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + var teamServer = optServer.get(); + + var downloadUrl = event.getOption("file").getAsAttachment().getProxy().getUrl(); + var path = event.getOption("path").getAsString(); + + if (path.contains("..")) { + event.reply(context.localize("error.invalidpath")).queue(); + return; + } + + var download = ProgressDownloader.download(event, context, downloadUrl); + + if (download.isEmpty()) return; + + var pluginFile = teamServer.plugins().resolve(path); + // No upload in plugin root + if (pluginFile.getParent().equals(teamServer.plugins())) { + event.reply(context.localize("error.invalidpath")).queue(); + return; + } + + // No updates into the update directory + if(pluginFile.equals(teamServer.plugins().resolve("update"))){ + event.reply(context.localize("error.invalidpath")).queue(); + return; + } + try { + Files.copy(download.get(), pluginFile, StandardCopyOption.REPLACE_EXISTING); + event.getHook().editOriginal(context.localize("command.server.upload.uploadplugindata.message.success")).queue(); + } catch (IOException e) { + event.getHook().editOriginal(context.localize("command.server.upload.uploadplugindata.message.fail")).queue(); + } + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var optServer = guilds.guild(event).jams().activeJam() + .map(Jam::teams) + .flatMap(teams -> teams.byMember(event.getUser())) + .map(serverService::get); + if (optServer.isEmpty()) return; + var server = optServer.get(); + + var option = event.getFocusedOption(); + if ("path".equals(option.getName())) { + var plugins = server.plugins(); + var currPath = option.getValue(); + if (currPath.contains("..")) { + event.replyChoices().queue(); + return; + } + var split = currPath.split("/"); + + // Root dir + if (split.length == 1 && !currPath.endsWith("/")) { + var currValue = split[0].toLowerCase(); + var files = files(plugins).stream() + .filter(File::isDirectory) + .filter(file -> file.getName().toLowerCase().startsWith(currValue)) + .limit(25) + .map(line -> fileName(plugins, line)); + event.replyChoices(Choice.toStringChoice(files)).queue(); + return; + } + + if (!currPath.endsWith("/")) { + split = Arrays.copyOfRange(split, 0, split.length - 1); + } + + var path = plugins; + + for (var s : split) { + path = path.resolve(s); + } + + if (currPath.endsWith("/")) { + var files = files(path).stream() + .limit(25) + .map(line -> fileName(plugins, line)); + event.replyChoices(Choice.toStringChoice(files)).queue(); + return; + } + split = currPath.split("/"); + var currValue = currPath.split("/")[split.length - 1].toLowerCase(); + + var files = files(path).stream() + .filter(file -> file.getName().toLowerCase().startsWith(currValue)) + .limit(24) + .map(line -> fileName(plugins, line)) + .collect(Collectors.toCollection(ArrayList::new)); + + files.add(0, currPath); + + event.replyChoices(Choice.toStringChoice(files)).queue(); + } + } + + private List files(Path path) { + return Arrays.stream(path.toFile().listFiles()).toList(); + } + + private String fileName(Path strip, File file) { + if (file.isDirectory()) { + return (file.toPath() + "/").replace(strip.toString(), "").replaceAll("^/", ""); + } + return file.toPath().toString().replace(strip.toString(), "").replaceAll("^/", ""); + + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/upload/World.java b/bot/src/main/java/de/chojo/gamejam/commands/server/upload/World.java new file mode 100644 index 0000000..720372a --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/upload/World.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.upload; + +import de.chojo.gamejam.commands.server.Server; +import de.chojo.gamejam.commands.server.util.ProgressDownloader; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import static org.slf4j.LoggerFactory.getLogger; + +public class World implements SlashHandler { + private static final Logger log = getLogger(World.class); + private final Server server; + + public World(Server server) { + this.server = server; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optServer = server.getServer(event, context); + if (optServer.isEmpty()) return; + var teamServer = optServer.get(); + + String downloadUrl = null; + + var urlOption = event.getOption("url"); + if (urlOption != null) { + downloadUrl = urlOption.getAsString(); + } + + var file = event.getOption("file"); + if (file != null) { + downloadUrl = file.getAsAttachment().getProxy().getUrl(); + } + + if (downloadUrl == null) { + event.reply(context.localize("command.server.upload.world.message.nofileorurl")).queue(); + return; + } + + var download = ProgressDownloader.download(event, context, downloadUrl); + + if (download.isEmpty()) return; + + event.getHook().editOriginal(context.localize("command.server.upload.world.message.replacing")).queue(); + if (teamServer.replaceWorld(download.get())) { + event.getHook().editOriginal(context.localize("command.server.upload.world.message.replaced")).queue(); + } else { + event.getHook().editOriginal(context.localize("command.server.upload.world.message.failed")).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/util/ProgressDownloader.java b/bot/src/main/java/de/chojo/gamejam/commands/server/util/ProgressDownloader.java new file mode 100644 index 0000000..919d5f6 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/util/ProgressDownloader.java @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.server.util; + +import de.chojo.gamejam.util.TempFile; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Path; +import java.util.Optional; + +import static org.slf4j.LoggerFactory.getLogger; + +public final class ProgressDownloader { + private static final Logger log = getLogger(ProgressDownloader.class); + + private ProgressDownloader() { + throw new UnsupportedOperationException("This is a utility class."); + } + + public static Optional download(SlashCommandInteractionEvent event, EventContext context, String downloadUrl) { + event.reply(context.localize("command.server.util.progressdownloader.message.attempting")).queue(); + + Path path; + try { + path = TempFile.createFile("gamejam", ".file"); + } catch (IOException e) { + log.error("Failed to create temp file", e); + event.getHook().editOriginal(context.localize("command.server.util.progressdownloader.message.fail.tempfile")).queue(); + return Optional.empty(); + } + + var request = HttpRequest.newBuilder(URI.create(downloadUrl)).GET().build(); + + try { + event.getHook().editOriginal(context.localize("command.server.util.progressdownloader.message.downloading")).queue(); + HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofFile(path)); + } catch (IOException e) { + log.error("Failed to write response", e); + event.getHook().editOriginal(context.localize("command.server.util.progressdownloader.message.fail.download")).queue(); + return Optional.empty(); + } catch (InterruptedException e) { + log.error("Failed to retrieve response", e); + event.getHook().editOriginal(context.localize("command.server.util.progressdownloader.message.fail.download")).queue(); + return Optional.empty(); + } + + event.getHook().editOriginal(context.localize("command.server.util.progressdownloader.message.done")).queue(); + return Optional.of(path); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/ServerAdmin.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/ServerAdmin.java new file mode 100644 index 0000000..00cbb5c --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/ServerAdmin.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin; + +import de.chojo.gamejam.commands.serveradmin.handler.SyncVelocity; +import de.chojo.gamejam.commands.serveradmin.handler.info.Detailed; +import de.chojo.gamejam.commands.serveradmin.handler.info.Short; +import de.chojo.gamejam.commands.serveradmin.handler.refresh.RefreshAll; +import de.chojo.gamejam.commands.serveradmin.handler.restart.RestartAll; +import de.chojo.gamejam.commands.serveradmin.handler.restart.RestartTeam; +import de.chojo.gamejam.commands.serveradmin.handler.start.StartAll; +import de.chojo.gamejam.commands.serveradmin.handler.start.StartTeam; +import de.chojo.gamejam.commands.serveradmin.handler.stop.StopAll; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.jdautil.interactions.slash.Argument; +import de.chojo.jdautil.interactions.slash.Group; +import de.chojo.jdautil.interactions.slash.Slash; +import de.chojo.jdautil.interactions.slash.SubCommand; +import de.chojo.jdautil.interactions.slash.provider.SlashCommand; + +public class ServerAdmin extends SlashCommand { + public ServerAdmin(Guilds guilds, ServerService serverService) { + super(Slash.of("serveradmin", "command.serveradmin.description") + .adminCommand() + .group(Group.of("start", "command.serveradmin.start.description") + .subCommand(SubCommand.of("all", "command.serveradmin.start.all.description") + .handler(new StartAll(serverService, guilds))) + .subCommand(SubCommand.of("team", "command.serveradmin.start.team.description") + .handler(new StartTeam(serverService, guilds)) + .argument(Argument.text("team", "command.serveradmin.start.team.options.team.description").asRequired().withAutoComplete()))) + .group(Group.of("restart", "command.serveradmin.restart.description") + .subCommand(SubCommand.of("all", "command.serveradmin.restart.all.description") + .handler(new RestartAll(serverService, guilds))) + .subCommand(SubCommand.of("team", "command.serveradmin.restart.team.description") + .handler(new RestartTeam(serverService, guilds)) + .argument(Argument.text("team", "command.serveradmin.restart.team.options.team.description").asRequired().withAutoComplete()))) + .group(Group.of("stop", "command.serveradmin.stop.description") + .subCommand(SubCommand.of("all", "command.serveradmin.stop.all.description") + .handler(new StopAll(serverService, guilds))) + .subCommand(SubCommand.of("team", "command.serveradmin.stop.team.description") + .handler(new StopAll(serverService, guilds)) + .argument(Argument.text("team", "command.serveradmin.stop.team.options.team.description").asRequired().withAutoComplete()))) + .group(Group.of("refresh", "command.serveradmin.refresh.description") + .subCommand(SubCommand.of("all", "command.serveradmin.refresh.all.description") + .handler(new RefreshAll(serverService, guilds))) + .subCommand(SubCommand.of("team", "command.serveradmin.refresh.team.description") + .handler(new RefreshAll(serverService, guilds)) + .argument(Argument.text("team", "command.serveradmin.refresh.team.options.team.description").asRequired().withAutoComplete()))) + .group(Group.of("info", "command.serveradmin.info.description") + .subCommand(SubCommand.of("short", "command.serveradmin.info.short.description") + .handler(new Short(serverService, guilds))) + .subCommand(SubCommand.of("detailed", "command.serveradmin.info.detailed.description") + .handler(new Detailed(serverService, guilds)) + .argument(Argument.text("team", "command.serveradmin.info.detailed.options.team.description").withAutoComplete()))) + .subCommand(SubCommand.of("syncvelocity", "command.serveradmin.syncvelocity.description") + .handler(new SyncVelocity(serverService))) + ); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/SyncVelocity.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/SyncVelocity.java new file mode 100644 index 0000000..3475535 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/SyncVelocity.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler; + +import de.chojo.gamejam.server.ServerService; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class SyncVelocity implements SlashHandler { + private final ServerService serverService; + + public SyncVelocity(ServerService serverService) { + this.serverService = serverService; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + serverService.syncVelocity(); + event.reply(context.localize("command.serveradmin.syncvelocity.message.synced")).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/info/Detailed.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/info/Detailed.java new file mode 100644 index 0000000..2889ef4 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/info/Detailed.java @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.info; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.pagination.bag.ListPageBag; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +public class Detailed implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public Detailed(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var teamArg = event.getOption("team"); + + if (teamArg != null) { + var team = jam.teams().byName(teamArg.getAsString()); + if (team.isEmpty()) { + event.reply(context.localize("error.unkownteam")).queue(); + return; + } + var teamServer = serverService.get(team.get()); + event.replyEmbeds(teamServer.detailStatus(context).join()).queue(); + return; + } + + var servers = jam.teams().teams().stream() + .map(serverService::get) + .toList(); + + context.registerPage(new ListPageBag<>(servers) { + @Override + public CompletableFuture buildPage() { + return currentElement().detailStatus(context); + } + }); + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var option = event.getFocusedOption(); + if ("team".equals(option.getName())) { + var choices = guild.jams().nextOrCurrent() + .map(jam -> jam.teams().completeTeam(option.getValue())) + .orElse(Collections.emptyList()); + event.replyChoices(choices).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/info/Short.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/info/Short.java new file mode 100644 index 0000000..bf8a1f9 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/info/Short.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.info; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.stream.Collectors; + +public class Short implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public Short(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var servers = jam.teams().teams().stream() + .map(serverService::get) + .map(TeamServer::status) + .collect(Collectors.joining("\n")); + + event.reply(servers).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshAll.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshAll.java new file mode 100644 index 0000000..01078f7 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshAll.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.refresh; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class RefreshAll implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public RefreshAll(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var count = jam.teams().teams().stream() + .map(serverService::get) + .map(TeamServer::refresh) + .filter(v -> v) + .count(); + event.reply(context.localize("command.serveradmin.refresh.refreshall.message.refreshed", + Replacement.create("AMOUNT", count))).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshTeam.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshTeam.java new file mode 100644 index 0000000..520b5a2 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshTeam.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.refresh; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Collections; + +public class RefreshTeam implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public RefreshTeam(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var optTeam = jam.teams().byName(event.getOption("team").getAsString()); + + if (optTeam.isEmpty()) { + event.reply(context.localize("error.unkownteam")).queue(); + return; + } + + var started = optTeam.map(serverService::get) + .map(TeamServer::refresh) + .orElse(false); + if (started) { + event.reply(context.localize("command.serveradmin.refresh.refreshteam.message.refreshed", + Replacement.create("TEAM", optTeam.get()))).queue(); + } else { + event.reply(context.localize("command.serveradmin.refresh.refreshteam.message.failed", + Replacement.create("TEAM", optTeam.get()))).queue(); + } + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var option = event.getFocusedOption(); + if ("team".equals(option.getName())) { + var choices = guild.jams().nextOrCurrent() + .map(jam -> jam.teams().completeTeam(option.getValue())) + .orElse(Collections.emptyList()); + event.replyChoices(choices).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartAll.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartAll.java new file mode 100644 index 0000000..8472b89 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartAll.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.restart; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class RestartAll implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public RestartAll(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var count = jam.teams().teams().stream() + .map(serverService::get) + .filter(server -> { + var running = server.running(); + if (running) { + server.stop(true); + } + return running; + }) + .count(); + event.reply(context.localize("command.serveradmin.restart.restartall.message.restarted", + Replacement.create("AMOUNT", count))).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartTeam.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartTeam.java new file mode 100644 index 0000000..e712f51 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartTeam.java @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.restart; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Collections; + +public class RestartTeam implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public RestartTeam(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var optTeam = jam.teams().byName(event.getOption("team").getAsString()); + + if (optTeam.isEmpty()) { + event.reply(context.localize("error.unkownteam")).queue(); + return; + } + + var started = optTeam.map(serverService::get).map(server -> { + var running = server.running(); + server.restart(); + return running; + }).orElse(false); + if (started) { + event.reply(context.localize("command.serveradmin.restart.restartteam.message.restarted", + Replacement.create("TEAM", optTeam.get()))).queue(); + } else { + event.reply(context.localize("command.serveradmin.restart.restartteam.message.failed", + Replacement.create("TEAM", optTeam.get()))).queue(); + } + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var option = event.getFocusedOption(); + if ("team".equals(option.getName())) { + var choices = guild.jams().nextOrCurrent() + .map(jam -> jam.teams().completeTeam(option.getValue())) + .orElse(Collections.emptyList()); + event.replyChoices(choices).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/start/StartAll.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/start/StartAll.java new file mode 100644 index 0000000..72bb07a --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/start/StartAll.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.start; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class StartAll implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public StartAll(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var count = jam.teams().teams().stream() + .map(serverService::get) + .map(TeamServer::start) + .filter(v -> v) + .count(); + event.reply(context.localize("command.serveradmin.start.startall.message.started", + Replacement.create("AMOUNT", count))).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/start/StartTeam.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/start/StartTeam.java new file mode 100644 index 0000000..60caf87 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/start/StartTeam.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.start; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Collections; + +public class StartTeam implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public StartTeam(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var optTeam = jam.teams().byName(event.getOption("team").getAsString()); + + if (optTeam.isEmpty()) { + event.reply(context.localize("error.unkownteam")).queue(); + return; + } + + var started = optTeam.map(serverService::get).map(TeamServer::start).orElse(false); + if (started) { + event.reply(context.localize("command.serveradmin.start.startteam.message.started", + Replacement.create("TEAM", optTeam.get()))).queue(); + } else { + event.reply(context.localize("command.serveradmin.start.startteam.message.failed", + Replacement.create("TEAM", optTeam.get()))).queue(); + } + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var option = event.getFocusedOption(); + if ("team".equals(option.getName())) { + var choices = guild.jams().nextOrCurrent() + .map(jam -> jam.teams().completeTeam(option.getValue())) + .orElse(Collections.emptyList()); + event.replyChoices(choices).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopAll.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopAll.java new file mode 100644 index 0000000..c6939e3 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopAll.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.stop; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class StopAll implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public StopAll(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var count = jam.teams().teams().stream() + .map(serverService::get) + .map(server -> { + var running = server.running(); + server.stop(false); + return running; + }) + .filter(v -> v) + .count(); + event.reply(context.localize("command.serveradmin.stop.stopall.message.stopped", + Replacement.create("AMOUNT", count))).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopTeam.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopTeam.java new file mode 100644 index 0000000..f6d25f8 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopTeam.java @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.serveradmin.handler.stop; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.server.ServerService; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Collections; + +public class StopTeam implements SlashHandler { + private final ServerService serverService; + private final Guilds guilds; + + public StopTeam(ServerService serverService, Guilds guilds) { + this.serverService = serverService; + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var currentJam = guilds.guild(event).jams().getCurrentJam(); + if (currentJam.isEmpty()) { + event.reply(context.localize("error.noactivejam")).queue(); + return; + } + var jam = currentJam.get(); + + var optTeam = jam.teams().byName(event.getOption("team").getAsString()); + + if (optTeam.isEmpty()) { + event.reply(context.localize("error.unkownteam")).queue(); + return; + } + + var started = optTeam.map(serverService::get).map(server -> { + var running = server.running(); + server.stop(); + return running; + }).orElse(false); + if (started) { + event.reply(context.localize("command.serveradmin.stop.stopteam.message.stopped", + Replacement.create("TEAM", optTeam.get()))).queue(); + } else { + event.reply(context.localize("command.serveradmin.stop.stopteam.message.failed", + Replacement.create("TEAM", optTeam.get()))).queue(); + } + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var option = event.getFocusedOption(); + if ("team".equals(option.getName())) { + var choices = guild.jams().nextOrCurrent() + .map(jam -> jam.teams().completeTeam(option.getValue())) + .orElse(Collections.emptyList()); + event.replyChoices(choices).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/settings/Settings.java b/bot/src/main/java/de/chojo/gamejam/commands/settings/Settings.java new file mode 100644 index 0000000..75daa8d --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/settings/Settings.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.settings; + +import de.chojo.gamejam.commands.settings.handler.Info; +import de.chojo.gamejam.commands.settings.handler.JamRole; +import de.chojo.gamejam.commands.settings.handler.Locale; +import de.chojo.gamejam.commands.settings.handler.TeamSize; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.Argument; +import de.chojo.jdautil.interactions.slash.Slash; +import de.chojo.jdautil.interactions.slash.SubCommand; +import de.chojo.jdautil.interactions.slash.provider.SlashCommand; + +public class Settings extends SlashCommand { + public Settings(Guilds guilds) { + super(Slash.of("settings", "command.settings.description") + .adminCommand() + .subCommand(SubCommand.of("jamrole", "command.settings.jamrole.description") + .handler(new JamRole(guilds)) + .argument(Argument.role("role", "command.settings.jamrole.options.role.description").asRequired())) + .subCommand(SubCommand.of("teamsize", "command.settings.teamsize.description") + .handler(new TeamSize(guilds)) + .argument(Argument.integer("size", "command.settings.teamsize.options.size.description").asRequired())) + .subCommand(SubCommand.of("orgarole", "command.settings.orgarole.description") + // TODO: Command gone? + .handler(null) + .argument(Argument.role("role", "command.settings.orgarole.options.role.description").asRequired())) + .subCommand(SubCommand.of("locale", "command.settings.locale.description") + .handler(new Locale(guilds)) + .argument(Argument.text("locale", "command.settings.locale.options.locale.description").asRequired())) + .subCommand(SubCommand.of("info", "command.settings.info.description") + .handler(new Info(guilds))) + ); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/Info.java b/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/Info.java new file mode 100644 index 0000000..de9e23f --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/Info.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.settings.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.gamejam.data.dao.guild.JamSettings; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.LocalizedEmbedBuilder; +import de.chojo.jdautil.util.MentionUtil; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public final class Info implements SlashHandler { + private final Guilds guilds; + + public Info(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + JamGuild guild = guilds.guild(event.getGuild()); + JamSettings jamSettings = guild.jamSettings(); + var settings = guild.settings(); + var embed = new LocalizedEmbedBuilder(context.guildLocalizer()) + .setTitle("command.settings.info.embed.settings") + .addField("command.settings.info.embed.jamrole", MentionUtil.role(jamSettings.jamRole()), true) + .addField("command.settings.info.embed.teamsize", String.valueOf(jamSettings.teamSize()), true) + .addField("command.settings.info.embed.orgarole", MentionUtil.role(settings.orgaRole()), true) + .build(); + event.replyEmbeds(embed).setEphemeral(true).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/JamRole.java b/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/JamRole.java new file mode 100644 index 0000000..3945d97 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/JamRole.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.settings.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public final class JamRole implements SlashHandler { + private final Guilds guilds; + + public JamRole(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + guilds.guild(event.getGuild()).jamSettings().jamRole(event.getOption("role").getAsRole()); + event.reply(context.localize("command.settings.jamrole.message.updated")).setEphemeral(true).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/Locale.java b/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/Locale.java new file mode 100644 index 0000000..23c3de6 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/Locale.java @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.settings.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.guild.Settings; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public final class Locale implements SlashHandler { + private final Guilds guilds; + + public Locale(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var settings = guilds.guild(event).settings(); + var locale = event.getOption("locale").getAsString(); + context.guildLocalizer().localizer().getLanguage(locale) + .ifPresentOrElse(language -> { + settings.locale(language.getLocale()); + event.reply(context.localize("command.settings.locale.message.updated")).setEphemeral(true).queue(); + context.commandHub().refreshGuildCommands(event.getGuild()); + }, () -> event.reply(context.localize("command.settings.locale.message.invalid")).setEphemeral(true).queue()); + + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/TeamSize.java b/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/TeamSize.java new file mode 100644 index 0000000..d58a406 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/settings/handler/TeamSize.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.settings.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public final class TeamSize implements SlashHandler { + private final Guilds guilds; + + public TeamSize(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var settings = guilds.guild(event).jamSettings(); + settings.teamSize(event.getOption("size").getAsInt()); + event.reply(context.localize("command.settings.teamsize.message.updated")).setEphemeral(true).queue(); + + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/Team.java b/bot/src/main/java/de/chojo/gamejam/commands/team/Team.java new file mode 100644 index 0000000..bbbf0a9 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/Team.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team; + +import de.chojo.gamejam.commands.team.handler.Create; +import de.chojo.gamejam.commands.team.handler.Disband; +import de.chojo.gamejam.commands.team.handler.Edit; +import de.chojo.gamejam.commands.team.handler.Invite; +import de.chojo.gamejam.commands.team.handler.Leave; +import de.chojo.gamejam.commands.team.handler.List; +import de.chojo.gamejam.commands.team.handler.Profile; +import de.chojo.gamejam.commands.team.handler.Promote; +import de.chojo.gamejam.commands.team.handler.Rename; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.Argument; +import de.chojo.jdautil.interactions.slash.Slash; +import de.chojo.jdautil.interactions.slash.SubCommand; +import de.chojo.jdautil.interactions.slash.provider.SlashCommand; + +public class Team extends SlashCommand { + public Team(Guilds guilds) { + super(Slash.of("team", "command.team.description") + .subCommand(SubCommand.of("create", "command.team.create.description") + .handler(new Create(guilds)) + .argument(Argument.text("name", "command.team.create.options.name.description") + .asRequired())) + .subCommand(SubCommand.of("edit", "Edit Eeam information") + .handler(new Edit(guilds))) + .subCommand(SubCommand.of("invite", "command.team.invite.description") + .handler(new Invite(guilds)) + .argument(Argument.user("user", "command.team.invite.options.user.description") + .asRequired())) + .subCommand(SubCommand.of("leave", "command.team.leave.description") + .handler(new Leave(guilds))) + .subCommand(SubCommand.of("disband", "command.team.disband.description") + .handler(new Disband(guilds)) + .argument(Argument.bool("confirm", "command.team.disband.options.confirm.description") + .asRequired())) + .subCommand(SubCommand.of("promote", "command.team.promote.description") + .handler(new Promote(guilds)) + .argument(Argument.user("user", "command.team.promote.options.user.description") + .asRequired())) + .subCommand(SubCommand.of("profile", "command.team.profile.description") + .handler(new Profile(guilds)) + .argument(Argument.user("user", "command.team.profile.options.user.description")) + .argument(Argument.text("team", "command.team.profile.options.team.description") + .withAutoComplete())) + .subCommand(SubCommand.of("list", "command.team.list.description") + .handler(new List(guilds))) + .subCommand(SubCommand.of("rename", "command.team.rename.description") + .handler(new Rename(guilds)) + .argument(Argument.text("name", "command.team.rename.options.name.description") + .asRequired())) + ); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Create.java b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Create.java new file mode 100644 index 0000000..cf0858f --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Create.java @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team.handler; + + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.guild.jams.jam.user.JamUser; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Collections; +import java.util.EnumSet; + +public final class Create implements SlashHandler { + private final Guilds guilds; + + public Create(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var jamGuild = guilds.guild(event.getGuild()); + var optJam = jamGuild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + + if (jam.state().isVoting()) { + event.reply(context.localize("error.votingactive")).setEphemeral(true).queue(); + return; + } + + if (!jam.registrations().contains(event.getMember().getIdLong())) { + event.reply(context.localize("command.team.create.message.unregistered")).setEphemeral(true).queue(); + return; + } + var jamUser = jam.user(event.getMember()); + + var userTeam = jamUser.team(); + if (userTeam.isPresent()) { + event.reply(context.localize("command.team.create.message.alreadymember")).setEphemeral(true).queue(); + return; + } + var teamName = event.getOption("name").getAsString(); + + // TODO: Enforce constrains of length and allowed chars + + var optTeam = jam.teams().byName(event.getOption("name").getAsString()); + + if (optTeam.isPresent()) { + event.reply(context.localize("command.team.create.message.nametaken")).setEphemeral(true).queue(); + return; + } + + event.deferReply().setEphemeral(true).queue(); + + var categoryList = event.getGuild().getCategoriesByName("Team", true); + + var optCategory = categoryList.stream().filter(cat -> cat.getChannels().size() < 48).findFirst(); + // This is really hacky and I dont like it. + // All this stuff is blocking atm but in a different thread already + var category = optCategory.orElseGet(() -> event.getGuild().createCategory("Team").complete()); + + var role = event.getGuild() + .createRole() + .setPermissions(0L) + .setMentionable(false) + .setHoisted(false) + .setName(teamName) + .complete(); + + var text = event.getGuild().createTextChannel(teamName.replace(" ", "-"), category) + .addRolePermissionOverride(role.getIdLong(), EnumSet.of(Permission.VIEW_CHANNEL), Collections.emptySet()) + .addMemberPermissionOverride(event.getJDA().getSelfUser() + .getIdLong(), EnumSet.of(Permission.VIEW_CHANNEL, Permission.MANAGE_CHANNEL), Collections.emptySet()) + .addRolePermissionOverride(event.getGuild().getPublicRole() + .getIdLong(), Collections.emptySet(), EnumSet.of(Permission.VIEW_CHANNEL)) + .complete(); + + var voice = event.getGuild().createVoiceChannel(teamName, category) + .addRolePermissionOverride(role.getIdLong(), EnumSet.of(Permission.VIEW_CHANNEL), Collections.emptySet()) + .addMemberPermissionOverride(event.getJDA().getSelfUser() + .getIdLong(), EnumSet.of(Permission.VIEW_CHANNEL, Permission.MANAGE_CHANNEL), Collections.emptySet()) + .addRolePermissionOverride(event.getGuild().getPublicRole() + .getIdLong(), Collections.emptySet(), EnumSet.of(Permission.VIEW_CHANNEL)) + .complete(); + + var team = jam.teams() + .create(teamName); + var meta = team.meta(); + meta.leader(event.getMember()); + meta.textChannel(text); + meta.voiceChannel(voice); + meta.role(role); + jamUser.join(team); + + event.getHook().editOriginal(context.localize("command.team.create.message.created")).queue(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Disband.java b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Disband.java new file mode 100644 index 0000000..fac2a9a --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Disband.java @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.guild.jams.Jam; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Optional; + +public final class Disband implements SlashHandler { + private final Guilds guilds; + + public Disband(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + Optional optJam = guilds.guild(event).jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + + if (jam.state().isVoting()) { + event.reply(context.localize("error.votingactive")).setEphemeral(true).queue(); + return; + } + + if (!event.getOption("confirm").getAsBoolean()) { + event.reply(context.localize("error.noconfirm")).setEphemeral(true).queue(); + return; + } + + var jamTeam = jam.teams().byMember(event.getMember()); + if (jamTeam.isEmpty()) { + event.reply(context.localize("error.noteam")).setEphemeral(true).queue(); + return; + } + + var team = jamTeam.get(); + + + var members = team.member(); + for (var teamMember : members) { + teamMember.member() + .getUser() + .openPrivateChannel() + .flatMap(channel -> channel.sendMessage(context.localize("command.team.disband.message.disbanded"))) + .queue(); + } + + if (team.disband()) { + event.reply(context.localize("command.team.disband.message.disbanded")).setEphemeral(true).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Edit.java b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Edit.java new file mode 100644 index 0000000..ebe87f3 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Edit.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.modals.handler.ModalHandler; +import de.chojo.jdautil.modals.handler.TextInputHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; + +public class Edit implements SlashHandler { + private final Guilds guilds; + + public Edit(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optJam = guilds.guild(event).jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var optTeam = optJam.get().teams().byMember(event.getMember()); + if (optTeam.isEmpty()) { + event.reply(context.localize("error.noteam")).setEphemeral(true).queue(); + return; + } + + var meta = optTeam.get().meta(); + + context.registerModal(ModalHandler.builder("Edit Profile") + .addInput(TextInputHandler.builder("descr", "Project Description", TextInputStyle.PARAGRAPH) + .withValue(meta.projectDescription()) + .withMaxLength(100) + .withHandler(mapping -> meta.projectDescription(mapping.getAsString()))) + .addInput(TextInputHandler.builder("url", "Project url", TextInputStyle.SHORT) + .withMaxLength(200) + .withValue(meta.projectUrl()) + .withHandler(mapping -> meta.projectUrl(mapping.getAsString()))) + .withHandler(modalEvent -> { + modalEvent.reply("Updated").queue(); + }) + .build()); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Invite.java b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Invite.java new file mode 100644 index 0000000..816d160 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Invite.java @@ -0,0 +1,134 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.LocalizationContext; +import de.chojo.jdautil.localization.util.LocalizedEmbedBuilder; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.menus.EntryContext; +import de.chojo.jdautil.menus.MenuAction; +import de.chojo.jdautil.menus.entries.ButtonEntry; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; + +public final class Invite implements SlashHandler { + private final Guilds guilds; + + public Invite(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + JamGuild guild = guilds.guild(event); + var optJam = guild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + + if (jam.state().isVoting()) { + event.reply(context.localize("error.votingactive")).setEphemeral(true).queue(); + return; + } + + var optTeam = jam.teams().byMember(event.getMember()); + if (optTeam.isEmpty()) { + event.reply(context.localize("error.noteam")).setEphemeral(true).queue(); + return; + } + + var team = optTeam.get(); + + if (!team.isLeader(event.getUser())) { + event.reply(context.localize("command.team.invite.message.noleader")).setEphemeral(true).queue(); + return; + } + + var member = team.member(); + var settings = guild.jamSettings(); + + if (member.size() >= settings.teamSize()) { + event.reply(context.localize("error.maxteamsize")).setEphemeral(true).queue(); + return; + } + + var user = event.getOption("user").getAsUser(); + + if (!jam.registrations().contains(user.getIdLong())) { + event.reply(context.localize("command.team.invite.message.notRegistered")).setEphemeral(true).queue(); + return; + } + + var currTeam = jam.teams().byMember(user); + + if (currTeam.isPresent()) { + event.reply(context.localize("command.team.invite.message.partofteam")).queue(); + return; + } + + user.openPrivateChannel().queue(channel -> { + var embed = new LocalizedEmbedBuilder(context.guildLocalizer()) + .setTitle("command.team.invite.message.invited", Replacement.create("GUILD", event.getGuild() + .getName())) + .setDescription("command.team.invite.message.invitation", + Replacement.createMention(event.getUser()), Replacement.create("TEAM", team.meta().name())) + .build(); + event.reply(context.localize("command.team.invite.message.send")).setEphemeral(true).queue(); + context.registerMenu(MenuAction.forChannel(embed, channel) + .addComponent(ButtonEntry.of(Button.of(ButtonStyle.SUCCESS, "accept", "command.team.invite.message.accept"), + button -> accept(button, event.getGuild().getIdLong(), + team, user.getIdLong(), context.guildLocalizer()))) + .build()); + }); + } + + private void accept(EntryContext button, long guildId, Team team, long userId, LocalizationContext localizer) { + var members = team.member(); + var interaction = button.event(); + interaction.deferReply().queue(); + var manager = interaction.getJDA().getShardManager(); + var guild = manager.getGuildById(guildId); + var user = manager.retrieveUserById(userId).complete(); + var member = guild.retrieveMember(user).complete(); + var jamGuild = guilds.guild(button.event()); + var settings = jamGuild.jamSettings(); + + if (members.size() >= settings.teamSize()) { + interaction.getHook().editOriginal(localizer.localize("error.maxteamsize")).queue(); + return; + } + var jam = jamGuild.jams().nextOrCurrent(); + if (jam.isEmpty()) { + interaction.getHook().editOriginal(localizer.localize("command.team.invite.gameJamOver")).queue(); + return; + } + + var currTeam = jam.get().teams().byMember(user); + + if (currTeam.isPresent()) { + interaction.getHook().editOriginal(localizer.localize("command.team.invite.alreadyMember")).queue(); + return; + } + + jam.get().user(member).join(team); + team.meta().role().ifPresent(role -> guild.addRoleToMember(member, role)); + interaction.getHook().editOriginal(localizer.localize("command.team.invite.joined")).queue(); + team.meta().textChannel().ifPresent(channel -> { + channel.sendMessage(localizer.localize("command.team.invite.joinedBroadcast", Replacement.createMention(member))) + .queue(); + }); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Leave.java b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Leave.java new file mode 100644 index 0000000..e66e873 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Leave.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public final class Leave implements SlashHandler { + private final Guilds guilds; + + public Leave(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var jamGuild = guilds.guild(event); + var optJam = jamGuild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + + if (jam.state().isVoting()) { + event.reply(context.localize("error.votingactive")).setEphemeral(true).queue(); + return; + } + + jam.teams().byMember(event.getMember()) + .ifPresentOrElse(team -> { + if (!team.isLeader(event.getUser())) { + event.reply(context.localize("command.team.leave.message.leaderleave")).setEphemeral(true).queue(); + return; + } + team.member(event.getMember()).ifPresent(member -> { + member.leave(); + team.meta().textChannel().ifPresent(channel -> { + channel.sendMessage(context.localize("command.team.leave.leftBroadcast", + Replacement.createMention(event.getMember()))) + .queue(); + }); + event.reply(context.localize("command.team.leave.left")).setEphemeral(true).queue(); + }); + }, () -> event.reply(context.localize("error.noteam")).setEphemeral(true).queue()); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/handler/List.java b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/List.java new file mode 100644 index 0000000..f80c9d9 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/List.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.pagination.bag.PrivateListPageBag; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.concurrent.CompletableFuture; + +public class List implements SlashHandler { + private final Guilds guilds; + + public List(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + JamGuild guild = guilds.guild(event); + var optJam = guild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + + context.registerPage(new PrivateListPageBag<>(jam.teams().teams(), event.getUser().getIdLong()) { + @Override + public CompletableFuture buildPage() { + return CompletableFuture.supplyAsync(() -> currentElement().profileEmbed(context.guildLocalizer())); + } + }, true); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Profile.java b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Profile.java new file mode 100644 index 0000000..0b7797f --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Profile.java @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.Command; + +import java.util.Collections; + +public final class Profile implements SlashHandler { + private final Guilds guilds; + + public Profile(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var optJam = guilds.guild(event).jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + var teams = jam.teams(); + + if (event.getOption("user") != null) { + teams.byMember(event.getOption("user").getAsMember()) + .ifPresentOrElse(team -> sendProfile(event, team, context), + () -> event.reply(context.localize("command.team.profile.message.nouserteam")) + .setEphemeral(true) + .queue()); + return; + } + if (event.getOption("team") != null) { + teams.byName(event.getOption("team").getAsString()) + .ifPresentOrElse(team -> sendProfile(event, team, context), + () -> event.reply(context.localize("error.unkownteam")).setEphemeral(true).queue()); + return; + } + teams.byMember(event.getMember()) + .ifPresentOrElse(team -> sendProfile(event, team, context), + () -> event.reply(context.localize("error.noteam")).setEphemeral(true).queue()); + } + + private void sendProfile(SlashCommandInteractionEvent event, Team team, EventContext context) { + event.replyEmbeds(team.profileEmbed(context.guildLocalizer())).setEphemeral(true).queue(); + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var option = event.getFocusedOption(); + if ("team".equals(option.getName())) { + var choices = guild.jams().nextOrCurrent() + .map(jam -> jam.teams().completeTeam(option.getValue())) + .orElse(Collections.emptyList()); + event.replyChoices(choices).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Promote.java b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Promote.java new file mode 100644 index 0000000..e0a5f78 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Promote.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Optional; + +public class Promote implements SlashHandler { + private final Guilds guilds; + + public Promote(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + JamGuild guild = guilds.guild(event); + var optJam = guild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + + var user = event.getOption("user").getAsMember(); + jam.teams().byMember(user) + .ifPresentOrElse( + targetTeam -> { + if (!targetTeam.isLeader(event.getUser())) { + event.reply(context.localize("error.noleader")).setEphemeral(true).queue(); + return; + } + + targetTeam.meta().leader(user); + event.reply(context.localize("command.team.promote.message.done")).setEphemeral(true).queue(); + + }, + () -> event.reply(context.localize("error.noteam")).setEphemeral(true).queue()); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Rename.java b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Rename.java new file mode 100644 index 0000000..d31a6bc --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/team/handler/Rename.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.team.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class Rename implements SlashHandler { + private final Guilds guilds; + + public Rename(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + JamGuild guild = guilds.guild(event); + var optJam = guild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + + jam.teams().byName(event.getOption("name").getAsString()) + .ifPresentOrElse( + team -> event.reply(context.localize("command.team.create.message.nametaken")).setEphemeral(true) + .queue(), + () -> { + var optCurrTeam = jam.teams().byMember(event.getUser()); + + if (optCurrTeam.isEmpty()) { + event.reply(context.localize("error.noteam")).setEphemeral(true).queue(); + return; + } + + var team = optCurrTeam.get(); + + if (!team.isLeader(event.getUser())) { + event.reply(context.localize("error.noleader")).setEphemeral(true).queue(); + return; + } + + team.meta().rename(event.getOption("name").getAsString()); + event.reply(context.localize("command.team.rename.message.done")).setEphemeral(true).queue(); + }); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/unregister/Unregister.java b/bot/src/main/java/de/chojo/gamejam/commands/unregister/Unregister.java new file mode 100644 index 0000000..b176e47 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/unregister/Unregister.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.unregister; + +import de.chojo.gamejam.commands.unregister.handler.Handler; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.Slash; +import de.chojo.jdautil.interactions.slash.provider.SlashCommand; + +public class Unregister extends SlashCommand { + public Unregister(Guilds guilds) { + super(Slash.of("unregister", "command.unregister.description") + .command(new Handler(guilds))); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/unregister/handler/Handler.java b/bot/src/main/java/de/chojo/gamejam/commands/unregister/handler/Handler.java new file mode 100644 index 0000000..a8969d3 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/unregister/handler/Handler.java @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.unregister.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +public class Handler implements SlashHandler { + private final Guilds guilds; + + public Handler(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var optJam = guild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.noupcomingjam")) + .setEphemeral(true) + .queue(); + return; + } + + var jam = optJam.get(); + + if (!jam.registrations().contains(event.getMember().getIdLong())) { + event.reply(context.localize("command.unregister.message.notregistered")).setEphemeral(true).queue(); + return; + } + + jam.teams().byMember(event.getMember()) + .ifPresentOrElse( + team -> event.reply(context.localize("command.unregister.message.inteam")).queue(), + () -> { + var settings = guild.jamSettings(); + var role = event.getGuild().getRoleById(settings.jamRole()); + if (role != null) { + event.getGuild().removeRoleFromMember(event.getMember(), role).queue(); + } + event.reply(context.localize("command.unregister.message.unregistered")) + .setEphemeral(true) + .queue(); + + }); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/vote/Votes.java b/bot/src/main/java/de/chojo/gamejam/commands/vote/Votes.java new file mode 100644 index 0000000..b52829a --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/vote/Votes.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.vote; + +import de.chojo.gamejam.commands.vote.handler.Info; +import de.chojo.gamejam.commands.vote.handler.Ranking; +import de.chojo.gamejam.commands.vote.handler.Vote; +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.Argument; +import de.chojo.jdautil.interactions.slash.Slash; +import de.chojo.jdautil.interactions.slash.SubCommand; +import de.chojo.jdautil.interactions.slash.provider.SlashCommand; + +public class Votes extends SlashCommand { + public Votes(Guilds guilds) { + super(Slash.of("votes", "command.votes.description") + .subCommand(SubCommand.of("vote", "command.votes.vote.description") + .handler(new Vote(guilds)) + .argument(Argument.text("team", "command.votes.vote.options.team.description").asRequired() + .withAutoComplete()) + .argument(Argument.integer("points", "command.votes.vote.options.points.description").asRequired() + .withAutoComplete())) + .subCommand(SubCommand.of("info", "command.votes.info.description") + .handler(new Info(guilds))) + .subCommand(SubCommand.of("ranking", "command.votes.ranking.description") + .handler(new Ranking(guilds))) + ); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/vote/handler/Info.java b/bot/src/main/java/de/chojo/gamejam/commands/vote/handler/Info.java new file mode 100644 index 0000000..6e9cfa5 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/vote/handler/Info.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.vote.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.LocalizedEmbedBuilder; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.stream.Collectors; + +public class Info implements SlashHandler { + private final Guilds guilds; + + public Info(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var optJam = guild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + var jam = optJam.get(); + + var voteEntries = jam.user(event.getMember()).votes(); + var given = voteEntries.stream() + .filter(e -> e.points() != 0) + .map(e -> e.team().meta().name() + ": **" + e.points() + "**") + .collect(Collectors.joining("\n")); + + var build = new LocalizedEmbedBuilder(context.guildLocalizer()) + .setTitle("command.votes.info.embed.title") + .setDescription(given) + .build(); + event.replyEmbeds(build).setEphemeral(true).queue(); + + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/vote/handler/Ranking.java b/bot/src/main/java/de/chojo/gamejam/commands/vote/handler/Ranking.java new file mode 100644 index 0000000..b5d6c89 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/vote/handler/Ranking.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.vote.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.LocalizedEmbedBuilder; +import de.chojo.jdautil.pagination.bag.ListPageBag; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.concurrent.CompletableFuture; + +public class Ranking implements SlashHandler { + private final Guilds guilds; + + public Ranking(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var optJam = guild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + + var jam = optJam.get(); + + if (jam.state().isVoting()) { + event.reply(context.localize("command.votes.ranking.message.voteactive")).setEphemeral(true).queue(); + return; + } + + var ranking = jam.votes(); + + var pageBag = new ListPageBag<>(ranking) { + @Override + public CompletableFuture buildPage() { + var teamVote = currentElement(); + var embed = new LocalizedEmbedBuilder(context.guildLocalizer()) + .setTitle(teamVote.rank() + " | " + teamVote.team().meta().name()) + .addField("command.votes.ranking.embed.votes", String.valueOf(teamVote.votes()), true) + .build(); + return CompletableFuture.completedFuture(embed); + } + }; + context.registerPage(pageBag, true); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/vote/handler/Vote.java b/bot/src/main/java/de/chojo/gamejam/commands/vote/handler/Vote.java new file mode 100644 index 0000000..9ec1e45 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/commands/vote/handler/Vote.java @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.commands.vote.handler; + +import de.chojo.gamejam.data.access.Guilds; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.gamejam.data.dao.guild.jams.jam.user.JamUser; +import de.chojo.gamejam.data.wrapper.votes.VoteEntry; +import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; +import de.chojo.jdautil.localization.util.Format; +import de.chojo.jdautil.localization.util.Replacement; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.Command; + +import java.util.Collections; +import java.util.stream.IntStream; + +public class Vote implements SlashHandler { + private final Guilds guilds; + + public Vote(Guilds guilds) { + this.guilds = guilds; + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { + var guild = guilds.guild(event); + var optJam = guild.jams().nextOrCurrent(); + if (optJam.isEmpty()) { + event.reply(context.localize("error.nojamactive")).setEphemeral(true).queue(); + return; + } + + var jam = optJam.get(); + + event.deferReply(true).queue(); + if (!jam.state().isVoting()) { + event.getHook().editOriginal(context.localize("command.votes.vote.message.notactive")).queue(); + return; + } + + if (!jam.registrations().contains(event.getMember().getIdLong())) { + event.getHook().editOriginal(context.localize("error.notregistered")).queue(); + return; + } + + var teams = jam.teams(); + var teamCount = teams.teams().size(); + var optVoteTeam = teams.byName(event.getOption("team").getAsString()); + + if (optVoteTeam.isEmpty()) { + event.getHook().editOriginal(context.localize("error.unkownteam")).queue(); + return; + } + + var voteTeam = optVoteTeam.get(); + + if (voteTeam.member(event.getMember()).isPresent()) { + event.getHook().editOriginal(context.localize("command.votes.vote.message.ownteam")).queue(); + return; + } + + var user = jam.user(event.getMember()); + + var pointsGiven = user.votesGiven(); + + //TODO: Max points and max points per team are currently hardcoded. should be configurable in the future. + var points = Math.min(5, Math.max(0, event.getOption("points").getAsInt())); + + var votes = voteTeam.votes(event.getMember()); + + if (votes < points && pointsGiven + points > teamCount) { + event.getHook().editOriginal(context.localize("command.votes.vote.message.maxpointsreached", + Replacement.create("REMAINING", teamCount - pointsGiven) + .addFormatting(Format.BOLD))) + .queue(); + return; + } + + voteTeam.vote(event.getMember(), points); + + event.getHook().editOriginal(context.localize("command.votes.vote.message.done", + Replacement.create("REMAINING", teamCount - user.votesGiven()).addFormatting(Format.BOLD), + Replacement.create("POINTS", points).addFormatting(Format.BOLD), + Replacement.create("TEAM", voteTeam.meta().name()).addFormatting(Format.BOLD))) + .queue(); + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { + var jam = guilds.guild(event).jams().nextOrCurrent(); + var option = event.getFocusedOption(); + if ("team".equals(option.getName())) { + if (jam.isEmpty()) { + event.replyChoices(Collections.emptyList()).queue(); + return; + } + var teams = jam.get().teams().teams().stream() + .filter(team -> team.matchName(option.getValue())) + .map(team -> team.meta().name()) + .map(team -> new Command.Choice(team, team)) + .toList(); + event.replyChoices(teams).queue(); + } + if ("points".equals(option.getName())) { + event.replyChoices(IntStream.range(0, 6).mapToObj(num -> new Command.Choice(String.valueOf(num), num)) + .toList()).queue(); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/ConfigFile.java b/bot/src/main/java/de/chojo/gamejam/configuration/ConfigFile.java new file mode 100644 index 0000000..eb299c9 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/ConfigFile.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration; + +import de.chojo.gamejam.configuration.elements.Api; +import de.chojo.gamejam.configuration.elements.BaseSettings; +import de.chojo.gamejam.configuration.elements.Database; +import de.chojo.gamejam.configuration.elements.Plugins; +import de.chojo.gamejam.configuration.elements.ServerManagement; +import de.chojo.gamejam.configuration.elements.ServerTemplate; + +@SuppressWarnings("FieldMayBeFinal") +public class ConfigFile { + private BaseSettings baseSettings = new BaseSettings(); + private Database database = new Database(); + private Api api = new Api(); + private ServerManagement serverManagement = new ServerManagement(); + private Plugins plugins = new Plugins(); + private ServerTemplate serverTemplate = new ServerTemplate(); + + public BaseSettings baseSettings() { + return baseSettings; + } + + public Database database() { + return database; + } + + public Api api() { + return api; + } + + public ServerManagement serverManagement(){ + return serverManagement; + } + + public Plugins plugins() { + return plugins; + } + + public ServerTemplate serverTemplate() { + return serverTemplate; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/Configuration.java b/bot/src/main/java/de/chojo/gamejam/configuration/Configuration.java new file mode 100644 index 0000000..7e0f7a4 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/Configuration.java @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import de.chojo.gamejam.configuration.elements.Api; +import de.chojo.gamejam.configuration.elements.BaseSettings; +import de.chojo.gamejam.configuration.elements.Database; +import de.chojo.gamejam.configuration.elements.Plugins; +import de.chojo.gamejam.configuration.elements.ServerManagement; +import de.chojo.gamejam.configuration.elements.ServerTemplate; +import de.chojo.gamejam.configuration.exception.ConfigurationException; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Configuration { + private static final Logger log = getLogger(Configuration.class); + private final ObjectMapper objectMapper; + private ConfigFile configFile; + + private Configuration() { + objectMapper = JsonMapper.builder() + .configure(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS, true) + .build() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) + .setDefaultPrettyPrinter(new DefaultPrettyPrinter()); + } + + public static Configuration create() { + var configuration = new Configuration(); + configuration.reload(); + return configuration; + } + + public void reload() { + try { + reloadFile(); + } catch (IOException e) { + log.info("Could not load config", e); + throw new ConfigurationException("Could not load config file", e); + } + try { + save(); + } catch (IOException e) { + log.error("Could not save config.", e); + } + } + + private void save() throws IOException { + try (var sequenceWriter = objectMapper.writerWithDefaultPrettyPrinter() + .writeValues(getConfig().toFile())) { + sequenceWriter.write(configFile); + } + } + + private void reloadFile() throws IOException { + forceConsistency(); + configFile = objectMapper.readValue(getConfig().toFile(), ConfigFile.class); + } + + private void forceConsistency() throws IOException { + Files.createDirectories(getConfig().getParent()); + if (!getConfig().toFile().exists()) { + if (getConfig().toFile().createNewFile()) { + try(var sequenceWriter = objectMapper.writerWithDefaultPrettyPrinter() + .writeValues(getConfig().toFile())) { + sequenceWriter.write(new ConfigFile()); + throw new ConfigurationException("Please configure the config."); + } + } + } + } + + private Path getConfig() { + var home = new File(".").getAbsoluteFile().getParentFile().toPath(); + var property = System.getProperty("bot.config"); + if (property == null) { + log.error("bot.config property is not set."); + throw new ConfigurationException("Property -Dbot.config= is not set."); + } + return Paths.get(home.toString(), property); + } + + public Database database() { + return configFile.database(); + } + + public BaseSettings baseSettings() { + return configFile.baseSettings(); + } + + public Api api() { + return configFile.api(); + } + + public ServerManagement serverManagement() { + return configFile.serverManagement(); + } + + public Plugins plugins() { + return configFile.plugins(); + } + + public ServerTemplate serverTemplate() { + return configFile.serverTemplate(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Api.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Api.java new file mode 100644 index 0000000..3dfd6fd --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Api.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration.elements; + +@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) +public class Api { + private String host = "localhost"; + private int port = 8888; + private String token = "letmein"; + + public String host() { + return host; + } + + public int port() { + return port; + } + + public String token() { + return token; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/BaseSettings.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/BaseSettings.java new file mode 100644 index 0000000..a246f0b --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/BaseSettings.java @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration.elements; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) +public class BaseSettings { + private String token = ""; + private List botOwner = new ArrayList<>(); + + public String token() { + return token; + } + + public boolean isOwner(long id) { + return botOwner.contains(id); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Database.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Database.java new file mode 100644 index 0000000..b0b4c87 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Database.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration.elements; + + +@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) +public class Database { + private String host = "localhost"; + private String port = "5432"; + private String database = "db"; + private String schema = "bot"; + private String user = "root"; + private String password = "changeme"; + private int poolSize = 5; + + public String host() { + return host; + } + + public String port() { + return port; + } + + public String database() { + return database; + } + + public String schema() { + return schema; + } + + public String user() { + return user; + } + + public String password() { + return password; + } + + public int poolSize() { + return poolSize; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Plugins.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Plugins.java new file mode 100644 index 0000000..7ea8a94 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Plugins.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration.elements; + +import de.chojo.jdautil.container.Pair; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) +public class Plugins { + private String pluginDir = "plugins"; + + public String pluginDir() { + return pluginDir; + } + + private Path pluginPath() { + return Path.of(pluginDir); + } + + public List pluginFiles() { + return List.of(pluginPath().toFile().listFiles(File::isFile)); + } + + public List pluginNames() { + return pluginFiles().stream() + .map(File::getName) + .map(name -> name.replace(".jar", "")).toList(); + } + + public List> plugins() { + return pluginFiles().stream() + .map(file -> Pair.of(file.getName().replace(".jar", ""), file)) + .toList(); + } + + public Optional byName(String pluginName) { + return plugins().stream().filter(plugin -> plugin.first.equals(pluginName)) + .findFirst() + .map(plugin -> plugin.second.toPath()); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/ServerManagement.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/ServerManagement.java new file mode 100644 index 0000000..aa6207b --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/ServerManagement.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration.elements; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) +public class ServerManagement { + private String serverDir= "server"; + private int minPort = 30001; + private int maxPort = 30500; + private int velocityApi = 30000; + private int maxPlayers = 50; + private int memory = 1024; + + private List parameter = new ArrayList<>(); + + public int minPort() { + return minPort; + } + + public int maxPort() { + return maxPort; + } + + public int velocityApi() { + return velocityApi; + } + + public List parameter() { + return parameter; + } + + public int maxPlayers() { + return maxPlayers; + } + + public int memory() { + return memory; + } + + public String serverDir() { + return serverDir; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/ServerTemplate.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/ServerTemplate.java new file mode 100644 index 0000000..29d8fb5 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/ServerTemplate.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration.elements; + +import java.util.List; + +@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) +public class ServerTemplate { + private String templateDir = "template"; + private List symLinks = List.of( + "bukkit.yml", + "commands.yml", + "config/paper-global.yml", + "config/paper-world-defaults.yml", + "help.yml", + "permissions.yml", + "plugins/pluginjam.jar", + "server-icon.png", + "server.jar", + "server.properties", + "spigot.yml", + "wepif.yml"); + + public String templateDir() { + return templateDir; + } + + public List symLinks() { + return symLinks; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/exception/ConfigurationException.java b/bot/src/main/java/de/chojo/gamejam/configuration/exception/ConfigurationException.java new file mode 100644 index 0000000..b98e5e8 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/exception/ConfigurationException.java @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration.exception; + +public class ConfigurationException extends RuntimeException { + /** + * Constructs a new runtime exception with the specified detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public ConfigurationException(String message) { + super(message); + } + + /** + * Constructs a new runtime exception with the specified detail message and + * cause.

Note that the detail message associated with + * {@code cause} is not automatically incorporated in + * this runtime exception's detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/access/Guilds.java b/bot/src/main/java/de/chojo/gamejam/data/access/Guilds.java new file mode 100644 index 0000000..98cd3a2 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/access/Guilds.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.access; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import de.chojo.gamejam.data.dao.JamGuild; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.interactions.Interaction; + +import javax.sql.DataSource; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public class Guilds { + private final Cache cache = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES) + .build(); + private final DataSource dataSource; + + public Guilds(DataSource dataSource) { + this.dataSource = dataSource; + } + + public JamGuild guild(Interaction interaction) { + return guild(interaction.getGuild()); + } + + public JamGuild guild(Guild guild) { + try { + return cache.get(guild.getIdLong(), () -> new JamGuild(dataSource, guild)).refresh(guild); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + + } + public JamGuild guild(long guild) { + try { + return cache.get(guild, () -> new JamGuild(dataSource, guild)); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/access/Teams.java b/bot/src/main/java/de/chojo/gamejam/data/access/Teams.java new file mode 100644 index 0000000..d4f4727 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/access/Teams.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.access; + +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.sadu.base.QueryFactory; +import net.dv8tion.jda.api.sharding.ShardManager; +import org.slf4j.Logger; + +import javax.sql.DataSource; +import java.util.Optional; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Teams extends QueryFactory { + private static final Logger log = getLogger(Teams.class); + private final Guilds guilds; + private final ShardManager shardManager; + + public Teams(DataSource dataSource, Guilds guilds, ShardManager shardManager) { + super(dataSource); + this.guilds = guilds; + this.shardManager = shardManager; + } + + public Optional byId(int id) { + return builder(Team.class) + .query(""" + SELECT guild_id + FROM team t + LEFT JOIN jam j ON t.jam_id = j.id + WHERE t.id = ? + """) + .parameter(stmt -> stmt.setInt(id)) + .readRow(row -> { + var guildId = row.getLong("guild_id"); + var guildById = shardManager.getGuildById(guildId); + if (guildById == null) { + log.warn("Could not find guild with id {}", guildId); + return guilds.guild(guildId).teams().byId(id).orElse(null); + } + return guilds.guild(guildById).teams().byId(id).orElse(null); + }) + .firstSync(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/JamGuild.java b/bot/src/main/java/de/chojo/gamejam/data/dao/JamGuild.java new file mode 100644 index 0000000..7316e22 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/JamGuild.java @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao; + +import de.chojo.gamejam.data.dao.guild.JamSettings; +import de.chojo.gamejam.data.dao.guild.Jams; +import de.chojo.gamejam.data.dao.guild.Settings; +import de.chojo.gamejam.data.dao.guild.Teams; +import de.chojo.sadu.base.QueryFactory; +import net.dv8tion.jda.api.entities.Guild; + +import javax.sql.DataSource; + +public class JamGuild extends QueryFactory { + private final long guildId; + private Guild guild; + private final Jams jams; + private final Teams teams; + + public JamGuild(DataSource dataSource, Guild guild) { + super(dataSource); + this.guild = guild; + this.guildId = guild.getIdLong(); + jams = new Jams(this); + teams = new Teams(this); + } + + public JamGuild(DataSource dataSource, long guild) { + super(dataSource); + this.guild = null; + this.guildId = guild; + jams = new Jams(this); + teams = new Teams(this); + } + + public Settings settings() { + return builder(Settings.class) + .query("SELECT manager_role, locale FROM settings WHERE guild_id = ?") + .parameter(p -> p.setLong(guild.getIdLong())) + .readRow(r -> new Settings(this, guild.getIdLong(), r.getString("locale"), r.getLong("manager_role"))) + .firstSync() + .orElseGet(() -> new Settings(this, guild.getIdLong())); + } + + public JamSettings jamSettings() { + return builder(JamSettings.class) + .query("SELECT jam_role, team_size FROM jam_settings WHERE guild_id = ?") + .parameter(p -> p.setLong(guild.getIdLong())) + .readRow(r -> new JamSettings(this, r.getInt("team_size"), r.getLong("jam_role"))) + .firstSync() + .orElseGet(() -> new JamSettings(this)); + } + + public JamGuild refresh(Guild guild) { + this.guild = guild; + return this; + } + + public Jams jams() { + return jams; + } + + public Teams teams() { + return teams; + } + + public Guild guild() { + return guild; + } + + public long guildId() { + return guildId; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/JamSettings.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/JamSettings.java new file mode 100644 index 0000000..00698c6 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/JamSettings.java @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild; + +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.sadu.base.QueryFactory; +import de.chojo.sadu.exceptions.ThrowingConsumer; +import de.chojo.sadu.wrapper.util.ParamBuilder; +import net.dv8tion.jda.api.entities.Role; + +import java.sql.SQLException; + +public class JamSettings extends QueryFactory { + private final JamGuild jamGuild; + private int teamSize; + private long jamRole; + + public JamSettings(JamGuild jamGuild) { + this(jamGuild, 4, 0); + } + + public JamSettings(JamGuild jamGuild, int teamSize, long jamRole) { + super(jamGuild); + this.jamGuild = jamGuild; + this.teamSize = teamSize; + this.jamRole = jamRole; + } + + public int teamSize() { + return teamSize; + } + + public long jamRole() { + return jamRole; + } + + public void teamSize(int teamSize) { + if (set("team_size", stmt -> stmt.setInt(teamSize))) { + this.teamSize = teamSize; + } + } + + public void jamRole(Role jamRole) { + if (set("jam_role", stmt -> stmt.setLong(jamRole.getIdLong()))) { + this.jamRole = jamRole.getIdLong(); + } + } + + private boolean set(String column, ThrowingConsumer stmt) { + return builder() + .query(""" + INSERT INTO jam_settings(guild_id, %s) VALUES(?,?) + ON CONFLICT(guild_id) + DO UPDATE + SET %s = excluded.%s + """, column, column, column) + .parameter(p -> { + p.setLong(jamGuild.guildId()); + stmt.accept(p); + }) + .update() + .sendSync() + .changed(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/Jams.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/Jams.java new file mode 100644 index 0000000..f3b7ff7 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/Jams.java @@ -0,0 +1,120 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild; + +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.gamejam.data.dao.guild.jams.Jam; +import de.chojo.gamejam.data.wrapper.jam.JamCreator; +import de.chojo.sadu.base.QueryFactory; + +import java.util.Optional; + +public class Jams extends QueryFactory { + + private final JamGuild jamGuild; + + public Jams(JamGuild jamGuild) { + super(jamGuild); + this.jamGuild = jamGuild; + } + + public void create(JamCreator jamCreator) { + builder(Integer.class) + .query("INSERT INTO jam(guild_id) VALUES(?) RETURNING id") + .parameter(stmt -> stmt.setLong(jamGuild.guildId())) + .readRow(r -> r.getInt("id")) + .firstSync() + .map(id -> { + var times = jamCreator.times(); + builder().query(""" + INSERT INTO jam_time( + jam_id, + registration_start, registration_end, + jam_start, jam_end, + zone_id) + VALUES (?,?,?,?,?,?) + """) + .parameter(stmt -> stmt.setInt(id) + .setTimestamp(times.registration().startTimestamp()) + .setTimestamp(times.registration().endTimestamp()) + .setTimestamp(times.jam().startTimestamp()) + .setTimestamp(times.jam().endTimestamp()) + .setString(times.zone().getId()) + ).append() + .query("INSERT INTO jam_meta(jam_id, topic) VALUES(?,?)") + .parameter(stmt -> stmt.setInt(id).setString(jamCreator.topic())) + .append() + .query("INSERT INTO jam_state(jam_id) VALUES(?)") + .parameter(stmt -> stmt.setInt(id)) + .insert() + .sendSync(); + return id; + }); + } + + public Optional getCurrentJam() { + return builder(Jam.class) + .query(""" + SELECT + id + FROM jam_time t + LEFT JOIN jam j ON j.id = t.jam_id + WHERE registration_start < NOW() AT TIME ZONE 'utc' + AND t.jam_end > NOW() AT TIME ZONE 'utc' + AND guild_id = ?; + """) + .parameter(stmt -> stmt.setLong(jamGuild.guildId())) + .readRow(r -> new Jam(jamGuild, r.getInt("id"))) + .firstSync(); + } + + public Optional nextOrCurrent() { + return builder(Integer.class) + .query(""" + SELECT + id + FROM jam_time t + LEFT JOIN jam j ON j.id = t.jam_id + LEFT JOIN jam_state js ON j.id = js.jam_id + WHERE js.active OR t.jam_end > NOW() AT TIME ZONE 'utc' + AND guild_id = ? + ORDER BY t.jam_end ASC + LIMIT 1; + """) + .parameter(stmt -> stmt.setLong(jamGuild.guildId())) + .readRow(r -> r.getInt("id")) + .firstSync() + .flatMap(this::byId); + } + + public Optional activeJam() { + return builder(Integer.class) + .query(""" + SELECT + id + FROM jam_state s + LEFT JOIN jam j ON j.id = s.jam_id + WHERE s.active + AND guild_id = ? + LIMIT 1; + """) + .parameter(stmt -> stmt.setLong(jamGuild.guildId())) + .readRow(r -> r.getInt("id")) + .firstSync() + .flatMap(this::byId); + } + + public Optional byId(int id) { + return builder(Jam.class) + .query(""" + SELECT id FROM jam WHERE guild_id = ? AND id = ? + """) + .parameter(stmt -> stmt.setLong(jamGuild.guildId()).setInt(id)) + .readRow(row -> new Jam(jamGuild, id)) + .firstSync(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/Settings.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/Settings.java new file mode 100644 index 0000000..6f6ebd3 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/Settings.java @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild; + +import de.chojo.sadu.base.QueryFactory; +import de.chojo.sadu.exceptions.ThrowingConsumer; +import de.chojo.sadu.wrapper.util.ParamBuilder; + +import java.sql.SQLException; + +public class Settings extends QueryFactory { + private final long guildId; + private String locale = "en_US"; + private long orgaRole = 0; + + public Settings(QueryFactory queryFactory, long guildId) { + super(queryFactory); + this.guildId = guildId; + } + + public Settings(QueryFactory queryFactory, long guildId, String locale, long orgaRole) { + super(queryFactory); + this.guildId = guildId; + this.locale = locale; + this.orgaRole = orgaRole; + } + + public String locale() { + return locale; + } + + public long orgaRole() { + return orgaRole; + } + + public void locale(String locale) { + if (set("locale", stmt -> stmt.setString(locale))) { + this.locale = locale; + } + } + + public void orgaRole(long orgaRole) { + if (set("manager_role", stmt -> stmt.setLong(orgaRole))) { + this.orgaRole = orgaRole; + } + } + + public Long guildId() { + return guildId; + } + + private boolean set(String column, ThrowingConsumer stmt) { + return builder() + .query(""" + INSERT INTO settings(guild_id, %s) VALUES(?,?) + ON CONFLICT(guild_id) + DO UPDATE + SET %s = excluded.%s + """, column, column, column) + .parameter(p -> { + p.setLong(guildId()); + stmt.accept(p); + }) + .update() + .sendSync() + .changed(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/Teams.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/Teams.java new file mode 100644 index 0000000..f2d375d --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/Teams.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild; + +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.sadu.base.QueryFactory; + +import java.util.Optional; + +public class Teams extends QueryFactory { + private final JamGuild guild; + + public Teams(JamGuild guild) { + super(guild); + this.guild = guild; + } + + public Optional byId(int id) { + return builder(Team.class) + .query("SELECT jam_id, id, team_name FROM team t LEFT JOIN team_meta m ON t.id = m.team_id WHERE id = ?") + .parameter(stmt -> stmt.setInt(id)) + .readRow(r -> { + var team = guild.jams().byId(r.getInt("jam_id")).orElse(null); + return new Team(team, r.getInt("id")); + }) + .firstSync(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/Jam.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/Jam.java new file mode 100644 index 0000000..855e4c5 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/Jam.java @@ -0,0 +1,130 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams; + +import de.chojo.gamejam.data.dao.JamGuild; +import de.chojo.gamejam.data.dao.guild.jams.jam.JamMeta; +import de.chojo.gamejam.data.dao.guild.jams.jam.JamState; +import de.chojo.gamejam.data.dao.guild.jams.jam.JamTimes; +import de.chojo.gamejam.data.dao.guild.jams.jam.JamTeams; +import de.chojo.gamejam.data.dao.guild.jams.jam.user.JamUser; +import de.chojo.gamejam.data.wrapper.jam.TimeFrame; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.team.TeamVote; +import de.chojo.sadu.base.QueryFactory; +import net.dv8tion.jda.api.entities.Member; + +import java.time.ZoneId; +import java.util.List; + +public class Jam extends QueryFactory { + private final JamGuild jamGuild; + private final int id; + private final JamTeams jamTeams; + + public Jam(JamGuild jamGuild, int id) { + super(jamGuild); + this.jamGuild = jamGuild; + this.id = id; + jamTeams = new JamTeams(this); + } + + public void register(Member member) { + builder() + .query("INSERT INTO jam_registrations(jam_id, user_id) VALUES(?,?) ON CONFLICT DO NOTHING") + .parameter(stmt -> stmt.setInt(id).setLong(member.getIdLong())) + .insert() + .sendSync(); + } + + public JamMeta meta() { + return builder(JamMeta.class) + .query(""" + SELECT topic FROM jam_meta WHERE jam_id = ? + """) + .parameter(stmt -> stmt.setInt(id)) + .readRow(row -> new JamMeta(row.getString("topic"))) + .firstSync() + .orElseThrow(); + } + + public List registrations() { + return builder(Long.class) + .query("SELECT user_id FROM jam_registrations WHERE jam_id = ?") + .parameter(stmt -> stmt.setInt(id)) + .readRow(row -> row.getLong("user_id")) + .allSync(); + } + + public JamTimes times() { + return builder(JamTimes.class) + .query(""" + SELECT registration_start, + registration_end, + zone_id, + jam_start, + jam_end + FROM jam_time + WHERE jam_id = ? + """) + .parameter(stmt -> stmt.setInt(id)) + .readRow(r -> { + var zone = ZoneId.of(r.getString("zone_id")); + return new JamTimes(zone, + TimeFrame.fromTimestamp(r.getTimestamp("registration_start"), + r.getTimestamp("registration_end"), zone), + TimeFrame.fromTimestamp(r.getTimestamp("jam_start"), + r.getTimestamp("jam_end"), zone) + ); + }) + .firstSync() + .orElseThrow(); + } + + public JamState state() { + return builder(JamState.class) + .query(""" + SELECT active, + voting, + ended + FROM jam_state + WHERE jam_id = ? + """) + .parameter(stmt -> stmt.setInt(jamId())) + .readRow(r -> new JamState(this, r.getBoolean("active"), r.getBoolean("voting"), r.getBoolean("ended"))) + .firstSync() + .orElseThrow(); + } + + public List votes() { + return builder(TeamVote.class) + .query(""" + SELECT + rank, team_id, points, jam_id + FROM team_ranking r + WHERE r.jam_id = ? + """) + .parameter(p -> p.setInt(id)) + .readRow(r -> new TeamVote(jamTeams.byId(r.getInt("team_id")).orElseThrow(), r.getInt("rank"), r.getInt("points"))) + .allSync(); + } + + public JamTeams teams() { + return jamTeams; + } + + public int jamId() { + return id; + } + + public JamGuild jamGuild() { + return jamGuild; + } + + public JamUser user(Member member) { + return new JamUser(this, member); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamMeta.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamMeta.java new file mode 100644 index 0000000..c5b2850 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamMeta.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams.jam; + +public class JamMeta { + private final String topic; + + public JamMeta(String topic) { + this.topic = topic; + } + + public String topic() { + return topic; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamState.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamState.java new file mode 100644 index 0000000..799f5f9 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamState.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams.jam; + +import de.chojo.gamejam.data.dao.guild.jams.Jam; +import de.chojo.sadu.base.QueryFactory; + +public class JamState extends QueryFactory { + private final Jam jam; + private boolean active; + private boolean voting; + private boolean ended; + + public JamState(Jam jam, boolean active, boolean voting, boolean ended) { + super(jam); + this.jam = jam; + this.active = active; + this.voting = voting; + this.ended = ended; + } + + public boolean isActive() { + return active; + } + + public boolean isVoting() { + return voting; + } + + public boolean hasEnded() { + return ended; + } + + public void active(boolean active) { + if (set("active", active)) { + this.active = active; + } + } + + public void voting(boolean voting) { + if (set("voting", voting)) { + this.voting = voting; + } + } + + public void ended(boolean ended) { + if (set("ended", ended)) { + this.ended = ended; + } + } + + public void finish(){ + active(false); + voting(false); + ended(true); + + for (var team : jam.teams().teams()) { + team.delete(); + } + } + + private boolean set(String column, boolean state) { + return builder() + .query(""" + INSERT INTO jam_state(jam_id, %s) VALUES(?,?) + ON CONFLICT(jam_id) + DO UPDATE + SET %s = excluded.%s + """, column, column, column) + .parameter(p -> p.setInt(jam.jamId()).setBoolean(state)) + .update() + .sendSync() + .changed(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamTeams.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamTeams.java new file mode 100644 index 0000000..a6aea88 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamTeams.java @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams.jam; + +import de.chojo.gamejam.data.dao.guild.jams.Jam; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.sadu.base.QueryFactory; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.interactions.commands.Command; + +import java.util.List; +import java.util.Optional; + +public class JamTeams extends QueryFactory { + private final Jam jam; + + public JamTeams(Jam jam) { + super(jam); + this.jam = jam; + } + + public Team create(String name) { + var teamId = builder(Integer.class) + .query(""" + INSERT INTO team(jam_id) VALUES(?) RETURNING id AS team_id; + """) + .parameter(stmt -> stmt.setInt(jam.jamId())) + .readRow(row -> row.getInt("team_id")) + .firstSync() + .orElseThrow(); + builder().query(""" + INSERT INTO team_meta(team_id, team_name) VALUES (?,?); + """) + .parameter(stmt -> stmt.setInt(teamId).setString(name)) + .insert() + .sendSync(); + return new Team(jam, teamId); + } + + public List teams() { + return builder(Team.class) + .query(""" + SELECT id, + team_name + FROM team t + LEFT JOIN team_meta m ON t.id = m.team_id + WHERE jam_id = ? + """) + .parameter(stmt -> stmt.setInt(jam.jamId())) + .readRow(r -> new Team(jam, r.getInt("id"))) + .allSync(); + } + + public Optional byMember(Member member) { + return byMember(member.getUser()); + } + + public Optional byMember(User member) { + return builder(Integer.class) + .query(""" + SELECT m.team_id + FROM team_member m + LEFT JOIN team t ON t.id = m.team_id + LEFT JOIN jam j ON j.id = t.jam_id + WHERE j.id = ? + AND user_id = ? + """) + .parameter(p -> p.setInt(jam.jamId()).setLong(member.getIdLong())) + .readRow(r -> r.getInt("team_id")) + .firstSync() + .flatMap(this::byId); + } + + public Optional byName(String name) { + return builder(Integer.class) + .query(""" + SELECT id FROM team t LEFT JOIN team_meta m ON t.id = m.team_id + WHERE jam_id = ? + AND LOWER(m.team_name) = LOWER(?) + """) + .parameter(p -> p.setInt(jam.jamId()).setString(name)) + .readRow(r -> r.getInt("id")) + .firstSync() + .flatMap(this::byId); + } + + public Optional byId(int id) { + return builder(Team.class) + .query("SELECT id, team_name FROM team t LEFT JOIN team_meta m ON t.id = m.team_id WHERE id = ?") + .parameter(stmt -> stmt.setInt(id)) + .readRow(r -> new Team(jam, r.getInt("id"))) + .firstSync(); + } + + public List completeTeam(String name) { + if (name.isBlank()) { + return teams().stream() + .map(team -> team.meta().name()) + .map(team -> new Command.Choice(team, team)) + .toList(); + } + + return teams().stream() + .filter(team -> team.matchName(name)) + .map(team -> team.meta().name()) + .map(team -> new Command.Choice(team, team)) + .toList(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamTimes.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamTimes.java new file mode 100644 index 0000000..791d2ae --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/JamTimes.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams.jam; + +import de.chojo.gamejam.data.wrapper.jam.TimeFrame; + +import java.time.ZoneId; + +public class JamTimes { + private final ZoneId zone; + private final TimeFrame total; + private final TimeFrame registration; + private final TimeFrame jam; + + + public JamTimes(ZoneId zone, TimeFrame registration, TimeFrame jam) { + this.zone = zone; + this.registration = registration; + this.jam = jam; + total = new TimeFrame(registration.start(), jam.end()); + } + + public TimeFrame total() { + return total; + } + + public TimeFrame registration() { + return registration; + } + + public TimeFrame jam() { + return jam; + } + + public ZoneId zone() { + return zone; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/Team.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/Team.java new file mode 100644 index 0000000..f329c56 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/Team.java @@ -0,0 +1,196 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams.jam.teams; + +import de.chojo.gamejam.data.dao.guild.jams.Jam; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.team.TeamMember; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.team.TeamMeta; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.team.TeamVote; +import de.chojo.jdautil.localization.LocalizationContext; +import de.chojo.jdautil.localization.util.LocalizedEmbedBuilder; +import de.chojo.jdautil.util.MentionUtil; +import de.chojo.sadu.base.QueryFactory; +import net.dv8tion.jda.api.entities.ISnowflake; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class Team extends QueryFactory { + private final Jam jam; + private final int id; + private TeamMeta meta; + + public Team(Jam jam, int id) { + super(jam); + this.jam = jam; + this.id = id; + } + + public void delete() { + var meta = meta(); + meta.textChannel().ifPresent(channel -> channel.delete().queue()); + meta.voiceChannel().ifPresent(channel -> channel.delete().queue()); + meta.role().ifPresent(role -> role.delete().queue()); + } + + public MessageEmbed profileEmbed(LocalizationContext localizer) { + + var member = member().stream() + .map(u -> u.member().getAsMention()) + .collect(Collectors.joining(", ")); + + var meta = meta(); + return new LocalizedEmbedBuilder(localizer) + .setTitle(meta.name()) + .setDescription(meta.projectDescription()) + .addField("command.team.profile.member", member, true) + .addField("command.team.profile.leader", MentionUtil.user(meta.leader()), true) + .addField("command.team.profile.projecturl", meta.projectUrl(), true) + .setFooter(String.format("#%s", id())) + .build(); + } + + public List member() { + return builder(TeamMember.class) + .query("SELECT user_id FROM team_member WHERE team_id = ?") + .parameter(p -> p.setInt(id())) + .readRow(r -> { + try { + var member = jam.jamGuild().guild().retrieveMemberById(r.getLong("user_id")).complete(); + return new TeamMember(this, member); + } catch (RuntimeException e) { + return null; + } + }) + .allSync() + .stream() + .filter(Objects::nonNull) + .toList(); + } + + public int id() { + return id; + } + + public boolean matchName(String name) { + if (name.isBlank()) return true; + return meta().name().toLowerCase(Locale.ROOT).contains(name.toLowerCase(Locale.ROOT)); + } + + public boolean isLeader(ISnowflake snowflake) { + return meta().leader() == snowflake.getIdLong(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Team team)) return false; + + return id == team.id; + } + + @Override + public int hashCode() { + return id; + } + + public List votes() { + return builder(TeamVote.class) + .query(""" + SELECT + rank, team_id, points, jam_id + FROM team_ranking + WHERE team_id = ? + """) + .parameter(p -> p.setInt(id())) + .readRow(r -> new TeamVote(this, r.getInt("rank"), r.getInt("points"))) + .allSync(); + } + + public boolean vote(Member member, int points) { + return builder() + .query(""" + INSERT INTO vote(team_id, voter_id, points) VALUES (?,?,?) + ON CONFLICT (team_id, voter_id) + DO UPDATE SET points = excluded.points; + """) + .parameter(p -> p.setInt(id()).setLong(member.getIdLong()).setInt(points)) + .insert() + .sendSync() + .changed(); + } + + + public Jam jam() { + return jam; + } + + public boolean disband() { + delete(); + return builder() + .query("DELETE FROM team WHERE id = ?") + .parameter(p -> p.setInt(id())) + .insert() + .sendSync() + .changed(); + } + + public TeamMeta meta() { + if (meta == null) { + meta = builder(TeamMeta.class) + .query(""" + SELECT team_name, + leader_id, + role_id, + text_channel_id, + voice_channel_id, + project_description, + project_url + FROM team_meta WHERE team_id = ? + """) + .parameter(stmt -> stmt.setInt(id)) + .readRow(row -> new TeamMeta(this, + row.getString("team_name"), + row.getLong("leader_id"), + row.getLong("role_id"), + row.getLong("text_channel_id"), + row.getLong("voice_channel_id"), + row.getString("project_description"), + row.getString("project_url"))) + .firstSync() + .orElseThrow(); + } + return meta; + } + + public Optional member(Member member) { + return builder(TeamMember.class) + .query("SELECT user_id FROM team_member WHERE team_id = ? AND user_id = ?") + .parameter(p -> p.setInt(id()).setLong(member.getIdLong())) + .readRow(r -> new TeamMember(this, member)) + .firstSync(); + } + + @Override + public String toString() { + return "%s (%s)".formatted(meta().name(), id); + } + + public Integer votes(Member member) { + return builder(Integer.class) + .query("SELECT points FROM vote WHERE team_id = ? AND voter_id = ?") + .parameter(stmt -> stmt.setInt(id()).setLong(member.getIdLong())) + .readRow(r -> r.getInt("vote")) + .firstSync() + .orElse(0); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/team/TeamMember.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/team/TeamMember.java new file mode 100644 index 0000000..b3e0002 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/team/TeamMember.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams.jam.teams.team; + +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.sadu.base.QueryFactory; +import net.dv8tion.jda.api.entities.Member; + +public final class TeamMember extends QueryFactory { + private final Team team; + private final Member member; + + public TeamMember(Team team, Member member) { + super(team); + this.team = team; + this.member = member; + } + + public Team team() { + return team; + } + + public Member member() { + return member; + } + + public boolean leave() { + var guild = team.jam().jamGuild().guild(); + var roleById = team.meta().role(); + + roleById.ifPresent(role -> guild.removeRoleFromMember(member, role).queue()); + return builder() + .query("DELETE FROM team_member WHERE team_id = ? AND user_id = ?") + .parameter(p -> p.setInt(team.id()).setLong(member.getIdLong())) + .insert() + .sendSync() + .changed(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/team/TeamMeta.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/team/TeamMeta.java new file mode 100644 index 0000000..6f72395 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/team/TeamMeta.java @@ -0,0 +1,155 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams.jam.teams.team; + +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.sadu.base.QueryFactory; +import de.chojo.sadu.exceptions.ThrowingConsumer; +import de.chojo.sadu.wrapper.util.ParamBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; + +import java.sql.SQLException; +import java.util.Optional; + +public class TeamMeta extends QueryFactory { + private final Team team; + private String name; + private long leader; + private long role; + private long textChannel; + private long voiceChannel; + private String projectDescription; + private String projectUrl; + + + public TeamMeta(Team team, String name, long leader, long role, long textChannel, long voiceChannel, String projectDescription, String projectUrl) { + super(team); + this.team = team; + this.name = name; + this.leader = leader; + this.role = role; + this.textChannel = textChannel; + this.voiceChannel = voiceChannel; + this.projectDescription = projectDescription; + this.projectUrl = projectUrl; + } + + public long leader() { + return leader; + } + + public void leader(Member leader) { + if (set("leader_id", leader.getIdLong())) { + this.leader = leader.getIdLong(); + } + } + + public Optional role() { + return Optional.ofNullable(guild().getRoleById(role)); + } + + public Optional textChannel() { + return Optional.ofNullable(guild().getTextChannelById(textChannel)); + } + + private Guild guild() { + return team.jam().jamGuild().guild(); + } + + public Optional voiceChannel() { + return Optional.ofNullable(guild().getVoiceChannelById(voiceChannel)); + } + + public void role(Role role) { + if (set("role_id", role.getIdLong())) { + this.role = role.getIdLong(); + } + } + + public void textChannel(TextChannel textChannel) { + if (set("text_channel_id", textChannel.getIdLong())) { + this.textChannel = textChannel.getIdLong(); + } + } + + public void voiceChannel(VoiceChannel voiceChannel) { + if (set("voice_channel_id", voiceChannel.getIdLong())) { + this.voiceChannel = voiceChannel.getIdLong(); + } + } + + + public String projectDescription() { + return projectDescription; + } + + public void projectDescription(String projectDescription) { + if (set("project_description", projectDescription)) { + this.projectDescription = projectDescription; + } + } + + public String projectUrl() { + return projectUrl; + } + + public void projectUrl(String projectUrl) { + if (set("project_url", projectUrl)) { + this.projectUrl = projectUrl; + } + } + + public String name() { + return name; + } + + public void rename(String name) { + var changed = builder() + .query(""" + UPDATE team_meta SET team_name = ? WHERE team_id = ? + """) + .parameter(stmt -> stmt.setString(name).setInt(team.id())) + .update() + .sendSync() + .changed(); + if (changed) { + this.name = name; + role().ifPresent(role -> role.getManager().setName(name()).queue()); + textChannel().ifPresent(channel -> channel.getManager().setName(name().replace(" ", "-")).queue()); + voiceChannel().ifPresent(channel -> channel.getManager().setName(name()).queue()); + } + } + + private boolean set(String column, long value) { + return set(column, p -> p.setLong(value)); + } + + private boolean set(String column, String value) { + return set(column, p -> p.setString(value)); + } + + private boolean set(String column, ThrowingConsumer value) { + return builder() + .query(""" + INSERT INTO team_meta(team_id, team_name, %s) VALUES(?,'',?) + ON CONFLICT(team_id) + DO UPDATE + SET %s = excluded.%s + """, column, column, column) + .parameter(p -> { + p.setInt(team.id()); + value.accept(p); + }) + .update() + .sendSync() + .changed(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/team/TeamVote.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/team/TeamVote.java new file mode 100644 index 0000000..a6de353 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/teams/team/TeamVote.java @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams.jam.teams.team; + + +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; + +public record TeamVote(Team team, int rank, int votes) { +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/user/JamUser.java b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/user/JamUser.java new file mode 100644 index 0000000..1667ee1 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/dao/guild/jams/jam/user/JamUser.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.dao.guild.jams.jam.user; + +import de.chojo.gamejam.data.dao.guild.jams.Jam; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.gamejam.data.wrapper.votes.VoteEntry; +import de.chojo.sadu.base.QueryFactory; +import net.dv8tion.jda.api.entities.Member; + +import java.util.List; +import java.util.Optional; + +public class JamUser extends QueryFactory { + private final Jam jam; + private final Member member; + + public JamUser(Jam jam, Member member) { + super(jam); + this.jam = jam; + this.member = member; + } + + public List votes() { + return builder(VoteEntry.class) + .query(""" + SELECT + v.team_id, + v.voter_id, + v.points + FROM vote v + LEFT JOIN team t ON t.id = v.team_id + WHERE t.jam_id = ? + AND voter_id = ? + """) + .parameter(p -> p.setInt(jam.jamId()).setLong(member.getIdLong())) + .readRow(r -> new VoteEntry(jam.teams().byId(r.getInt("team_id")).orElseThrow(), + r.getLong("voter_id"), r.getInt("points"))) + .allSync(); + } + + public int votesGiven() { + return builder(Integer.class) + .query(""" + SELECT + sum(points) + FROM vote v + LEFT JOIN team t ON t.id = v.team_id + WHERE t.jam_id = ? + AND voter_id = ? + """) + .parameter(p -> p.setInt(jam.jamId()).setLong(member.getIdLong())) + .readRow(r -> r.getInt("points")) + .firstSync() + .orElse(0); + } + + public Optional team() { + return jam.teams().byMember(member); + } + + public boolean join(Team team) { + var guild = team.jam().jamGuild().guild(); + var roleById = team.meta().role(); + + roleById.ifPresent(role -> guild.addRoleToMember(member, role).queue()); + + return builder() + .query("INSERT INTO team_member(team_id, user_id) VALUES(?,?)") + .parameter(p -> p.setInt(team.id()).setLong(member.getIdLong())) + .insert() + .sendSync() + .changed(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/wrapper/jam/JamBuilder.java b/bot/src/main/java/de/chojo/gamejam/data/wrapper/jam/JamBuilder.java new file mode 100644 index 0000000..7072d36 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/wrapper/jam/JamBuilder.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.wrapper.jam; + +import de.chojo.gamejam.data.dao.guild.jams.jam.JamTimes; + +public class JamBuilder { + private JamTimes times; + private String topic; + + public JamBuilder setTimes(JamTimes times) { + this.times = times; + return this; + } + + public JamBuilder setTopic(String topic) { + this.topic = topic; + return this; + } + + public JamCreator build() { + return new JamCreator(times, topic); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/wrapper/jam/JamCreator.java b/bot/src/main/java/de/chojo/gamejam/data/wrapper/jam/JamCreator.java new file mode 100644 index 0000000..3ce098e --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/wrapper/jam/JamCreator.java @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.wrapper.jam; + +import de.chojo.gamejam.data.dao.guild.jams.jam.JamTimes; + +public record JamCreator(JamTimes times, String topic) { + + public static JamBuilder create() { + return new JamBuilder(); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/wrapper/jam/TimeFrame.java b/bot/src/main/java/de/chojo/gamejam/data/wrapper/jam/TimeFrame.java new file mode 100644 index 0000000..84d3272 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/wrapper/jam/TimeFrame.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.wrapper.jam; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public record TimeFrame(ZonedDateTime start, ZonedDateTime end) { + public TimeFrame { + if (start.isAfter(end)) { + throw new IllegalStateException("Start time is after end time."); + } + } + + public static TimeFrame fromEpoch(long start, long end, ZoneId zone) { + return new TimeFrame(ZonedDateTime.ofInstant(Instant.ofEpochSecond(start), zone), + ZonedDateTime.ofInstant(Instant.ofEpochSecond(end), zone)); + } + + public static TimeFrame fromTimestamp(Timestamp start, Timestamp end, ZoneId zoneId) { + return new TimeFrame(ZonedDateTime.ofInstant(Instant.ofEpochMilli(start.getTime()), zoneId), + ZonedDateTime.ofInstant(Instant.ofEpochMilli(end.getTime()), zoneId)); + } + + public long epochStart() { + return start.toEpochSecond(); + } + + public long epochEnd() { + return end.toEpochSecond(); + } + + public boolean contains(ZonedDateTime time) { + return time.isAfter(start) && time.isBefore(end) || time.isEqual(start) || time.isEqual(end); + } + + public Timestamp startTimestamp() { + return Timestamp.from(Instant.ofEpochSecond(epochStart())); + } + + public Timestamp endTimestamp() { + return Timestamp.from(Instant.ofEpochSecond(epochEnd())); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/data/wrapper/votes/VoteEntry.java b/bot/src/main/java/de/chojo/gamejam/data/wrapper/votes/VoteEntry.java new file mode 100644 index 0000000..8b4c260 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/data/wrapper/votes/VoteEntry.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.wrapper.votes; + +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; + +public record VoteEntry(Team team, long voterId, int points) { +} diff --git a/bot/src/main/java/de/chojo/gamejam/server/ServerService.java b/bot/src/main/java/de/chojo/gamejam/server/ServerService.java new file mode 100644 index 0000000..5b0183e --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/server/ServerService.java @@ -0,0 +1,139 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.server; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.chojo.gamejam.configuration.Configuration; +import de.chojo.gamejam.data.access.Teams; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.gamejam.util.Mapper; +import de.chojo.pluginjam.payload.Registration; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import static org.slf4j.LoggerFactory.getLogger; + +public class ServerService implements Runnable { + private static final Logger log = getLogger(ServerService.class); + private final Map server = new HashMap<>(); + private Teams teams; + private final Configuration configuration; + private final Stack freePorts = new Stack<>(); + + public static ServerService create(ScheduledExecutorService executorService, Configuration configuration) { + var serverService = new ServerService(configuration); + executorService.scheduleAtFixedRate(serverService, 10, 10, TimeUnit.SECONDS); + return serverService; + } + + private ServerService(Configuration configuration) { + this.configuration = configuration; + IntStream.rangeClosed(configuration.serverManagement().minPort(), configuration.serverManagement().maxPort()) + .forEach(freePorts::add); + } + + @Override + public void run() { + for (var value : server.values()) { + if (!value.running()) continue; + try { + value.serverRequests() + .ifPresent(server -> { + if (server.restart()) { + log.info("Server of team {} requested restart", value.team()); + value.restart(); + } + }); + } catch (RuntimeException e) { + log.error("Could not reach server {}", value); + } + } + } + + public void syncVelocity() { + log.info("Syncing server with velocity instance."); + freePorts.clear(); + IntStream.rangeClosed(configuration.serverManagement().minPort(), configuration.serverManagement().maxPort()) + .forEach(freePorts::add); + var velocityApi = configuration.serverManagement().velocityApi(); + var httpClient = HttpClient.newHttpClient(); + var req = HttpRequest.newBuilder(URI.create("http://localhost:%d/v1/server".formatted(velocityApi))) + .GET() + .build(); + HttpResponse response; + try { + response = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + log.error("Could not reach velocity inteance", e); + return; + } catch (InterruptedException e) { + log.error("Interrupted", e); + return; + } + var collectionType = Mapper.MAPPER.getTypeFactory() + .constructCollectionType(List.class, Registration.class); + List registrations; + try { + registrations = Mapper.MAPPER.readValue(response.body(), collectionType); + } catch (JsonProcessingException e) { + log.error("Could not map response"); + throw new RuntimeException(e); + } + + server.clear(); + for (var registration : registrations) { + var optTeam = teams.byId(registration.id()); + if (optTeam.isEmpty()) { + log.warn("Could not find a matching team for id {} of team {}", registration.id(), registration.name()); + return; + } + var team = optTeam.get(); + log.info("Registered server for team {} with id {}", team.meta().name(), team.id()); + var teamServer = new TeamServer(this, team, configuration, registration.port(), registration.apiPort()); + teamServer.running(true); + server.put(team, teamServer); + freePorts.removeElement(registration.apiPort()); + freePorts.removeElement(registration.port()); + } + } + + public TeamServer get(Team team) { + return server.computeIfAbsent(team, key -> new TeamServer(this, key, configuration, nextPort(), nextPort())); + } + + private int nextPort() { + if (!freePorts.isEmpty()) { + return freePorts.pop(); + } + throw new RuntimeException("Ports exhausted"); + } + + void stopped(TeamServer server, boolean restart) { + this.server.remove(server.team()); + freePorts.push(server.port()); + freePorts.push(server.apiPort()); + if (restart) { + get(server.team()).start(); + } + } + + public void inject(Teams teams) { + this.teams = teams; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java b/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java new file mode 100644 index 0000000..49258bd --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java @@ -0,0 +1,504 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.server; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.chojo.gamejam.configuration.Configuration; +import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.gamejam.util.Mapper; +import de.chojo.jdautil.localization.util.LocalizedEmbedBuilder; +import de.chojo.jdautil.util.Futures; +import de.chojo.jdautil.wrapper.EventContext; +import de.chojo.pluginjam.payload.RequestsPayload; +import de.chojo.pluginjam.payload.StatsPayload; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.lingala.zip4j.ZipFile; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import static org.slf4j.LoggerFactory.getLogger; + +public class TeamServer { + private static final HttpClient http = HttpClient.newHttpClient(); + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss.SSSS"); + private static final Logger log = getLogger(TeamServer.class); + private final ServerService serverService; + private final Team team; + private final Configuration configuration; + private final int port; + private final int apiPort; + private static final List AIKAR = List.of( + "-XX:+ParallelRefProcEnabled", + "-XX:MaxGCPauseMillis=200", + "-XX:+UnlockExperimentalVMOptions", + "-XX:+DisableExplicitGC", + "-XX:+AlwaysPreTouch", + "-XX:G1NewSizePercent=30", + "-XX:G1MaxNewSizePercent=40", "-XX:G1HeapRegionSize=8M", + "-XX:G1ReservePercent=20", + "-XX:G1HeapWastePercent=5", + "-XX:G1MixedGCCountTarget=4", + "-XX:InitiatingHeapOccupancyPercent=15", + "-XX:G1MixedGCLiveThresholdPercent=90", + "-XX:G1RSetUpdatingPauseTimePercent=5", + "-XX:SurvivorRatio=32", + "-XX:+PerfDisableSharedMem", + "-XX:MaxTenuringThreshold=1", + "-Dusing.aikars.flags=https://mcflags.emc.gs", + "-Daikars.new.flags=true" + ); + private boolean running; + + + public TeamServer(ServerService serverService, Team team, Configuration configuration, int port, int apiPort) { + this.serverService = serverService; + this.team = team; + this.configuration = configuration; + this.port = port; + this.apiPort = apiPort; + } + + public boolean running() { + return running; + } + + public boolean exists() { + return serverDir().toFile().exists(); + } + + /** + * Sets up the server if it doesn't exist yet. + * + * @return true if setup was successful. + * @throws IOException if data could not be written + */ + public boolean setup() throws IOException { + if (exists()) return false; + log.info("Setting up server of team {}", team); + writeTemplate(); + return true; + } + + /** + * Refresh the files of the server present in the template. This is basically a new setup without purging the data beforehand. + *

+ * Files with the same name will be overridden + * + * @return true when the refresh was successful + */ + public boolean refresh() { + log.info("Refreshing template files of server {}", team); + try { + writeTemplate(); + } catch (IOException e) { + log.error("Could not refresh template", e); + return false; + } + return true; + } + + private void writeTemplate() throws IOException { + var serverDir = serverDir(); + Files.createDirectories(serverDir); + + var sourceDir = Path.of(configuration.serverTemplate().templateDir()); + var symlinks = configuration.serverTemplate().symLinks() + .stream() + .map(sourceDir::resolve) + .collect(Collectors.toSet()); + try (var files = Files.walk(sourceDir)) { + for (var sourceTarget : files.toList()) { + // skip root dir + if (sourceTarget.getNameCount() == 1) continue; + var filePath = sourceTarget.subpath(1, sourceTarget.getNameCount()); + var serverTarget = serverDir.resolve(filePath); + if (symlinks.contains(sourceTarget)) { + // Not really required since the current and new symlink are probably equal, but the creation will fail otherwise. + if (serverTarget.toFile().isFile() && serverTarget.toFile().delete()) { + log.debug("Deleted old version of file {}", serverTarget); + } + Files.createSymbolicLink(serverTarget, sourceTarget.toAbsolutePath()); + } else { + // ignore already existing directories + if (sourceTarget.toFile().isDirectory() && serverTarget.toFile().exists()) { + continue; + } + Files.copy(sourceTarget, serverTarget, StandardCopyOption.REPLACE_EXISTING); + } + } + } + } + + /** + * Delete all the server data. + * @return true when server was deleted. + * @throws IOException + */ + public boolean purge() throws IOException { + if (!exists()) return false; + log.info("Purging server of team {}", team); + return deleteDirectory(serverDir()); + } + + public boolean start() { + if (!exists() || running()) return false; + var server = configuration.serverManagement(); + var command = new ArrayList(); + command.add("screen"); + command.add("-dmS"); + command.add(screenName()); + command.add("java"); + command.add("-Xmx%dM".formatted(server.memory())); + command.add("-Xms%dM".formatted(server.memory())); + command.addAll(AIKAR); + command.addAll(server.parameter()); + command.add("-Dpluginjam.port=" + server.velocityApi()); + command.add("-Dpluginjam.team.id=" + team.id()); + command.add("-Dpluginjam.team.name=" + teamName()); + command.add("-Djavalin.port=" + apiPort); + command.add("-Dcom.mojang.eula.agree=true"); + command.add("-jar"); + command.add("server.jar"); + command.add("--max-players"); + command.add(String.valueOf(server.maxPlayers())); + command.add("--nogui"); + command.add("--port"); + command.add(String.valueOf(port)); + log.info("Starting server server of team {}", team); + try { + new ProcessBuilder() + .directory(serverDir().toFile()) + .command(command) + .redirectOutput(ProcessBuilder.Redirect.to(processLogFile("start"))) + .start(); + running = true; + } catch (IOException e) { + throw new RuntimeException(e); + } + return true; + } + + public CompletableFuture stop() { + return stop(false); + } + + public CompletableFuture restart() { + return stop(true); + } + + public CompletableFuture stop(boolean restart) { + if (!running) { + return CompletableFuture.completedFuture(null); + } + running = false; + try { + CompletableFuture future = new ProcessBuilder() + .directory(new File("").toPath().toAbsolutePath().toFile()) + .command("./wait.sh", screenName()) + .redirectOutput(ProcessBuilder.Redirect.to(processLogFile("stop"))) + .start() + .onExit() + .whenComplete(Futures.whenComplete( + exit -> { + log.info("Stopped server of team {}", team); + serverService.stopped(this, restart); + }, + err -> log.error("Could not stop server {}", team)) + ) + .thenApply(r -> null); + send("stop"); + log.info("Stopping server of team {}", team); + return future; + } catch (IOException e) { + log.error("Failed to build process builder", e); + throw new RuntimeException(e); + } + } + + public void send(String command) { + log.info("Sending command \"{}\" to server of team {}.", command, team); + try { + new ProcessBuilder() + .directory(serverDir().toFile()) + .redirectOutput(ProcessBuilder.Redirect.to(processLogFile("send"))) + .command(List.of( + "screen", + "-S", + screenName(), + "-p", + "0", + "-X", + "stuff", + "%s^M".formatted(command) + )) + .start(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private File processLogFile(String type) { + var processlog = serverDir().resolve("processlog"); + try { + Files.createDirectories(processlog); + } catch (IOException e) { + throw new RuntimeException(e); + } + return processlog + .resolve("%s_%s.log".formatted(type, FORMATTER.format(LocalDateTime.now()))) + .toFile(); + } + + private Path serverDir() { + return Path.of(configuration.serverManagement().serverDir(), String.valueOf(team().id())); + } + + private String teamName() { + return team.meta().name().toLowerCase().replace(" ", "_"); + } + + private String screenName() { + return "team_%d_%d".formatted(team.id(), port); + } + + public Team team() { + return team; + } + + public int port() { + return port; + } + + public int apiPort() { + return apiPort; + } + + @Override + public String toString() { + return "TeamServer{" + + "team=" + team + + ", port=" + port + + ", apiPort=" + apiPort + + '}'; + } + + public Path logFile() { + return serverDir().resolve("logs").resolve("latest.log"); + } + + public boolean replaceWorld(Path newWorld) { + log.info("Replacing world"); + var worldDir = serverDir().resolve("world"); + var tempWorld = serverDir().resolve("t_world"); + + try (var zip = new ZipFile(newWorld.toFile())) { + log.info("Extracting zip file"); + zip.extractAll(tempWorld.toAbsolutePath().toString()); + } catch (IOException e) { + log.info("Failed to extract zip file", e); + return false; + } + + var copyWorld = tempWorld; + var dirFiles = List.of(tempWorld.toFile().listFiles()); + + var dirOffstet = 3; + + if (dirFiles.size() == 1) { + log.info("No world data found"); + copyWorld = tempWorld.resolve(dirFiles.get(0).getName()); + dirOffstet++; + } + + if (!copyWorld.resolve("region").toFile().exists()) { + log.warn("No region directory."); + return false; + } + + log.info("Deleting old world"); + if (!deleteDirectory(worldDir)) { + return false; + } + + if (copyWorld.resolve("session.lock").toFile().exists()) { + log.info("Found session lock. Deleting."); + copyWorld.resolve("session.lock").toFile().delete(); + } + + log.info("Copy new world data."); + try (var files = Files.walk(copyWorld)) { + Files.createDirectories(worldDir); + for (var sourceTarget : files.toList()) { + // skip root dir + if (sourceTarget.getNameCount() == dirOffstet) continue; + var filePath = sourceTarget.subpath(dirOffstet, sourceTarget.getNameCount()); + var serverTarget = worldDir.resolve(filePath); + Files.copy(sourceTarget, serverTarget, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + log.error("Could not copy world", e); + return false; + } + + log.info("Cleaning up temp world"); + return deleteDirectory(tempWorld); + } + + public boolean deleteDirectory(Path path) { + try (var files = Files.walk(path)) { + files.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (NoSuchFileException e) { + return true; + } catch (IOException e) { + log.info("Could not delete directory", e); + return false; + } + return true; + } + + public Path plugins() { + var plugins = serverDir().resolve("plugins"); + try { + Files.createDirectories(plugins); + } catch (IOException e) { + //ignore + } + return plugins; + } + + public Path world() { + var plugins = serverDir().resolve("world"); + try { + Files.createDirectories(plugins); + } catch (IOException e) { + //ignore + } + return plugins; + } + + public CompletableFuture detailStatus(EventContext context) { + return CompletableFuture.supplyAsync(() -> { + + var builder = new LocalizedEmbedBuilder(context.guildLocalizer()) + .setTitle("%s #%d | %s".formatted(statusEmoji(), team.id(), team.meta().name())); + if (!exists()) { + builder.setDescription("teamserver.message.detailstatus.nonexisting.description"); + } else { + if (running()) { + builder.setDescription("teamserver.message.detailstatus.existing.description") + .addField("word.ports", "$word.server$: %d%n$word.api$: %d".formatted(port, apiPort), true); + stats().ifPresent(stats -> { + var memory = stats.memory(); + builder.addField("word.memory", "$word.used$ %d%n$word.total$: %d%n$word.max$: %d".formatted(memory.usedMb(), memory.totalMb(), memory.maxMb()), true) + .addField("word.tps", "1 $word.min$: %.2f%n5 $word.min$: %.2f%n 15 $word.min$: %.2f%n$word.averageticktime$ %.2f".formatted( + stats.tps()[0], stats.tps()[1], stats.tps()[2], stats.averageTickTime()), true) + .addField("word.players", String.valueOf(stats.onlinePlayers()), true) + .addField("word.system", "$word.activethreads$: %d".formatted(stats.activeThreads()), true); + }); + } else { + builder.setDescription("word.serversetup") + .addField("word.ports", "word.notrunning", true); + } + } + + return builder.build(); + }); + } + + public String status() { + var status = statusEmoji(); + var ports = ""; + if (exists() && running()) { + ports = "Server: %s Api: %s".formatted(port, apiPort); + } + return "%s %s %s".formatted(status, team, ports); + } + + public Optional stats() { + var request = requestBuilder("v1/stats") + .GET() + .build(); + HttpResponse send = null; + try { + send = http().send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + log.error("Could not read stats", e); + return Optional.empty(); + } catch (InterruptedException e) { + log.error("Interrupted", e); + return Optional.empty(); + } + try { + return Optional.of(Mapper.MAPPER.readValue(send.body(), StatsPayload.class)); + } catch (JsonProcessingException e) { + log.error("Could not parse status", e); + return Optional.empty(); + } + } + + public HttpRequest.Builder requestBuilder(String path) { + return HttpRequest.newBuilder(URI.create("http://localhost:%d/%s".formatted(apiPort(), path))); + } + + public HttpRequest.Builder requestBuilder(String path, String query) { + return HttpRequest.newBuilder(URI.create("http://localhost:%d/%s?%s".formatted(apiPort(), path, query))); + } + + public HttpClient http() { + return http; + } + + public void running(boolean running) { + this.running = running; + } + + private String statusEmoji() { + if (exists() && running()) return "🟢"; + if (exists()) return "🟡"; + return "🔴"; + } + + public Optional serverRequests() { + var request = requestBuilder("v1/requests").GET().build(); + HttpResponse send; + try { + send = http().send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + log.error("Could not connect to server"); + throw new RuntimeException(e); + } catch (InterruptedException e) { + log.error("Could not connect to server", e); + throw new RuntimeException(e); + } + try { + return Optional.of(Mapper.MAPPER.readValue(send.body(), RequestsPayload.class)); + } catch (JsonProcessingException e) { + log.error("Could not parse response", e); + throw new RuntimeException(e); + } + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/util/Future.java b/bot/src/main/java/de/chojo/gamejam/util/Future.java new file mode 100644 index 0000000..3ec04a8 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/util/Future.java @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.util; + +import net.dv8tion.jda.api.events.message.MessageDeleteEvent; +import org.slf4j.Logger; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static org.slf4j.LoggerFactory.getLogger; + +public final class Future { + private static final Logger log = getLogger(Future.class); + + private Future() { + throw new UnsupportedOperationException("This is a utility class."); + } + + public static Object log(Runnable run) { + run.run(); + return null; + } + + public static void error(Throwable err) { + log.error("Unhandled Exception occured", err); + } + + public static Consumer error() { + return err -> { + log.error("Unhandled Exception occured", err); + }; + } + + public static BiConsumer handleComplete() { + return (value, err) -> { + if (err != null) { + log.error("Unhandled Exception occured", err); + } + }; + } + + public static java.util.function.BiConsumer handle(Consumer result, Consumer err) { + return (BiConsumer) (t, throwable) -> { + if (throwable != null) { + err.accept(throwable); + return; + } + result.accept(t); + }; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/util/LogNotify.java b/bot/src/main/java/de/chojo/gamejam/util/LogNotify.java new file mode 100644 index 0000000..75913ad --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/util/LogNotify.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.util; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +public final class LogNotify { + private LogNotify() { + throw new UnsupportedOperationException("This is a utility class."); + } + + /** + * Will be send to error-log channel. + */ + public static final Marker NOTIFY_ADMIN = createMarker("NOTIFY_ADMIN"); + /** + * Will be send to status-log. + */ + public static final Marker STATUS = createMarker("STATUS"); + /** + * Currently unused. + */ + public static final Marker DISCORD = createMarker("DISCORD"); + + private static Marker createMarker(@NotNull String name, @NotNull Marker... children) { + var marker = MarkerFactory.getMarker(name); + for (var child : children) { + marker.add(child); + } + return marker; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/util/Mapper.java b/bot/src/main/java/de/chojo/gamejam/util/Mapper.java new file mode 100644 index 0000000..e92cb80 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/util/Mapper.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.util; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + +public class Mapper { + public static final ObjectMapper MAPPER = JsonMapper.builder() + .configure(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS, true) + .build() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) + .setDefaultPrettyPrinter(new DefaultPrettyPrinter()); + +} diff --git a/bot/src/main/java/de/chojo/gamejam/util/TempFile.java b/bot/src/main/java/de/chojo/gamejam/util/TempFile.java new file mode 100644 index 0000000..cdd7f6a --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/util/TempFile.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class TempFile { + public static Path createFile(String prefix, String suffix) throws IOException { + var tempFile = Files.createTempFile(prefix, suffix); + tempFile.toFile().deleteOnExit(); + return tempFile; + } + + public static Path createPath(String prefix, String suffix) throws IOException { + var tempFile = Files.createTempFile(prefix, suffix); + tempFile.toFile().delete(); + tempFile.toFile().deleteOnExit(); + return tempFile; + } +} diff --git a/bot/src/main/resources/database/postgresql/1/patch_1.sql b/bot/src/main/resources/database/postgresql/1/patch_1.sql new file mode 100644 index 0000000..9793c4a --- /dev/null +++ b/bot/src/main/resources/database/postgresql/1/patch_1.sql @@ -0,0 +1,10 @@ +CREATE UNIQUE INDEX IF NOT EXISTS vote_team_id_voter_id_uindex + ON gamejam.vote (team_id, voter_id); + +CREATE OR REPLACE VIEW gamejam.team_ranking AS +SELECT ROW_NUMBER() OVER (PARTITION BY jam_id ORDER BY points DESC ) as rank, team_id, points, jam_id +FROM (SELECT team_id, SUM(points) AS points + FROM gamejam.vote + GROUP BY team_id) a + LEFT JOIN gamejam.team t ON t.id = a.team_id +ORDER BY points DESC diff --git a/bot/src/main/resources/database/postgresql/1/patch_2.sql b/bot/src/main/resources/database/postgresql/1/patch_2.sql new file mode 100644 index 0000000..cab39d2 --- /dev/null +++ b/bot/src/main/resources/database/postgresql/1/patch_2.sql @@ -0,0 +1,20 @@ +ALTER TABLE gamejam.team_meta + ALTER COLUMN leader_id SET DEFAULT 0; + +ALTER TABLE gamejam.team_meta + ALTER COLUMN role_id SET DEFAULT 0; + +ALTER TABLE gamejam.team_meta + ALTER COLUMN text_channel_id SET DEFAULT 0; + +ALTER TABLE gamejam.team_meta + ALTER COLUMN voice_channel_id SET DEFAULT 0; + +ALTER TABLE gamejam.team_meta + RENAME COLUMN name TO team_name; + +ALTER TABLE gamejam.team_meta + ADD project_description TEXT DEFAULT '' NOT NULL; + +ALTER TABLE gamejam.team_meta + ADD project_url TEXT DEFAULT '' NOT NULL; diff --git a/bot/src/main/resources/database/postgresql/1/setup.sql b/bot/src/main/resources/database/postgresql/1/setup.sql new file mode 100644 index 0000000..2fb393a --- /dev/null +++ b/bot/src/main/resources/database/postgresql/1/setup.sql @@ -0,0 +1,141 @@ +CREATE SEQUENCE gamejam.jam_times_id_seq + AS INTEGER; + +CREATE TABLE IF NOT EXISTS gamejam.jam +( + id SERIAL, + guild_id BIGINT NOT NULL, + CONSTRAINT jam_pk + PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS jam_guild_id_index + ON gamejam.jam (guild_id); + +CREATE TABLE IF NOT EXISTS gamejam.jam_time +( + jam_id INTEGER NOT NULL, + registration_start TIMESTAMP NOT NULL, + registration_end TIMESTAMP NOT NULL, + zone_id TEXT NOT NULL, + jam_start TIMESTAMP NOT NULL, + jam_end TIMESTAMP NOT NULL, + CONSTRAINT jam_times_pk + PRIMARY KEY (jam_id), + CONSTRAINT jam_times_jam_id_fk + FOREIGN KEY (jam_id) REFERENCES gamejam.jam + ON DELETE CASCADE +); + +ALTER SEQUENCE gamejam.jam_times_id_seq OWNED BY gamejam.jam_time.jam_id; + +CREATE TABLE IF NOT EXISTS gamejam.jam_meta +( + jam_id INTEGER NOT NULL, + topic TEXT NOT NULL, + CONSTRAINT jam_topic_pk + PRIMARY KEY (jam_id), + CONSTRAINT jam_topic_jam_id_fk + FOREIGN KEY (jam_id) REFERENCES gamejam.jam + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS gamejam.team +( + id SERIAL, + jam_id INTEGER NOT NULL, + CONSTRAINT team_pk + PRIMARY KEY (id), + CONSTRAINT team_jam_id_fk + FOREIGN KEY (jam_id) REFERENCES gamejam.jam + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS gamejam.team_member +( + team_id INTEGER NOT NULL, + user_id BIGINT, + CONSTRAINT team_member_team_id_fk + FOREIGN KEY (team_id) REFERENCES gamejam.team + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS team_member_team_id_user_id_uindex + ON gamejam.team_member (team_id, user_id); + +CREATE INDEX IF NOT EXISTS team_member_team_id_index + ON gamejam.team_member (team_id); + +CREATE TABLE IF NOT EXISTS gamejam.team_meta +( + team_id INTEGER NOT NULL, + name TEXT NOT NULL, + leader_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + text_channel_id BIGINT NOT NULL, + voice_channel_id BIGINT NOT NULL, + CONSTRAINT team_meta_pk + PRIMARY KEY (team_id), + CONSTRAINT team_meta_team_id_fk + FOREIGN KEY (team_id) REFERENCES gamejam.team + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS gamejam.vote +( + team_id BIGINT NOT NULL, + voter_id BIGINT NOT NULL, + points INTEGER DEFAULT 0 NOT NULL, + CONSTRAINT vote_team_id_fk + FOREIGN KEY (team_id) REFERENCES gamejam.team + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS gamejam.jam_registrations +( + jam_id INTEGER NOT NULL, + user_id BIGINT NOT NULL, + CONSTRAINT jam_registrations_jam_id_fk + FOREIGN KEY (jam_id) REFERENCES gamejam.jam + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS jam_registrations_jam_id_user_id_uindex + ON gamejam.jam_registrations (jam_id, user_id); + +CREATE TABLE IF NOT EXISTS gamejam.jam_settings +( + guild_id BIGINT NOT NULL, + jam_role BIGINT DEFAULT 0 NOT NULL, + team_size INTEGER DEFAULT 4 NOT NULL, + CONSTRAINT jam_settings_pk + PRIMARY KEY (guild_id) +); + +CREATE TABLE IF NOT EXISTS gamejam.jam_state +( + jam_id INTEGER NOT NULL, + active BOOLEAN DEFAULT FALSE NOT NULL, + voting BOOLEAN DEFAULT FALSE NOT NULL, + ended BOOLEAN DEFAULT FALSE NOT NULL, + CONSTRAINT jam_state_pk + PRIMARY KEY (jam_id), + CONSTRAINT jam_state_jam_id_fk + FOREIGN KEY (jam_id) REFERENCES gamejam.jam + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS gamejam.version +( + major INTEGER, + patch INTEGER +); + +CREATE TABLE IF NOT EXISTS gamejam.settings +( + guild_id BIGINT NOT NULL, + manager_role BIGINT, + locale TEXT, + CONSTRAINT settings_pk + PRIMARY KEY (guild_id) +); diff --git a/bot/src/main/resources/database/version b/bot/src/main/resources/database/version new file mode 100644 index 0000000..5625e59 --- /dev/null +++ b/bot/src/main/resources/database/version @@ -0,0 +1 @@ +1.2 diff --git a/bot/src/main/resources/locale.properties b/bot/src/main/resources/locale.properties new file mode 100644 index 0000000..d3305fa --- /dev/null +++ b/bot/src/main/resources/locale.properties @@ -0,0 +1,259 @@ +command.jamadmin.create.description= +command.jamadmin.create.options.jamend.description= +command.jamadmin.create.options.jamstart.description= +command.jamadmin.create.message.created= +command.jamadmin.create.options.registerend.description= +command.jamadmin.create.options.registerstart.description= +command.jamadmin.create.options.tagline.description= +command.jamadmin.create.options.timezone.description= +command.jamadmin.create.options.topic.description= +command.jamadmin.description= +command.jamadmin.jam.end.options.confirm.description= +command.jamadmin.jam.description= +command.jamadmin.jam.end.description= +command.jamadmin.jam.end.message.ended= +command.jamadmin.jam.start.description= +command.jamadmin.votes.close.description= +command.jamadmin.votes.description= +command.jamadmin.votes.message.closed= +command.jamadmin.votes.message.opened= +command.jamadmin.votes.open.description= +command.register.description= +command.register.message.alreadyregistered= +command.register.message.notanymore= +command.register.message.notyet= +command.register.message.registered= +command.server.configure.description= +command.server.configure.maxplayers.options.amount.description= +command.server.configure.maxplayers.description= +command.server.configure.maxplayers.message.success= +command.server.configure.message.description= +command.server.configure.message.message.modal.input.message.label= +command.server.configure.message.message.modal.input.message.placeholder= +command.server.configure.message.message.modal.title= +command.server.configure.message.message.success= +command.server.configure.spectatoroverflow.description= +command.server.configure.spectatoroverflow.message.success= +command.server.configure.spectatoroverflow.options.state.description= +command.server.configure.whitelist.description= +command.server.configure.whitelist.message.success= +command.server.configure.whitelist.options.state.description= +command.server.description= +command.server.download.description= +command.server.download.downloadplugindata.message.fail.filetolarge= +command.server.download.downloadplugindata.message.fail.tempfile= +command.server.download.downloadplugindata.message.fail.zip= +command.server.download.downloadplugindata.message.success= +command.server.download.downloadplugindata.message.zipping= +command.server.download.plugindata.description= +command.server.download.plugindata.options.path.description= +command.server.plugins.description= +command.server.plugins.install.description= +command.server.plugins.install.message.fail= +command.server.plugins.install.message.success= +command.server.plugins.install.options.plugin.description= +command.server.plugins.uninstall.options.deletedata.description= +command.server.plugins.uninstall.description= +command.server.plugins.uninstall.message.success.plugin= +command.server.plugins.uninstall.message.success.pluginanddata= +command.server.plugins.uninstall.options.plugin.description= +command.server.process.console.options.command.description= +command.server.process.console.description= +command.server.process.console.message.executed= +command.server.process.console.message.notexecutable= +command.server.process.description= +command.server.process.log.description= +command.server.process.restart.description= +command.server.process.restart.message.restarted= +command.server.process.restart.message.restarting= +command.server.process.start.description= +command.server.process.start.message.fail= +command.server.process.start.message.success= +command.server.process.status.description= +command.server.process.stop.description= +command.server.process.stop.message.stopped= +command.server.process.stop.message.stopping= +command.server.system.delete.description= +command.server.system.delete.message.error= +command.server.system.delete.message.notsetup= +command.server.system.delete.message.success= +command.server.system.description= +command.server.system.setup.description= +command.server.system.setup.message.alreadysetup= +command.server.system.setup.message.error= +command.server.system.setup.message.success= +command.server.upload.description= +command.server.upload.plugin.description= +command.server.upload.plugin.options.file.description= +command.server.upload.plugin.message.fail= +command.server.upload.plugin.message.success= +command.server.upload.plugindata.description= +command.server.upload.plugindata.options.file.description= +command.server.upload.plugindata.options.path.description= +command.server.upload.uploadplugindata.message.fail= +command.server.upload.uploadplugindata.message.success= +command.server.upload.world.description= +command.server.upload.world.options.file.description= +command.server.upload.world.message.failed= +command.server.upload.world.message.nofileorurl= +command.server.upload.world.message.replaced= +command.server.upload.world.message.replacing= +command.server.upload.world.options.url.description= +command.server.util.progressdownloader.message.attempting= +command.server.util.progressdownloader.message.done= +command.server.util.progressdownloader.message.downloading= +command.server.util.progressdownloader.message.fail.download= +command.server.util.progressdownloader.message.fail.tempfile= +command.serveradmin.description= +command.serveradmin.info.description= +command.serveradmin.info.detailed.description= +command.serveradmin.info.detailed.options.team.description= +command.serveradmin.info.short.description= +command.serveradmin.refresh.all.description= +command.serveradmin.refresh.description= +command.serveradmin.refresh.refreshall.message.refreshed= +command.serveradmin.refresh.refreshteam.message.failed= +command.serveradmin.refresh.refreshteam.message.refreshed= +command.serveradmin.refresh.team.description= +command.serveradmin.refresh.team.options.team.description= +command.serveradmin.restart.all.description= +command.serveradmin.restart.description= +command.serveradmin.restart.restartall.message.restarted= +command.serveradmin.restart.restartteam.message.failed= +command.serveradmin.restart.restartteam.message.restarted= +command.serveradmin.restart.team.description= +command.serveradmin.restart.team.options.team.description= +command.serveradmin.start.all.description= +command.serveradmin.start.description= +command.serveradmin.start.startall.message.started= +command.serveradmin.start.startteam.message.failed= +command.serveradmin.start.startteam.message.started= +command.serveradmin.start.team.description= +command.serveradmin.start.team.options.team.description= +command.serveradmin.stop.all.description= +command.serveradmin.stop.description= +command.serveradmin.stop.stopall.message.stopped= +command.serveradmin.stop.stopteam.message.failed= +command.serveradmin.stop.stopteam.message.stopped= +command.serveradmin.stop.team.description= +command.serveradmin.stop.team.options.team.description= +command.serveradmin.syncvelocity.description= +command.serveradmin.syncvelocity.message.synced= +command.settings.description= +command.settings.info.description= +command.settings.info.embed.jamrole= +command.settings.info.embed.orgarole= +command.settings.info.embed.settings= +command.settings.info.embed.teamsize= +command.settings.jamrole.description= +command.settings.jamrole.message.updated= +command.settings.jamrole.options.role.description= +command.settings.locale.description= +command.settings.locale.options.locale.description= +command.settings.locale.message.invalid= +command.settings.locale.message.updated= +command.settings.orgarole.description= +command.settings.orgarole.options.role.description= +command.settings.teamsize.description= +command.settings.teamsize.message.updated= +command.settings.teamsize.options.size.description= +command.start.message.activated= +command.team.create.description= +command.team.create.message.alreadymember= +command.team.create.message.created= +command.team.create.message.nametaken= +command.team.create.message.unregistered= +command.team.create.options.name.description= +command.team.description= +command.team.disband.options.confirm.description= +command.team.disband.description= +command.team.disband.message.disbanded= +command.team.invite.alreadyMember= +command.team.invite.description= +command.team.invite.gameJamOver= +command.team.invite.joined= +command.team.invite.joinedBroadcast= +command.team.invite.message.accept= +command.team.invite.message.invitation= +command.team.invite.message.invited= +command.team.invite.message.noleader= +command.team.invite.message.notRegistered= +command.team.invite.message.partofteam= +command.team.invite.message.send= +command.team.invite.options.user.description= +command.team.leave.description= +command.team.leave.left= +command.team.leave.leftBroadcast= +command.team.leave.message.leaderleave= +command.team.list.description= +command.team.profile.description= +command.team.profile.leader= +command.team.profile.member= +command.team.profile.message.nouserteam= +command.team.profile.projecturl= +command.team.profile.options.team.description= +command.team.profile.options.user.description= +command.team.promote.description= +command.team.promote.message.done= +command.team.promote.message.notinteam= +command.team.promote.options.user.description= +command.team.rename.description= +command.team.rename.message.done= +command.team.rename.options.name.description= +command.unregister.description= +command.unregister.message.inteam= +command.unregister.message.notregistered= +command.unregister.message.unregistered= +command.votes.description= +command.votes.info.description= +command.votes.info.embed.title= +command.votes.ranking.description= +command.votes.ranking.embed.votes= +command.votes.ranking.message.voteactive= +command.votes.vote.description= +command.votes.vote.message.done= +command.votes.vote.message.maxpointsreached= +command.votes.vote.message.notactive= +command.votes.vote.message.ownteam= +command.votes.vote.options.points.description= +command.votes.vote.options.team.description= + +error.alreadyActive= +error.invalidpath= +error.invalidrimeformat= +error.invalidtimezone= +error.maxteamsize= +error.noactivejam= +error.noconfirm= +error.nojamactive= +error.noleader= +error.noteam= +error.notregistered= +error.noupcomingjam= +error.pluginnotfound= +error.servernotrunning= +error.unkownteam= +error.votingactive= + +teamserver.message.detailstatus.existing.description= +teamserver.message.detailstatus.nonexisting.description= + +word.activethreads= +word.api= +word.averageticktime= +word.max= +word.memory= +word.min= +word.notrunning= +word.players= +word.ports= +word.server= +word.serversetup= +word.system= +word.total= +word.tps= +word.used= + +words.dateformat= +words.format= +command.team.edit.description= diff --git a/bot/src/main/resources/locale_de.properties b/bot/src/main/resources/locale_de.properties new file mode 100644 index 0000000..a17088d --- /dev/null +++ b/bot/src/main/resources/locale_de.properties @@ -0,0 +1,259 @@ +command.jamadmin.create.description=Neuen Game-Jam erstellen +command.jamadmin.create.options.jamend.description=Game-Jam-Ende. $words.format$: $words.dateformat$ +command.jamadmin.create.options.jamstart.description=Game-Jam-Start. $words.format$: $words.dateformat$ +command.jamadmin.create.message.created=Game-Jam erstellt. +command.jamadmin.create.options.registerend.description=Registrierungen schließen. $words.format$: $words.dateformat$ +command.jamadmin.create.options.registerstart.description=Registrierungen öffnen. $words.format$: $words.dateformat$ +command.jamadmin.create.options.tagline.description=Topic-Tagline als Zusatz zum Thema +command.jamadmin.create.options.timezone.description=Die Zeitzone des Game Jams. Zum Beispiel "Europe/Berlin". +command.jamadmin.create.options.topic.description=Das Thema des Game-Jam +command.jamadmin.description=Verwalte Game-Jams +command.jamadmin.jam.end.options.confirm.description=Bestätige das Ende. +command.jamadmin.jam.description=Verwalte Game-Jams +command.jamadmin.jam.end.description=Beendet den aktuellen Game-Jam. Löscht alle Rollen und Kanäle +command.jamadmin.jam.end.message.ended=Game-Jam beendet. +command.jamadmin.jam.start.description=Startet den nächsten geplanten Game-Jam +command.jamadmin.votes.close.description=Abstimmungen für den aktuellen Game-Jam schließen. +command.jamadmin.votes.description=Verwalte Abstimmungen +command.jamadmin.votes.message.closed=Abstimmungen für den aktuellen Game-Jam geschlossen. +command.jamadmin.votes.message.opened=Abtimmung für den aktuellen Game-Jam geöffnet. +command.jamadmin.votes.open.description=Abtimmung für den aktuellen Game-Jam öffnen. +command.register.description=Du hast dich für einen kommenden Game-Jam angemeldet. +command.register.message.alreadyregistered=Du bist bereits registriert. +command.register.message.notanymore=Registrierungen sind geschlossen. +command.register.message.notyet=Du kannst dich noch nicht für diesen Game-Jam anmelden. Du kannst dich am %TIMESTAMP% anmelden. +command.register.message.registered=Du hast dich für den nächsten Game-Jam angemeldet. Er beginnt am %TIMESTAMP%. +command.server.configure.description=Server konfigurieren +command.server.configure.maxplayers.options.amount.description=Maximale Anzahl von Spielern. +command.server.configure.maxplayers.description=Die maximale Spieleranzahl für diesen Server festlegen +command.server.configure.maxplayers.message.success=Maximale Spielerzahl festlegen. +command.server.configure.message.description=Lege die Willkommensnachricht fest +command.server.configure.message.message.modal.input.message.label=Willkommensnachricht +command.server.configure.message.message.modal.input.message.placeholder=Willkommensnachricht +command.server.configure.message.message.modal.title=Definiere die Willkommensnachricht +command.server.configure.message.message.success=Nachricht einstellen. +command.server.configure.spectatoroverflow.description=Aktiviere den Zuschauerüberlauf +command.server.configure.spectatoroverflow.message.success=Überlauf einstellen. +command.server.configure.spectatoroverflow.options.state.description=True, um den Überlauf zu aktivieren +command.server.configure.whitelist.description=Aktiviere die Whitelist +command.server.configure.whitelist.message.success=Whitelist gesetzt. +command.server.configure.whitelist.options.state.description=True, um die Whitelist zu aktivieren +command.server.description=Verwalte deinen Server +command.server.download.description=Dateien herunterladen +command.server.download.downloadplugindata.message.fail.filetolarge=Die Datei ist zu groß. +command.server.download.downloadplugindata.message.fail.tempfile=Temporäre Datei konnte nicht erstellt werden +command.server.download.downloadplugindata.message.fail.zip=Daten konnten nicht gezippt werden. +command.server.download.downloadplugindata.message.success=Upload durchgeführt +command.server.download.downloadplugindata.message.zipping=Daten zippen. +command.server.download.plugindata.description=Plugin-Dateien herunterladen +command.server.download.plugindata.options.path.description=Pfad im Plugin-Verzeichnis +command.server.plugins.description=Installierte Plugins verwalten +command.server.plugins.install.description=Ein anderes Plugin installieren +command.server.plugins.install.message.fail=Plugin konnte nicht installiert werden +command.server.plugins.install.message.success=Installiert %NAME%. Neustart, um Änderungen zu übernehmen +command.server.plugins.install.options.plugin.description=Zu installierendes Plugin +command.server.plugins.uninstall.options.deletedata.description=Richtig, um auch die Plugin-Daten zu löschen +command.server.plugins.uninstall.description=Zu deinstallierendes Plugin +command.server.plugins.uninstall.message.success.plugin=Plugin deinstalliert. +command.server.plugins.uninstall.message.success.pluginanddata=Plugin deinstalliert und Daten gelöscht +command.server.plugins.uninstall.options.plugin.description=Das zu deinstallierende Plugin +command.server.process.console.options.command.description=Der zu sendende Befehl +command.server.process.console.description=Einen Befehl über die Konsole senden +command.server.process.console.message.executed=Ausgeführt +command.server.process.console.message.notexecutable=Diese Befehle können nicht ausgeführt werden +command.server.process.description=Serverprozess verwalten +command.server.process.log.description=Neustart des Servers +command.server.process.restart.description=Neustart des Servers +command.server.process.restart.message.restarted=Der Server wurde neu gestartet. +command.server.process.restart.message.restarting=Server neu gestartet +command.server.process.start.description=Start des Servers +command.server.process.start.message.fail=Der Server konnte nicht gestartet werden. Er läuft bereits oder ist nicht eingerichtet. +command.server.process.start.message.success=Server gestartet +command.server.process.status.description=Status des Servers +command.server.process.stop.description=Server anhalten +command.server.process.stop.message.stopped=Server gestoppt +command.server.process.stop.message.stopping=Server gestoppt +command.server.system.delete.description=Löschen der Serverdaten +command.server.system.delete.message.error=Beim Löschen des Servers ist etwas schief gelaufen +command.server.system.delete.message.notsetup=Server ist nicht eingerichtet. +command.server.system.delete.message.success=Der Server wurde erfolgreich gelöscht. +command.server.system.description=Verwalten des Serversystems +command.server.system.setup.description=Den Server einrichten +command.server.system.setup.message.alreadysetup=Der Server wurde bereits erstellt. +command.server.system.setup.message.error=Bei der Einrichtung des Servers ist etwas schief gelaufen +command.server.system.setup.message.success=Der Server wurde erfolgreich eingerichtet. +command.server.upload.description=Dateien hochladen +command.server.upload.plugin.description=Dein Plugin hochladen +command.server.upload.plugin.options.file.description=Deine Plugin-Datei +command.server.upload.plugin.message.fail=Plugin konnte nicht hinzugefügt werden. +command.server.upload.plugin.message.success=Plugin hinzugefügt oder ersetzt. +command.server.upload.plugindata.description=Plugin-Daten hochladen +command.server.upload.plugindata.options.file.description=Datei zum Hochladen +command.server.upload.plugindata.options.path.description=Pfad im Plugin-Verzeichnis +command.server.upload.uploadplugindata.message.fail=Datei konnte nicht hinzugefügt werden. +command.server.upload.uploadplugindata.message.success=Datei hinzugefügt oder ersetzt. +command.server.upload.world.description=Eine Welt hochladen, die die aktuelle Welt ersetzt +command.server.upload.world.options.file.description=Welt als zip +command.server.upload.world.message.failed=Ersetzen der Welt fehlgeschlagen +command.server.upload.world.message.nofileorurl=Keine Datei oder Url angegeben +command.server.upload.world.message.replaced=Ersetzte Welt +command.server.upload.world.message.replacing=Download abgeschlossen und ersetzt. +command.server.upload.world.options.url.description=Link zum Herunterladen der Welt als zip +command.server.util.progressdownloader.message.attempting=Ich versuche, die Datei herunterzuladen. +command.server.util.progressdownloader.message.done=Download abgeschlossen. +command.server.util.progressdownloader.message.downloading=Datei wird heruntergeladen. +command.server.util.progressdownloader.message.fail.download=Datei konnte nicht heruntergeladen werden. +command.server.util.progressdownloader.message.fail.tempfile=Temporäre Datei kann nicht erstellt werden +command.serveradmin.description=Verwaltung der Teamserver +command.serveradmin.info.description=Server-Informationen +command.serveradmin.info.detailed.description=Detaillierte Informationen zu Teamservern +command.serveradmin.info.detailed.options.team.description=team +command.serveradmin.info.short.description=Kurze Informationen über Teamserver +command.serveradmin.refresh.all.description=Alle Server aktualisieren +command.serveradmin.refresh.description=Aktualisiere die Dateien der Vorlage auf allen Servern. +command.serveradmin.refresh.refreshall.message.refreshed=%AMOUNT% Server aktualisiert. +command.serveradmin.refresh.refreshteam.message.failed=Bei der Aktualisierung von %TEAM% ist ein Fehler aufgetreten. +command.serveradmin.refresh.refreshteam.message.refreshed=Der Server von Team %TEAM% wurde aktualisiert. +command.serveradmin.refresh.team.description=Aktualisiere einen Teamserver +command.serveradmin.refresh.team.options.team.description=team +command.serveradmin.restart.all.description=Alle Server starten +command.serveradmin.restart.description=Server neu starten +command.serveradmin.restart.restartall.message.restarted=Neustart von %AMOUNT% Servern. +command.serveradmin.restart.restartteam.message.failed=Server von Team %TEAM% wurde nicht gestartet. +command.serveradmin.restart.restartteam.message.restarted=Der Server von Team %TEAM% wurde neu gestartet. +command.serveradmin.restart.team.description=Start eines Team-Servers +command.serveradmin.restart.team.options.team.description=team +command.serveradmin.start.all.description=Alle Server starten +command.serveradmin.start.description=Server starten +command.serveradmin.start.startall.message.started=Starte %AMOUNT% Server. +command.serveradmin.start.startteam.message.failed=Der Server von Team %TEAM% konnte nicht gestartet werden. +command.serveradmin.start.startteam.message.started=Server von Team %TEAM% wurde gestartet. +command.serveradmin.start.team.description=Starte einen Teamserver +command.serveradmin.start.team.options.team.description=Team +command.serveradmin.stop.all.description=Alle Server anhalten +command.serveradmin.stop.description=Server anhalten +command.serveradmin.stop.stopall.message.stopped=%AMOUNT% Server gestoppt. +command.serveradmin.stop.stopteam.message.failed=Server von Team %TEAM% läuft nicht. +command.serveradmin.stop.stopteam.message.stopped=Server von Team %TEAM% wurde gestoppt. +command.serveradmin.stop.team.description=Einen Teamserver stoppen +command.serveradmin.stop.team.options.team.description=team +command.serveradmin.syncvelocity.description=Serverstatus mit Velocity-Daten synchronisieren +command.serveradmin.syncvelocity.message.synced=Server synchronisiert +command.settings.description=Verwalte die Bot-Einstellungen +command.settings.info.description=Zeige die aktuellen Einstellungen +command.settings.info.embed.jamrole=Game-Jam Rolle +command.settings.info.embed.orgarole=Organisations-Rolle +command.settings.info.embed.settings=Einstellungen +command.settings.info.embed.teamsize=Max Team Size +command.settings.jamrole.description=Stellt die Rolle ein, die den registrierten Mitgliedern zugewiesen wird. +command.settings.jamrole.message.updated=Aktualisiere die Game-Jam Rolle. +command.settings.jamrole.options.role.description=Die Rolle, die nach der Registrierung zugewiesen wird +command.settings.locale.description=Ändert die Bot-Sprache. +command.settings.locale.options.locale.description=Die neue Sprache +command.settings.locale.message.invalid=Ungültige Sprache +command.settings.locale.message.updated=Sprache aktualisiert +command.settings.orgarole.description=Definiere die Rolle des Organisationsteams +command.settings.orgarole.options.role.description=Die Rolle, die den Bot verwalten kann +command.settings.teamsize.description=Definiere die maximale Teamgröße. +command.settings.teamsize.message.updated=Aktualisierte maximale Teamgröße. +command.settings.teamsize.options.size.description=Die maximale Teamgröße. +command.start.message.activated=Der Teamstatus wurde auf aktiv geändert. +command.team.create.description=Erstelle ein Team +command.team.create.message.alreadymember=Du bist bereits Teil eines Teams. Um dein eigenes Team zu erstellen, musst du es erst verlassen. +command.team.create.message.created=Team erstellt. +command.team.create.message.nametaken=Dieser Teamname ist bereits vergeben. +command.team.create.message.unregistered=Du musst dich erst registrieren, um ein Team zu erstellen +command.team.create.options.name.description=Name deines Teams +command.team.description=Verwalte dein Team +command.team.disband.options.confirm.description=Bestätige die Löschung deines Teams mit "true" +command.team.disband.description=Löse dein Team auf +command.team.disband.message.disbanded=Dein Team wurde aufgelöst. +command.team.invite.alreadyMember=Du bist bereits Mitglied in einem Team. +command.team.invite.description=Lade jemanden in dein Team ein +command.team.invite.gameJamOver=Der Game-Jam ist vorbei +command.team.invite.joined=Du bist dem Team beigetreten. +command.team.invite.joinedBroadcast=%USER% ist dem Team beigetreten. +command.team.invite.message.accept=Akzeptieren +command.team.invite.message.invitation=%USER% hat dich eingeladen, dem Team %TEAM% beizutreten. +command.team.invite.message.invited=Du hast eine Einladung für den Game-Jam auf %GUILD% erhalten +command.team.invite.message.noleader=Nur die/die Gruppenleiter:in kann einladen. +command.team.invite.message.notRegistered=Dieser Benutzer ist nicht für den Game-Jam registriert. +command.team.invite.message.partofteam=Dieser/diese Benutzer:in ist bereits Teil eines Teams. +command.team.invite.message.send=Einladung senden. +command.team.invite.options.user.description=Der/Die Benutzer:in, den/die du einladen möchtest +command.team.leave.description=Verlasse dein Team +command.team.leave.left=Du hast das Team verlassen. +command.team.leave.leftBroadcast=%USER% hat das Team verlassen. +command.team.leave.message.leaderleave=Der Anführer kann das Team nicht verlassen. +command.team.list.description=Erhalte eine Liste alles existierenden Teams. +command.team.profile.description=Zeigt das Profil eines Teams oder dein eigenes an +command.team.profile.leader=Leiter +command.team.profile.member=Mitglied +command.team.profile.message.nouserteam=Dieser Benutzer ist nicht Teil eines Teams. +command.team.profile.projecturl=Projekt Adresse +command.team.profile.options.team.description=Dieses Team anzeigen +command.team.profile.options.user.description=Das Team dieses Benutzers anzeigen +command.team.promote.description=Ändere den/die Teamleiter:in des Teams +command.team.promote.message.done=Der/Die Teamleiter:in wurde geändert +command.team.promote.message.notinteam=Diese/r Nutzer:in ist nicht Teil des Teams. +command.team.promote.options.user.description=Der/Die neue Teamleiter:in. +command.team.rename.description=Ändere den Namen des Teams. +command.team.rename.message.done=Der Name wurde geändert. +command.team.rename.options.name.description=Der neue Teamname +command.unregister.description=Ziehe deine Registrierung zurück. +command.unregister.message.inteam=Du musst erst dein Team verlassen. +command.unregister.message.notregistered=Du bist derzeit für keinen Game-Jam registriert. +command.unregister.message.unregistered=Deine Registrierung wurde zurückgezogen. +command.votes.description=Bewertung vergeben +command.votes.info.description=Informationen über deine Stimmen. +command.votes.info.embed.title=Vergebene Punkte +command.votes.ranking.description=Das aktuelle Ranking +command.votes.ranking.embed.votes=Punkte +command.votes.ranking.message.voteactive=Abstimmung aktiv. Die Rangliste kann erst nach dem Beenden der Abstimmung angezeigt werden. +command.votes.vote.description=Stimme für ein Team ab. +command.votes.vote.message.done=Du hast %TEAM% %POINTS% punkte gegeben. Verbleibende Punkte: %REMAINING% +command.votes.vote.message.maxpointsreached=Du hast das maximum an Punkten vergeben. Entferne Punkte von anderen teams wenn du weiter Punkte vergeben willst. Verbleibende Punkte: %REMAINING% +command.votes.vote.message.notactive=Es ist keine Abstimmung aktiv. +command.votes.vote.message.ownteam=Du kannst nicht für dein eigenes Team abstimmen. +command.votes.vote.options.points.description=Die Punkte zum vergeben +command.votes.vote.options.team.description=Das Tam für das du abstimmen möchtest. + +error.alreadyActive=Ein Game-Jam ist bereits aktiv. +error.invalidpath=Ungültiger Pfad +error.invalidrimeformat=Ungültiges Zeitformat. Das Format ist %FORMAT% +error.invalidtimezone=Ungültige Zeitzone. +error.maxteamsize=Das Team hat die maximale Größe erreicht. +error.noactivejam=Es gibt keinen aktiven Game-Jam. +error.noconfirm=Bitte bestätige mit dem Parameter confirm +error.nojamactive=Es ist kein Game-Jam im Gange. Teams sind nicht verfügbar. +error.noleader=Nur der/die Teamleiter:in kann dies tun. +error.noteam=Du bist nicht Teil eines Teams. +error.notregistered=Du bist für keinen Game-Jam registriert. +error.noupcomingjam=Es gibt keinen bevorstehenden Game-Jams. +error.pluginnotfound=Plugin nicht gefunden +error.servernotrunning=Server ist nicht online. +error.unkownteam=Dieses Team existiert nicht. +error.votingactive=Das ist nicht möglich während eine Abstimmung aktiv ist. + +teamserver.message.detailstatus.existing.description=Server läuft +teamserver.message.detailstatus.nonexisting.description=Server nicht eingerichtet. + +word.activethreads=Aktive Threads +word.api=Api +word.averageticktime=Durchschnittliche Tick-Zeit +word.max=Max +word.memory=Speicher +word.min=min +word.notrunning=Läuft nicht +word.players=Spieler +word.ports=Ports +word.server=Server +word.serversetup=Server eingerichtet +word.system=System +word.total=Gesamt +word.tps=Tps +word.used=Verwendet + +words.dateformat=yyyy.MM.dd HH:mm +words.format=Format +command.team.edit.description=Bearbeite Team Informationen diff --git a/bot/src/main/resources/locale_en_US.properties b/bot/src/main/resources/locale_en_US.properties new file mode 100644 index 0000000..2836552 --- /dev/null +++ b/bot/src/main/resources/locale_en_US.properties @@ -0,0 +1,259 @@ +command.jamadmin.create.description=Create a new game jam +command.jamadmin.create.options.jamend.description=Game Jam end. $words.format$: $words.dateformat$ +command.jamadmin.create.options.jamstart.description=Game Jam start. $words.format$: $words.dateformat$ +command.jamadmin.create.message.created=Jam created. +command.jamadmin.create.options.registerend.description=Registrations close. $words.format$: $words.dateformat$ +command.jamadmin.create.options.registerstart.description=Registrations opening. $words.format$: $words.dateformat$ +command.jamadmin.create.options.tagline.description=Topic tagline as an addition to the topic +command.jamadmin.create.options.timezone.description=The timezone of the game jam. "Europe/Berlin" for example. +command.jamadmin.create.options.topic.description=The topic of the game jam +command.jamadmin.description=Manage jams +command.jamadmin.jam.end.options.confirm.description=Confirm the end +command.jamadmin.jam.description=Manage jams +command.jamadmin.jam.end.description=Ends the currently active jam. Deletes all roles and channel +command.jamadmin.jam.end.message.ended=Jam ended. +command.jamadmin.jam.start.description=Start the next scheduled jam +command.jamadmin.votes.close.description=Close votes for the current jam +command.jamadmin.votes.description=Manage votes +command.jamadmin.votes.message.closed=Votes closed for the current jam. +command.jamadmin.votes.message.opened=Votes for the current jam opened. +command.jamadmin.votes.open.description=Open votes for the current jam +command.register.description=Register for an upcomming Game Jam +command.register.message.alreadyregistered=You are already registered. +command.register.message.notanymore=Registrations are closed. +command.register.message.notyet=You cant register for this game jam yet. You can register at %TIMESTAMP% +command.register.message.registered=You have registered yourself for the next game jam. It will start at %TIMESTAMP%. +command.server.configure.description=Configure server +command.server.configure.maxplayers.options.amount.description=Max amount of players. +command.server.configure.maxplayers.description=Set the max players of this server +command.server.configure.maxplayers.message.success=Max players set. +command.server.configure.message.description=Set the welcome message +command.server.configure.message.message.modal.input.message.label=Welcome message +command.server.configure.message.message.modal.input.message.placeholder=Welcome message +command.server.configure.message.message.modal.title=Define the welcome message +command.server.configure.message.message.success=Message set. +command.server.configure.spectatoroverflow.description=Active the spectator overflow +command.server.configure.spectatoroverflow.message.success=Overflow set. +command.server.configure.spectatoroverflow.options.state.description=True to enable overflow +command.server.configure.whitelist.description=Enable the whitelist +command.server.configure.whitelist.message.success=Whitelist set. +command.server.configure.whitelist.options.state.description=True to enable whitelist +command.server.description=Manage your server +command.server.download.description=Download files +command.server.download.downloadplugindata.message.fail.filetolarge=File is too large. +command.server.download.downloadplugindata.message.fail.tempfile=Failed to create temp file +command.server.download.downloadplugindata.message.fail.zip=Failed to zip data. +command.server.download.downloadplugindata.message.success=Upload done +command.server.download.downloadplugindata.message.zipping=Zipping data. +command.server.download.plugindata.description=Download plugin files +command.server.download.plugindata.options.path.description=Path in the plugin directory +command.server.plugins.description=Manage installed plugins +command.server.plugins.install.description=Install another plugin +command.server.plugins.install.message.fail=Could not install plugin +command.server.plugins.install.message.success=Installed %NAME%. Restart to apply changes +command.server.plugins.install.options.plugin.description=Plugin to install +command.server.plugins.uninstall.options.deletedata.description=True to delete the plugin data as well +command.server.plugins.uninstall.description=Plugin to uninstall +command.server.plugins.uninstall.message.success.plugin=Uninstalled plugin. +command.server.plugins.uninstall.message.success.pluginanddata=Uninstalled plugin and deleted data +command.server.plugins.uninstall.options.plugin.description=The plugin to uninstall +command.server.process.console.options.command.description=The command to send +command.server.process.console.description=Send a command via console +command.server.process.console.message.executed=Executed +command.server.process.console.message.notexecutable=Those commands can not be executed +command.server.process.description=Manage server process +command.server.process.log.description=Restart the server +command.server.process.restart.description=Restart the server +command.server.process.restart.message.restarted=Server restarted. +command.server.process.restart.message.restarting=Server restarting +command.server.process.start.description=Start the server +command.server.process.start.message.fail=Could not start server. It is already running or not set up. +command.server.process.start.message.success=Server started +command.server.process.status.description=Server status +command.server.process.stop.description=Stop the server +command.server.process.stop.message.stopped=Server stopped +command.server.process.stop.message.stopping=Stopping server +command.server.system.delete.description=Delete the server data +command.server.system.delete.message.error=Something went wrong during server deletion +command.server.system.delete.message.notsetup=Server is not set up. +command.server.system.delete.message.success=Server was deleted successfully. +command.server.system.description=Manage the server system +command.server.system.setup.description=Setup the server +command.server.system.setup.message.alreadysetup=Server was already created. +command.server.system.setup.message.error=Something went wrong during server setup +command.server.system.setup.message.success=Server was setup successfully. +command.server.upload.description=Upload files +command.server.upload.plugin.description=Upload your plugin +command.server.upload.plugin.options.file.description=Your plugin file +command.server.upload.plugin.message.fail=Failed to add plugin. +command.server.upload.plugin.message.success=Added or replaced plugin. +command.server.upload.plugindata.description=Upload plugin data +command.server.upload.plugindata.options.file.description=File to upload +command.server.upload.plugindata.options.path.description=Path in the plugin directory +command.server.upload.uploadplugindata.message.fail=Failed to add file. +command.server.upload.uploadplugindata.message.success=Added or replaced file. +command.server.upload.world.description=Upload a world replacing the current world +command.server.upload.world.options.file.description=World as zip +command.server.upload.world.message.failed=Failed to replace world +command.server.upload.world.message.nofileorurl=No file or url provided +command.server.upload.world.message.replaced=Replaced world +command.server.upload.world.message.replacing=Download done. Replacing. +command.server.upload.world.options.url.description=Link to download the world as zip +command.server.util.progressdownloader.message.attempting=Attempting to download file. +command.server.util.progressdownloader.message.done=Download done. +command.server.util.progressdownloader.message.downloading=Downloading file. +command.server.util.progressdownloader.message.fail.download=Could not download file. +command.server.util.progressdownloader.message.fail.tempfile=Failed to create temp file +command.serveradmin.description=Administration of team servers +command.serveradmin.info.description=Server information +command.serveradmin.info.detailed.description=Detailed information about team servers +command.serveradmin.info.detailed.options.team.description=team +command.serveradmin.info.short.description=Short information about team servers +command.serveradmin.refresh.all.description=Refresh all server +command.serveradmin.refresh.description=Refresh files of the template in all servers. +command.serveradmin.refresh.refreshall.message.refreshed=Refreshed %AMOUNT% servers. +command.serveradmin.refresh.refreshteam.message.failed=Failed during refresh of %TEAM%. +command.serveradmin.refresh.refreshteam.message.refreshed=Server of team %TEAM% refreshed. +command.serveradmin.refresh.team.description=Refresh a team server +command.serveradmin.refresh.team.options.team.description=team +command.serveradmin.restart.all.description=Start all server +command.serveradmin.restart.description=Restart servers +command.serveradmin.restart.restartall.message.restarted=Restarted %AMOUNT% servers. +command.serveradmin.restart.restartteam.message.failed=Server of team %TEAM% was not running. +command.serveradmin.restart.restartteam.message.restarted=Server of team %TEAM% restarted. +command.serveradmin.restart.team.description=Start a team server +command.serveradmin.restart.team.options.team.description=team +command.serveradmin.start.all.description=Start all server +command.serveradmin.start.description=Start servers +command.serveradmin.start.startall.message.started=Started %AMOUNT% servers. +command.serveradmin.start.startteam.message.failed=Could not start server of team %TEAM%. +command.serveradmin.start.startteam.message.started=Server of team %TEAM% started. +command.serveradmin.start.team.description=Start a team server +command.serveradmin.start.team.options.team.description=team +command.serveradmin.stop.all.description=Stop all server +command.serveradmin.stop.description=Stop servers +command.serveradmin.stop.stopall.message.stopped=Stopped %AMOUNT% servers. +command.serveradmin.stop.stopteam.message.failed=Server of team %TEAM% is not running. +command.serveradmin.stop.stopteam.message.stopped=Server of team %TEAM% stopped. +command.serveradmin.stop.team.description=Stop a team server +command.serveradmin.stop.team.options.team.description=team +command.serveradmin.syncvelocity.description=Sync servers with velocity data +command.serveradmin.syncvelocity.message.synced=Synced server +command.settings.description=Manage bot settings +command.settings.info.description=Show the current settings +command.settings.info.embed.jamrole=Game Jam Role +command.settings.info.embed.orgarole=Organization Role +command.settings.info.embed.settings=Settings +command.settings.info.embed.teamsize=Max Team Size +command.settings.jamrole.description=Set the role which will be assigned to registered members. +command.settings.jamrole.message.updated=Updated the game jam role. +command.settings.jamrole.options.role.description=The role to assign after registration +command.settings.locale.description=Change the bot language. +command.settings.locale.options.locale.description=The new language +command.settings.locale.message.invalid=Invalid locale +command.settings.locale.message.updated=Locale updated +command.settings.orgarole.description=Define the organisation team role +command.settings.orgarole.options.role.description=The role which can manage the bot +command.settings.teamsize.description=Define the max team size. +command.settings.teamsize.message.updated=Updated max team size. +command.settings.teamsize.options.size.description=The max team size. +command.start.message.activated=Jam state changed to active. +command.team.create.description=Create a team +command.team.create.message.alreadymember=You are already part of a team. You need to leave first to create your own team. +command.team.create.message.created=Team created. +command.team.create.message.nametaken=This team name is already taken. +command.team.create.message.unregistered=You need to register first to create a team +command.team.create.options.name.description=Name of your team +command.team.description=Manage your team +command.team.disband.options.confirm.description=Confirm deletion of your team with "true" +command.team.disband.description=Disband your team +command.team.disband.message.disbanded=Your team was disbanded. +command.team.invite.alreadyMember=You are already member of a team. +command.team.invite.description=Invite someone to your team +command.team.invite.gameJamOver=The game jam is over +command.team.invite.joined=You joined the team. +command.team.invite.joinedBroadcast=%USER% joined the team. +command.team.invite.message.accept=Accept +command.team.invite.message.invitation=%USER% invited you to join their team %TEAM%. +command.team.invite.message.invited=You received a invitation for the game jam on %GUILD% +command.team.invite.message.noleader=Only the group leader can invite people. +command.team.invite.message.notRegistered=This user is not registered for the game jam. +command.team.invite.message.partofteam=This user is already part of a team. +command.team.invite.message.send=Invitation send. +command.team.invite.options.user.description=The user you want to invite +command.team.leave.description=Leave your team +command.team.leave.left=You left the team. +command.team.leave.leftBroadcast=%USER% left the team. +command.team.leave.message.leaderleave=The leader cant leave the team. +command.team.list.description=Get a list of all existing teams. +command.team.profile.description=Shows the profile of a team or your own +command.team.profile.leader=Leader +command.team.profile.member=Member +command.team.profile.message.nouserteam=This user is not part of a team. +command.team.profile.projecturl=Project Url +command.team.profile.options.team.description=Show this team +command.team.profile.options.user.description=Show team of this user +command.team.promote.description=Change the team leader. +command.team.promote.message.done=Changed the team leader. +command.team.promote.message.notinteam=This user is not part of your team. +command.team.promote.options.user.description=The new team leader. +command.team.rename.description=Change the team name. +command.team.rename.message.done=Team name changed. +command.team.rename.options.name.description=The new team name +command.unregister.description=Withdraw your registration +command.unregister.message.inteam=You need to leave your team first. +command.unregister.message.notregistered=You are not registered for a jam. +command.unregister.message.unregistered=You are now unregistered. +command.votes.description=Cast votes +command.votes.info.description=Information about your votes. +command.votes.info.embed.title=Given Votes +command.votes.ranking.description=The current ranking +command.votes.ranking.embed.votes=Votes +command.votes.ranking.message.voteactive=A vote is active. Rankings can be accessed after the polls are closed. +command.votes.vote.description=Vote for a team. +command.votes.vote.message.done=You gave %TEAM% %POINTS% points. Remaining points: %REMAINING% +command.votes.vote.message.maxpointsreached=You have reached the max amount of given points. Remove points from other teams if you want to add more. Remaining points: %REMAINING% +command.votes.vote.message.notactive=No voting is active. +command.votes.vote.message.ownteam=You cant vote for your own team. +command.votes.vote.options.points.description=The points to give. +command.votes.vote.options.team.description=The team you want to vote for. + +error.alreadyActive=A jam is already active. +error.invalidpath=Invalid path +error.invalidrimeformat=Invalid time format. Format is %FORMAT% +error.invalidtimezone=Invalid timezone. +error.maxteamsize=The team has reached the max size. +error.noactivejam=There is no active jam. +error.noconfirm=Please confirm by setting the confirm parameter +error.nojamactive=No jam is in progress. Teams are not available. +error.noleader=Only the leader can do this. +error.noteam=You are not part of a team. +error.notregistered=You are not registered for a jam. +error.noupcomingjam=There is no upcoming jam. +error.pluginnotfound=Plugin not found +error.servernotrunning=Server is not online. +error.unkownteam=This team does not exist. +error.votingactive=This function is not available while a vote is active. + +teamserver.message.detailstatus.existing.description=Server running +teamserver.message.detailstatus.nonexisting.description=Server not set up. + +word.activethreads=Active threads +word.api=Api +word.averageticktime=Average Tick time +word.max=Max +word.memory=Memory +word.min=min +word.notrunning=Not running +word.players=Players +word.ports=Ports +word.server=Server +word.serversetup=Server set up +word.system=System +word.total=Total +word.tps=Tps +word.used=Used + +words.dateformat=yyyy.MM.dd HH:mm +words.format=Format +command.team.edit.description=Edit team information diff --git a/bot/src/main/resources/log4j2.xml b/bot/src/main/resources/log4j2.xml new file mode 100644 index 0000000..f05b7d9 --- /dev/null +++ b/bot/src/main/resources/log4j2.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bot/src/main/resources/wait.sh b/bot/src/main/resources/wait.sh new file mode 100644 index 0000000..afc8240 --- /dev/null +++ b/bot/src/main/resources/wait.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +while screen -ls | grep -q "$1" + do + sleep 1 + echo "wait" + done +sleep 1 diff --git a/bot/src/test/java/de/chojo/gamejam/data/wrapper/jam/TimeFrameTest.java b/bot/src/test/java/de/chojo/gamejam/data/wrapper/jam/TimeFrameTest.java new file mode 100644 index 0000000..449bab0 --- /dev/null +++ b/bot/src/test/java/de/chojo/gamejam/data/wrapper/jam/TimeFrameTest.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.data.wrapper.jam; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +class TimeFrameTest { + private static final ZoneId berlin = ZoneId.of("Europe/Berlin"); + private static final ZoneId newyork = ZoneId.of("America/New_York"); + + @Test + void equals() { + Assertions.assertEquals(TimeFrame.fromEpoch(0, 1000, berlin), TimeFrame.fromEpoch(0, 1000, berlin)); + Assertions.assertNotEquals(TimeFrame.fromEpoch(0, 1500, berlin), TimeFrame.fromEpoch(0, 1000, berlin)); + } + + @Test + void fromEpoch() { + var berlinFrame = TimeFrame.fromEpoch(0, 1000, berlin); + var newyorkFrame = TimeFrame.fromEpoch(0, 1000, newyork); + Assertions.assertNotEquals(berlinFrame, newyorkFrame); + } + + @Test + void fromTimestamp() { + var frame = TimeFrame.fromEpoch(0, 1000, berlin); + var start = frame.startTimestamp(); + var end = frame.endTimestamp(); + + Assertions.assertEquals(frame, TimeFrame.fromTimestamp(start, end, berlin)); + } +} diff --git a/bot/src/test/java/de/chojo/gamejam/dataconsistency/LocalizationTest.java b/bot/src/test/java/de/chojo/gamejam/dataconsistency/LocalizationTest.java new file mode 100644 index 0000000..3ddfefc --- /dev/null +++ b/bot/src/test/java/de/chojo/gamejam/dataconsistency/LocalizationTest.java @@ -0,0 +1,153 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.dataconsistency; + +import net.dv8tion.jda.api.interactions.DiscordLocale; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class LocalizationTest { + private static final Pattern LOCALIZATION_CODE = Pattern.compile("\\$([a-zA-Z.]+?)\\$"); + private static final Pattern SIMPLE_LOCALIZATION_CODE = Pattern.compile("\"([a-zA-Z]+?\\.[a-zA-Z.]+)\""); + private static final Pattern REPLACEMENTS = Pattern.compile("%[a-zA-Z\\d.]+?%"); + private static final Set WHITELIST = Set.of("bot.config", "bot.testmode", "bot.cleancommands", "yyyy.MM.dd", "io.javalin.example.java"); + private static final Set WHITELIST_ENDS = Set.of(".gg", ".com", "bot.config", ".png", ".json", ".yml", ".jar", ".properties", ".zip", ".log", ".lock", ".sh"); + + private static final DiscordLocale[] LOCALES = { + DiscordLocale.ENGLISH_US, + DiscordLocale.GERMAN + }; + + + @Test + public void checkKeys() { + Map resourceBundles = new EnumMap<>(DiscordLocale.class); + for (var code : LOCALES) { + var bundle = ResourceBundle.getBundle("locale", Locale.forLanguageTag(code.getLocale())); + resourceBundles.put(code, bundle); + } + + System.out.printf("Loaded %s languages!%n", LOCALES.length); + + Set keySet = new HashSet<>(); + for (var resourceBundle : resourceBundles.values()) { + keySet.addAll(resourceBundle.keySet()); + } + + Map> replacements = new HashMap<>(); + var english = resourceBundles.get(DiscordLocale.ENGLISH_US); + for (var key : english.keySet()) { + replacements.put(key, getReplacements(english.getString(key))); + Assertions.assertFalse(english.getString(key).isBlank(), + "Blank string at " + key + "@" + DiscordLocale.ENGLISH_US); + } + + for (var resourceBundle : resourceBundles.values()) { + for (var key : keySet) { + var keyLoc = key + "@" + resourceBundle.getLocale(); + var locale = resourceBundle.getString(key); + Assertions.assertFalse(locale.isBlank(), "Blank or unlocalized key at " + keyLoc); + var localeReplacements = getReplacements(locale); + var defReplacements = replacements.get(key); + Assertions.assertTrue(localeReplacements.containsAll(defReplacements), + "Missing replacement key in " + keyLoc + + ". Expected \"" + String.join(", ", defReplacements) + "\". Actual \"" + String.join(", ", localeReplacements) + "\""); + } + } + } + + private Set getReplacements(String message) { + Set found = new HashSet<>(); + var matcher = REPLACEMENTS.matcher(message); + while (matcher.find()) { + found.add(matcher.group()); + } + return found; + } + + @Test + public void detectMissingKeys() throws IOException { + var keys = ResourceBundle.getBundle("locale").keySet(); + List files; + try (var stream = Files.walk(Path.of("src", "main", "java"))) { + files = stream + .filter(p -> p.toFile().isFile()) + .collect(Collectors.toCollection(ArrayList::new)); + } + + try (var stream = Files.walk(Path.of("src", "main", "resources"))) { + files.addAll(stream + .filter(p -> p.toFile().isFile()) + .filter(p -> p.getFileName().startsWith("locale")) + .toList()); + } + + var count = 0; + + Set foundKeys = new HashSet<>(); + + for (var file : files) { + var localCount = 0; + List content; + content = Files.readAllLines(file); + + var currentLine = 0; + + for (var line : content) { + currentLine++; + var matcher = SIMPLE_LOCALIZATION_CODE.matcher(line); + while (matcher.find()) { + count++; + localCount++; + var key = matcher.group(1); + foundKeys.add(key); + Assertions.assertTrue(keys.contains(key) || whitelisted(key), "Found unkown key \"" + key + "\" in " + file + " at line " + currentLine); + } + + matcher = LOCALIZATION_CODE.matcher(line); + while (matcher.find()) { + count++; + localCount++; + var key = matcher.group(1); + foundKeys.add(key); + Assertions.assertTrue(keys.contains(key) || whitelisted(key), "Found unkown key \"" + key + "\" in " + file + " at line " + currentLine); + } + } + System.out.println("Found " + localCount + " key in " + file); + } + System.out.println("Found a total of " + count + " keys in " + files.size() + " files."); + + keys.removeAll(foundKeys); + System.out.println("Found " + keys.size() + " without any direct usage in the code."); + for (var key : keys) { + System.out.println(key); + } + } + + private boolean whitelisted(String key) { + if (WHITELIST.contains(key)) return true; + for (var end : WHITELIST_ENDS) { + if (key.endsWith(end)) return true; + } + return false; + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 317ebef..e47823d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,17 +4,50 @@ plugins { } group = "de.chojo" -version = "1.0" +version = "1.0.0" subprojects { apply { plugin() + plugin() } } allprojects { + repositories { + mavenCentral() + maven("https://eldonexus.de/repository/maven-public/") + maven("https://eldonexus.de/repository/maven-proxies/") + } + license { header(rootProject.file("HEADER.txt")) include("**/*.java") } + + java { + withSourcesJar() + withJavadocJar() + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + + tasks { + test { + dependsOn(licenseCheck) + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } + } + + compileJava { + options.encoding = "UTF-8" + } + + javadoc { + options.encoding = "UTF-8" + } + } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index cc02e63..0000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -plugins { - `kotlin-dsl` -} - -repositories { - gradlePluginPortal() -} diff --git a/buildSrc/src/main/kotlin/de.chojo.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/de.chojo.java-conventions.gradle.kts deleted file mode 100755 index deb9996..0000000 --- a/buildSrc/src/main/kotlin/de.chojo.java-conventions.gradle.kts +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - `java-library` -} - -group = "de.chojo" -version = "2.1.2" - -repositories { - maven("https://eldonexus.de/repository/maven-public") - maven("https://eldonexus.de/repository/maven-proxies") - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") -} - -dependencies { - compileOnly("org.jetbrains", "annotations", "20.1.0") - - testImplementation(platform("org.junit:junit-bom:5.7.2")) - testImplementation("org.junit.jupiter:junit-jupiter") -} - -allprojects { - java { - sourceCompatibility = JavaVersion.VERSION_15 - withSourcesJar() - withJavadocJar() - } -} - -tasks { - compileJava { - options.encoding = "UTF-8" - } - - compileTestJava { - options.encoding = "UTF-8" - } -} diff --git a/buildSrc/src/main/kotlin/de.chojo.library-conventions.gradle.kts b/buildSrc/src/main/kotlin/de.chojo.library-conventions.gradle.kts deleted file mode 100755 index 39672aa..0000000 --- a/buildSrc/src/main/kotlin/de.chojo.library-conventions.gradle.kts +++ /dev/null @@ -1,5 +0,0 @@ -plugins { - `java-library` - `maven-publish` - id("de.chojo.java-conventions") -} diff --git a/conf/dev/config.json b/conf/dev/config.json new file mode 100755 index 0000000..dc566d6 --- /dev/null +++ b/conf/dev/config.json @@ -0,0 +1,20 @@ +{ + "baseSettings" : { + "token" : "NzY2Njg5NDMzNjQxNjgwODk3.X4nBLQ.02vjfG0dF0YWrH450JkayB6bsF4", + "botOwner" : [ ] + }, + "database" : { + "host" : "database", + "port" : "5432", + "database" : "db", + "schema" : "public", + "user" : "postgres", + "password" : "changeme", + "poolSize" : 5 + }, + "api" : { + "host" : "0.0.0.0", + "port" : 8888, + "token" : "letmein" + } +} diff --git a/conf/dev/temp.config.json b/conf/dev/temp.config.json new file mode 100644 index 0000000..468fd12 --- /dev/null +++ b/conf/dev/temp.config.json @@ -0,0 +1,20 @@ +{ + "baseSettings" : { + "token" : "", + "botOwner" : [ ] + }, + "database" : { + "host" : "database", + "port" : "5432", + "database" : "db", + "schema" : "public", + "user" : "postgres", + "password" : "changeme", + "poolSize" : 5 + }, + "api" : { + "host" : "0.0.0.0", + "port" : 8888, + "token" : "letmein" + } +} diff --git a/docker/bot.DockerFile b/docker/bot.DockerFile new file mode 100644 index 0000000..22f9d27 --- /dev/null +++ b/docker/bot.DockerFile @@ -0,0 +1,8 @@ +FROM openjdk:18 + +WORKDIR /app + +ADD bot/build/libs/bot-1.0-all.jar bot.jar +ADD conf/dev/config.json config.json + +ENTRYPOINT ["java", "-Dbot.config=./config.json","-jar", "bot.jar"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..641dca8 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,28 @@ +version: v3 + +services: + bot: + image: game-jam-bot + build: + dockerfile: docker/bot.DockerFile + context: .. + networks: + - plugin-jam + depends_on: + - database + ports: + - 8888:8888 + database: + image: postgres:14.2 + restart: always + user: postgres + environment: + POSTGRES_PASSWORD: "changeme" + POSTGRES_USER: "postgres" + POSTGRES_DB: "db" + networks: + - plugin-jam +networks: + plugin-jam: + name: "plugin-jam" + external: false diff --git a/docker/kubernetes/database.yaml b/docker/kubernetes/database.yaml new file mode 100644 index 0000000..6f202de --- /dev/null +++ b/docker/kubernetes/database.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: database +spec: + template: + metadata: + labels: + app: database + spec: + containers: + - env: + - name: POSTGRES_DB + value: db + - name: POSTGRES_PASSWORD + value: changeme + - name: POSTGRES_USER + value: postgres + image: postgres:14.2 + name: database + volumeMounts: + - mountPath: "var/lib/postgresql/data" + name: postgres-data + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: postgres-data + selector: + matchLabels: + app: database +--- +apiVersion: v1 +kind: Service +metadata: + name: database +spec: + selector: + app: database + ports: + - name: postgres + protocol: TCP + port: 5432 +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: postgres-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 3Gi diff --git a/docker/kubernetes/game-jam-bot.yaml b/docker/kubernetes/game-jam-bot.yaml new file mode 100644 index 0000000..d8d27c1 --- /dev/null +++ b/docker/kubernetes/game-jam-bot.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bot +spec: + template: + metadata: + labels: + app: bot + spec: + containers: + - name: bot + image: game-jam-bot + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8888 + name: jam-bot + initContainers: + - name: init-myservice + image: busybox:1.28 + command: [ 'sh', '-c', "until nslookup database.default.svc.cluster.local; do echo waiting for myservice; sleep 2; done" ] + + selector: + matchLabels: + app: bot +--- +apiVersion: v1 +kind: Service +metadata: + name: bot-01 +spec: + ports: + - name: http + port: 80 + targetPort: jam-bot + protocol: TCP + selector: + app: bot diff --git a/docker/kubernetes/ingress.yaml b/docker/kubernetes/ingress.yaml new file mode 100644 index 0000000..563e29d --- /dev/null +++ b/docker/kubernetes/ingress.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: game-jam-bot-ingres + annotations: + kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.middlewares: default-game-jam-strip-prefix@kubernetescrd +spec: + rules: + - http: + paths: + - path: /game-jam + backend: + service: + name: bot-01 + port: + name: http + pathType: Prefix +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: game-jam-strip-prefix +spec: + stripPrefix: + prefixes: + - /game-jam diff --git a/docker/kubernetes/kustomization.yaml b/docker/kubernetes/kustomization.yaml new file mode 100644 index 0000000..0f57bb1 --- /dev/null +++ b/docker/kubernetes/kustomization.yaml @@ -0,0 +1,7 @@ +kind: Kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 + +resources: + - ingress.yaml + - database.yaml + - game-jam-bot.yaml diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 00e33ed..ae04661 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/plugin-api/build.gradle.kts b/plugin-api/build.gradle.kts new file mode 100644 index 0000000..ae2bfec --- /dev/null +++ b/plugin-api/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + java + `java-library` +} + +group = "de.chojo" +version = "1.0" + +repositories { + mavenCentral() +} + +dependencies { + api("com.fasterxml.jackson.core", "jackson-databind", "2.13.4.2") + testImplementation("org.junit.jupiter", "junit-jupiter-api", "5.6.0") + testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine") +} + +tasks.getByName("test") { + useJUnitPlatform() +} diff --git a/plugin-api/src/main/java/de/chojo/pluginjam/payload/Registration.java b/plugin-api/src/main/java/de/chojo/pluginjam/payload/Registration.java new file mode 100644 index 0000000..7f5b90a --- /dev/null +++ b/plugin-api/src/main/java/de/chojo/pluginjam/payload/Registration.java @@ -0,0 +1,10 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.payload; + +public record Registration(int id, String name, int port, int apiPort) { +} diff --git a/plugin-api/src/main/java/de/chojo/pluginjam/payload/RequestsPayload.java b/plugin-api/src/main/java/de/chojo/pluginjam/payload/RequestsPayload.java new file mode 100644 index 0000000..9f6b347 --- /dev/null +++ b/plugin-api/src/main/java/de/chojo/pluginjam/payload/RequestsPayload.java @@ -0,0 +1,10 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.payload; + +public record RequestsPayload (boolean restart){ +} diff --git a/plugin-api/src/main/java/de/chojo/pluginjam/payload/StatsPayload.java b/plugin-api/src/main/java/de/chojo/pluginjam/payload/StatsPayload.java new file mode 100644 index 0000000..462d8f6 --- /dev/null +++ b/plugin-api/src/main/java/de/chojo/pluginjam/payload/StatsPayload.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.payload; + +public record StatsPayload( + double[] tps, + double averageTickTime, + int onlinePlayers, + int activeThreads, + Memory memory +) { + public record Memory(long totalMb, + long freeMb, + long usedMb, + long maxMb) { + } +} diff --git a/plugin-paper/build.gradle.kts b/plugin-paper/build.gradle.kts new file mode 100644 index 0000000..1816f8f --- /dev/null +++ b/plugin-paper/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + java + id("net.minecrell.plugin-yml.bukkit") version "0.5.2" + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +group = "de.chojo" + +dependencies { + implementation(project(":plugin-api")) + compileOnly("io.papermc.paper", "paper-api", "1.19.2-R0.1-SNAPSHOT") + implementation("io.javalin", "javalin", "4.6.5") + implementation("de.eldoria", "eldo-util", "1.14.0-DEV") + implementation("org.slf4j", "slf4j-api", "1.7.36") +} + +tasks { + shadowJar { + val shadebase = "de.chojo.pluginjam." + relocate("de.eldoria.eldoutilities", shadebase + "eldoutilities") + //relocate("io.javalin", shadebase + "javalin") + mergeServiceFiles() + archiveFileName.set("pluginjam.jar") + //minimize() + } + + register("copyToServer") { + val path = project.property("targetDir") ?: ""; + if (path.toString().isEmpty()) { + println("targetDir is not set in gradle properties") + return@register + } + from(shadowJar) + destinationDir = File(path.toString()) + } + + build{ + dependsOn(shadowJar) + } +} + +bukkit { + name = "PluginJam" + main = "de.chojo.pluginjam.PluginJam" + website = "https://github.com/devcordde/plugin-jam-bot" + apiVersion = "1.19" + version = rootProject.version.toString() + authors = listOf("Taucher2003", "RainbowdashLabs") +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/PluginJam.java b/plugin-paper/src/main/java/de/chojo/pluginjam/PluginJam.java new file mode 100644 index 0000000..2ead7e9 --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/PluginJam.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam; + +import de.chojo.pluginjam.api.Api; +import de.chojo.pluginjam.greeting.Welcomer; +import de.chojo.pluginjam.service.CommandBlocker; +import de.chojo.pluginjam.service.JoinService; +import de.chojo.pluginjam.service.ServerRequests; +import de.chojo.pluginjam.velocity.ReportService; +import de.eldoria.eldoutilities.localization.ILocalizer; +import de.eldoria.eldoutilities.plugin.EldoPlugin; +import org.bukkit.event.Listener; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.Locale; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +public class PluginJam extends EldoPlugin implements Listener { + private Api api; + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + private ReportService service; + + @Override + public void onPluginEnable() { + saveDefaultConfig(); + + var localizer = ILocalizer.create(this, "de_DE"); + localizer.setLocale("de_DE"); + + var serverRequests = new ServerRequests(); + + api = Api.create(this, serverRequests); + service = ReportService.create(this, executor); + + registerListener(new CommandBlocker(serverRequests, localizer), new Welcomer(this), new JoinService(this, serverRequests, localizer)); + } + + @Override + public void onPluginDisable() { + service.shutdown(); + executor.shutdown(); + } +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/api/Api.java b/plugin-paper/src/main/java/de/chojo/pluginjam/api/Api.java new file mode 100644 index 0000000..dec6e7d --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/api/Api.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.api; + +import de.chojo.pluginjam.PluginJam; +import de.chojo.pluginjam.api.routes.Configuration; +import de.chojo.pluginjam.api.routes.Requests; +import de.chojo.pluginjam.api.routes.Stats; +import de.chojo.pluginjam.service.ServerRequests; +import io.javalin.Javalin; +import org.bukkit.plugin.Plugin; +import org.slf4j.Logger; + +import static io.javalin.apibuilder.ApiBuilder.before; +import static io.javalin.apibuilder.ApiBuilder.path; +import static org.slf4j.LoggerFactory.getLogger; + +public class Api { + private static final Logger log = getLogger(Api.class); + private final Javalin javalin; + private final Configuration configuration; + private final Stats stats; + private final Requests requests; + + private Api(Javalin javalin, Plugin plugin, ServerRequests serverRequests) { + this.javalin = javalin; + configuration = new Configuration(plugin); + stats = new Stats(plugin); + requests = new Requests(serverRequests); + } + + public static Api create(Plugin plugin, ServerRequests serverRequests) { + var classLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(PluginJam.class.getClassLoader()); + var javalin = Javalin.create(); + javalin.start("0.0.0.0", Integer.parseInt(System.getProperty("javalin.port", "30000"))); + Thread.currentThread().setContextClassLoader(classLoader); + var api = new Api(javalin, plugin, serverRequests); + api.ignite(); + return api; + } + + private void ignite() { + javalin.routes(() -> { + before(ctx -> log.debug("Received request on {}.", ctx.path())); + path("v1", configuration::buildRoutes); + path("v1", stats::buildRoutes); + path("v1", requests::buildRoutes); + }); + } +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/api/routes/Configuration.java b/plugin-paper/src/main/java/de/chojo/pluginjam/api/routes/Configuration.java new file mode 100644 index 0000000..d19389d --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/api/routes/Configuration.java @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.api.routes; + +import io.javalin.http.HttpCode; +import org.bukkit.plugin.Plugin; + +import static io.javalin.apibuilder.ApiBuilder.path; +import static io.javalin.apibuilder.ApiBuilder.post; + +public class Configuration { + private final Plugin plugin; + + public Configuration(Plugin plugin) { + this.plugin = plugin; + } + + public void buildRoutes() { + path("config", () -> { + post("message", ctx -> { + plugin.getConfig().set("message", ctx.body()); + plugin.saveConfig(); + ctx.status(HttpCode.OK); + }); + + post("maxplayers", ctx -> { + plugin.getConfig().set("maxplayers", Integer.parseInt(ctx.body())); + plugin.saveConfig(); + ctx.status(HttpCode.OK); + }); + + post("spectatoroverflow", ctx -> { + plugin.getConfig().set("spectatoroverflow", Boolean.parseBoolean(ctx.body())); + plugin.saveConfig(); + ctx.status(HttpCode.OK); + }); + + post("reviewmode", ctx -> { + plugin.getConfig().set("spectatoroverflow", Boolean.parseBoolean(ctx.body())); + plugin.saveConfig(); + ctx.status(HttpCode.OK); + }); + + post("whitelist", ctx -> { + plugin.getServer().setWhitelist(Boolean.parseBoolean(ctx.body())); + ctx.status(HttpCode.OK); + }); + }); + } +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/api/routes/Requests.java b/plugin-paper/src/main/java/de/chojo/pluginjam/api/routes/Requests.java new file mode 100644 index 0000000..cb8b60d --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/api/routes/Requests.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.api.routes; + +import de.chojo.pluginjam.service.ServerRequests; +import org.bukkit.plugin.Plugin; + +import static io.javalin.apibuilder.ApiBuilder.get; +import static io.javalin.apibuilder.ApiBuilder.path; + +public class Requests { + private final ServerRequests requests; + + public Requests(ServerRequests requests) { + this.requests = requests; + } + + public void buildRoutes() { + path("requests", () -> { + get("", ctx -> { + ctx.json(requests.get()); + }); + }); + } +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/api/routes/Stats.java b/plugin-paper/src/main/java/de/chojo/pluginjam/api/routes/Stats.java new file mode 100644 index 0000000..1546798 --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/api/routes/Stats.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.api.routes; + +import de.chojo.pluginjam.payload.StatsPayload; +import org.bukkit.plugin.Plugin; + +import static io.javalin.apibuilder.ApiBuilder.get; +import static io.javalin.apibuilder.ApiBuilder.path; + +public class Stats { + private static final int MB = 1024 * 1024; + private final Plugin plugin; + + public Stats(Plugin plugin) { + this.plugin = plugin; + } + + public void buildRoutes() { + path("stats", () -> { + get("", ctx -> { + var server = plugin.getServer(); + var tps = server.getTPS(); + var avgTickTime = server.getAverageTickTime(); + var onlinePlayers = server.getOnlinePlayers().size(); + + var instance = Runtime.getRuntime(); + var total = instance.totalMemory() / MB; + var free = instance.freeMemory() / MB; + var used = total - free / MB; + var max = instance.maxMemory() / MB; + var activeThreads = Thread.activeCount(); + + var statsPayload = new StatsPayload(tps, avgTickTime, onlinePlayers, activeThreads, + new StatsPayload.Memory(total, free, used, max)); + + ctx.json(statsPayload); + }); + }); + } +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/greeting/Welcomer.java b/plugin-paper/src/main/java/de/chojo/pluginjam/greeting/Welcomer.java new file mode 100644 index 0000000..a43f1b7 --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/greeting/Welcomer.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.greeting; + +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.plugin.Plugin; + +public class Welcomer implements Listener { + + private final Plugin plugin; + + public Welcomer(Plugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + var message = MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("message", "Welcome!")); + event.getPlayer().sendMessage(message); + } +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/service/CommandBlocker.java b/plugin-paper/src/main/java/de/chojo/pluginjam/service/CommandBlocker.java new file mode 100644 index 0000000..939e5b0 --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/service/CommandBlocker.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.service; + +import de.eldoria.eldoutilities.localization.ILocalizer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; + +import java.util.List; + +public class CommandBlocker implements Listener { + private final List blocked = List.of("/restart", "/stop", "/minecraft:stop"); + private final ServerRequests requests; + private final ILocalizer localizer; + + public CommandBlocker(ServerRequests requests, ILocalizer localizer) { + this.requests = requests; + this.localizer = localizer; + } + + @EventHandler + public void onCommand(PlayerCommandPreprocessEvent event) { + var cmd = event.getMessage(); + + if ("/restart".equals(cmd) || "/spigot:restart".equals(cmd)) { + event.getPlayer().sendMessage(Component.text(localizer.localize("commandblocked.restartrequested"))); + requests.restartByCommand(true); + event.setCancelled(true); + return; + } + + for (var block : blocked) { + if (cmd.startsWith(block)) { + event.getPlayer().sendMessage(Component.text(localizer.localize("commandblocker.blocked"), NamedTextColor.RED)); + event.setCancelled(true); + return; + } + } + } +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/service/JoinService.java b/plugin-paper/src/main/java/de/chojo/pluginjam/service/JoinService.java new file mode 100644 index 0000000..47a9706 --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/service/JoinService.java @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.service; + +import de.eldoria.eldoutilities.localization.ILocalizer; +import net.kyori.adventure.text.Component; +import org.bukkit.GameMode; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.Plugin; + +public class JoinService implements Listener { + private final Plugin plugin; + private final ServerRequests requests; + private final ILocalizer localizer; + + public JoinService(Plugin plugin, ServerRequests requests, ILocalizer localizer) { + this.plugin = plugin; + this.requests = requests; + this.localizer = localizer; + } + + @EventHandler(priority = EventPriority.HIGH) + public void onPlayerJoin(PlayerJoinEvent event) { + if (plugin.getServer().getOnlinePlayers().size() < maxPlayers()) return; + + // Ops are welcome + if (event.getPlayer().isOp()) return; + + // Silent join + event.joinMessage(Component.empty()); + + if (isSpectatorOverflow()) { + event.getPlayer().setGameMode(GameMode.SPECTATOR); + event.getPlayer().sendMessage(localizer.localize("joinservice.spectatoroverflow")); + return; + } + + event.getPlayer().kick(Component.text(localizer.localize("joinservice.kick"))); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + if (plugin.getServer().getOnlinePlayers().isEmpty()) { + requests.restartByEmpty(true); + } + } + + private int maxPlayers() { + return plugin.getConfig().getInt("maxplayers", 50); + } + + private boolean isSpectatorOverflow() { + return plugin.getConfig().getBoolean("spectatoroverflow", false); + } +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/service/ServerRequests.java b/plugin-paper/src/main/java/de/chojo/pluginjam/service/ServerRequests.java new file mode 100644 index 0000000..ad7805e --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/service/ServerRequests.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.service; + +import de.chojo.pluginjam.payload.RequestsPayload; + +public class ServerRequests { + private boolean restartByCommand = false; + private boolean restartByEmpty = false; + + public RequestsPayload get() { + return new RequestsPayload(restartByCommand || restartByEmpty); + } + + public void restartByCommand(boolean state) { + restartByCommand = state; + } + + public void restartByEmpty(boolean state) { + restartByEmpty = state; + } +} diff --git a/plugin-paper/src/main/java/de/chojo/pluginjam/velocity/ReportService.java b/plugin-paper/src/main/java/de/chojo/pluginjam/velocity/ReportService.java new file mode 100644 index 0000000..8cd5548 --- /dev/null +++ b/plugin-paper/src/main/java/de/chojo/pluginjam/velocity/ReportService.java @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.velocity; + +import com.google.gson.Gson; +import de.chojo.pluginjam.payload.Registration; +import org.bukkit.plugin.Plugin; +import org.slf4j.Logger; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.slf4j.LoggerFactory.getLogger; + +public class ReportService implements Runnable { + private static final Logger log = getLogger(ReportService.class); + private final Plugin plugin; + private final HttpClient client = HttpClient.newBuilder().build(); + private final int id; + private final Gson gson = new Gson(); + private final String name; + private final int velocityApi; + private final int apiPort; + + private ReportService(Plugin plugin) { + this.plugin = plugin; + id = Integer.parseInt(System.getProperty("pluginjam.team.id")); + name = System.getProperty("pluginjam.team.name"); + velocityApi = Integer.parseInt(System.getProperty("pluginjam.port")); + apiPort = Integer.parseInt(System.getProperty("javalin.port", "30000")); + } + + public static ReportService create(Plugin plugin, ScheduledExecutorService executor) { + var service = new ReportService(plugin); + executor.scheduleAtFixedRate(service, 10, 10, TimeUnit.SECONDS); + service.register(); + return service; + } + + @Override + public void run() { + log.debug("Sending ping"); + ping(); + } + + private void register() { + log.info("Registering server at velocity instance"); + var registration = new Registration(id, name, plugin.getServer().getPort(), apiPort); + var builder = HttpRequest.newBuilder(apiUrl("v1", "server")) + .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(registration))) + .build(); + client.sendAsync(builder, HttpResponse.BodyHandlers.discarding()) + .whenComplete((res, err) -> { + if (err == null) { + log.info("Registered server at velocity instance"); + } else { + log.error("Could not register", err); + } + }); + } + + private void ping() { + var registration = new Registration(id, name, plugin.getServer().getPort(), apiPort); + var builder = HttpRequest.newBuilder(apiUrl("v1", "server")) + .method("PATCH", HttpRequest.BodyPublishers.ofString(gson.toJson(registration))) + .build(); + client.sendAsync(builder, HttpResponse.BodyHandlers.discarding()) + .whenComplete((res, err) -> { + if (err != null) { + log.error("Could not send ping to velocity instance", err); + } + }); + } + + private void unregister() { + log.info("Unregistering server at velocity instance."); + var builder = HttpRequest.newBuilder(queryApiUrl("id=%s&port=%s".formatted(id, + plugin.getServer() + .getPort()), + "v1", "server")) + .DELETE() + .build(); + client.sendAsync(builder, HttpResponse.BodyHandlers.discarding()) + .whenComplete((res, err) -> { + if (err != null) { + log.error("Could not unregister", err); + } else { + log.info("Server unregistered at velocity instance"); + } + }); + } + + public void shutdown() { + unregister(); + } + + private URI apiUrl(String... path) { + return URI.create("http://localhost:%d/%s".formatted(velocityApi, String.join("/", path))); + } + + private URI queryApiUrl(String query, String... path) { + return URI.create("http://localhost:%d/%s?%s".formatted(velocityApi, String.join("/", path), query)); + } +} diff --git a/plugin-paper/src/main/resources/config.yml b/plugin-paper/src/main/resources/config.yml new file mode 100644 index 0000000..f1c5646 --- /dev/null +++ b/plugin-paper/src/main/resources/config.yml @@ -0,0 +1,3 @@ +message: "" +maxplayers: 50 +spectatoroverlow: false diff --git a/plugin-paper/src/main/resources/messages.properties b/plugin-paper/src/main/resources/messages.properties new file mode 100644 index 0000000..46ce584 --- /dev/null +++ b/plugin-paper/src/main/resources/messages.properties @@ -0,0 +1,4 @@ +joinservice.spectatoroverflow= +joinservice.kick= +commandblocker.blocked= +commandblocked.restartrequested= diff --git a/plugin-paper/src/main/resources/messages_de_DE.properties b/plugin-paper/src/main/resources/messages_de_DE.properties new file mode 100644 index 0000000..3923628 --- /dev/null +++ b/plugin-paper/src/main/resources/messages_de_DE.properties @@ -0,0 +1,4 @@ +joinservice.spectatoroverflow=Server ist voll. Du wurdest in den Beobachtermodus gesetzt. +joinservice.kick=Server ist voll. +commandblocker.blocked=Dieser Befehl ist blockiert. +commandblocked.restartrequested=Neustart wurde angefragt. diff --git a/plugin-paper/src/main/resources/messages_en_US.properties b/plugin-paper/src/main/resources/messages_en_US.properties new file mode 100644 index 0000000..4590d5d --- /dev/null +++ b/plugin-paper/src/main/resources/messages_en_US.properties @@ -0,0 +1,4 @@ +joinservice.spectatoroverflow=Server full. You were set into spectator mode. +joinservice.kick=Server is full. +commandblocker.blocked=Command is blocked. +commandblocked.restartrequested=Restart requested. diff --git a/plugin-velocity/build.gradle.kts b/plugin-velocity/build.gradle.kts new file mode 100644 index 0000000..331f760 --- /dev/null +++ b/plugin-velocity/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + java + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +repositories{ + maven("https://repo.velocitypowered.com/snapshots/") +} + +dependencies{ + implementation("io.javalin", "javalin", "4.6.5") + implementation(project(":plugin-api")) + compileOnly("com.velocitypowered", "velocity-api", "1.0.0-SNAPSHOT") + annotationProcessor("com.velocitypowered", "velocity-api", "1.0.0-SNAPSHOT") +} + +tasks{ + shadowJar { + val shadebase = "de.chojo.pluginjam.libs" + //relocate("io.javalin", "$shadebase.javalin") + //relocate("org.eclipse", shadebase) + mergeServiceFiles() + archiveFileName.set("PluginJam.jar") + } + + register("copyToServer") { + val path = project.property("targetDir") ?: ""; + if (path.toString().isEmpty()) { + println("targetDir is not set in gradle properties") + return@register + } + from(shadowJar) + destinationDir = File(path.toString()) + } +} diff --git a/plugin-velocity/src/main/java/de/chojo/pluginjam/PluginJam.java b/plugin-velocity/src/main/java/de/chojo/pluginjam/PluginJam.java new file mode 100644 index 0000000..c5af078 --- /dev/null +++ b/plugin-velocity/src/main/java/de/chojo/pluginjam/PluginJam.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam; + +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.proxy.ProxyServer; +import de.chojo.pluginjam.servers.ServerRegistry; +import de.chojo.pluginjam.web.Api; +import org.slf4j.Logger; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static org.slf4j.LoggerFactory.getLogger; + +@Plugin(id = "pluginjam", name = "Plugin Jam", version = "1.0.0", authors = {"RainbowdashLabs"}) +public class PluginJam { + private static final Logger log = getLogger(PluginJam.class); + private final ProxyServer proxy; + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + private Api api; + + @Inject + public PluginJam(ProxyServer proxy) { + this.proxy = proxy; + log.info("Plugin Jam enabled"); + } + + @Subscribe + public void onProxyInitialization(ProxyInitializeEvent event) { + var registry = ServerRegistry.create(proxy, executor); + + api = Api.create(registry); + } +} diff --git a/plugin-velocity/src/main/java/de/chojo/pluginjam/configuration/Configuration.java b/plugin-velocity/src/main/java/de/chojo/pluginjam/configuration/Configuration.java new file mode 100644 index 0000000..4c463f9 --- /dev/null +++ b/plugin-velocity/src/main/java/de/chojo/pluginjam/configuration/Configuration.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.configuration; + +import com.google.gson.Gson; +import de.chojo.pluginjam.configuration.elements.Api; +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.slf4j.LoggerFactory.getLogger; + +public class Configuration { + private static final Logger log = getLogger(Configuration.class); + private Api api = new Api(); + + public Api api() { + return api; + } + + public static Configuration load() throws IOException { + var gson = new Gson(); + + var path = Path.of("plugins", "pluginjam", "config.json").toAbsolutePath(); + + log.info("Loading {}", path); + + if (!path.toFile().exists()) { + Files.createDirectories(path.getParent()); + Files.writeString(path, gson.toJson(new Configuration()), StandardCharsets.UTF_8); + } + + var configuration = gson.fromJson(Files.readString(path), Configuration.class); + + Files.writeString(path, gson.toJson(configuration), StandardCharsets.UTF_8); + + return configuration; + } +} diff --git a/plugin-velocity/src/main/java/de/chojo/pluginjam/configuration/elements/Api.java b/plugin-velocity/src/main/java/de/chojo/pluginjam/configuration/elements/Api.java new file mode 100644 index 0000000..864d5fd --- /dev/null +++ b/plugin-velocity/src/main/java/de/chojo/pluginjam/configuration/elements/Api.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.configuration.elements; + +public class Api { + private String host = "localhost"; + private int port = 25000; + + public String host() { + return host; + } + + public int port() { + return port; + } +} diff --git a/plugin-velocity/src/main/java/de/chojo/pluginjam/servers/ServerRegistry.java b/plugin-velocity/src/main/java/de/chojo/pluginjam/servers/ServerRegistry.java new file mode 100644 index 0000000..79c0e07 --- /dev/null +++ b/plugin-velocity/src/main/java/de/chojo/pluginjam/servers/ServerRegistry.java @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.servers; + +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import de.chojo.pluginjam.payload.Registration; +import de.chojo.pluginjam.servers.exceptions.AlreadyRegisteredException; +import org.slf4j.Logger; + +import java.net.InetSocketAddress; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.slf4j.LoggerFactory.getLogger; + +public class ServerRegistry implements Runnable { + private static final Logger log = getLogger(ServerRegistry.class); + private final Map ids = new HashMap<>(); + private final Map ports = new HashMap<>(); + private final Map seen = new HashMap<>(); + + private final ProxyServer proxy; + + private ServerRegistry(ProxyServer proxy) { + this.proxy = proxy; + } + + public static ServerRegistry create(ProxyServer proxy, ScheduledExecutorService executor) { + var registry = new ServerRegistry(proxy); + executor.scheduleAtFixedRate(registry, 1, 1, TimeUnit.MINUTES); + return registry; + } + + @Override + public void run() { + var timeout = Instant.now().minus(2, ChronoUnit.MINUTES); + for (var entry : new HashMap<>(seen).entrySet()) { + if (entry.getValue().isBefore(timeout)) { + log.info("Server {}:{} timed out.", entry.getKey().name(), entry.getKey().port()); + unregister(entry.getKey()); + } + } + } + + public void register(Registration registration) { + var reg = ids.get(registration.id()); + + if (reg != null && !reg.equals(registration)) { + throw AlreadyRegisteredException.forName(registration.name()); + } + + reg = ports.get(registration.port()); + + if (reg != null && !reg.equals(registration)) { + throw AlreadyRegisteredException.forPort(registration.port()); + } + + log.info("Registered server {} on port {}", registration.name(), registration.port()); + + ids.put(registration.id(), registration); + ports.put(registration.port(), registration); + + proxy.registerServer(new ServerInfo(registration.name(), new InetSocketAddress("localhost", registration.port()))); + ping(registration); + } + + public void ping(Registration registration) { + log.debug("Ping of server {} received.", registration.name()); + seen.put(registration, Instant.now()); + if (!ids.containsKey(registration.id()) && !ports.containsKey(registration.port())) { + log.info("Received ping of unkown server {} with id {}", registration.id(), registration.name()); + log.info("Attempting to register server."); + register(registration); + } + } + + public void unregister(Registration registration) { + var removed = ids.remove(registration.id()); + if(removed == null){ + log.warn("Unregistered server {} from port {}, but this server is not known.", registration.id(), registration.port()); + return; + } + ports.remove(removed.port()); + seen.remove(removed); + log.info("Unregistered server {} on port {}", removed.name(), removed.port()); + proxy.unregisterServer(new ServerInfo(removed.name(), new InetSocketAddress("localhost", removed.port()))); + } + + public Collection server() { + return ids.values(); + } +} diff --git a/plugin-velocity/src/main/java/de/chojo/pluginjam/servers/exceptions/AlreadyRegisteredException.java b/plugin-velocity/src/main/java/de/chojo/pluginjam/servers/exceptions/AlreadyRegisteredException.java new file mode 100644 index 0000000..00d30ef --- /dev/null +++ b/plugin-velocity/src/main/java/de/chojo/pluginjam/servers/exceptions/AlreadyRegisteredException.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.servers.exceptions; + +public class AlreadyRegisteredException extends RuntimeException { + private AlreadyRegisteredException(String message) { + super(message); + } + + public static AlreadyRegisteredException forName(String name) { + return new AlreadyRegisteredException("Name " + name + " is already in use."); + } + + public static AlreadyRegisteredException forPort(int port) { + return new AlreadyRegisteredException("Port " + port + " is already in use."); + } +} diff --git a/plugin-velocity/src/main/java/de/chojo/pluginjam/web/Api.java b/plugin-velocity/src/main/java/de/chojo/pluginjam/web/Api.java new file mode 100644 index 0000000..fc817a4 --- /dev/null +++ b/plugin-velocity/src/main/java/de/chojo/pluginjam/web/Api.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.web; + +import com.velocitypowered.api.proxy.ProxyServer; +import de.chojo.pluginjam.PluginJam; +import de.chojo.pluginjam.configuration.Configuration; +import de.chojo.pluginjam.servers.ServerRegistry; +import de.chojo.pluginjam.web.server.Server; +import io.javalin.Javalin; +import org.slf4j.Logger; + +import static io.javalin.apibuilder.ApiBuilder.before; +import static io.javalin.apibuilder.ApiBuilder.path; +import static org.slf4j.LoggerFactory.getLogger; + +public class Api { + private static final Logger log = getLogger(Api.class); + private final Javalin javalin; + private final Server server; + + private Api(ServerRegistry registry, Javalin javalin) { + this.javalin = javalin; + this.server = new Server(registry); + } + + public static Api create(ServerRegistry registry) { + var classLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(PluginJam.class.getClassLoader()); + var javalin = Javalin.create(); + javalin.start("0.0.0.0", Integer.parseInt(System.getProperty("javalin.port", "30000"))); + Thread.currentThread().setContextClassLoader(classLoader); + var api = new Api(registry, javalin); + api.ignite(); + return api; + } + + private void ignite() { + javalin.routes(() -> { + before(ctx -> log.debug("Received request on {}.", ctx.path())); + path("v1", server::buildRoutes); + }); + } +} diff --git a/plugin-velocity/src/main/java/de/chojo/pluginjam/web/server/Server.java b/plugin-velocity/src/main/java/de/chojo/pluginjam/web/server/Server.java new file mode 100644 index 0000000..8b2645b --- /dev/null +++ b/plugin-velocity/src/main/java/de/chojo/pluginjam/web/server/Server.java @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.pluginjam.web.server; + +import de.chojo.pluginjam.payload.Registration; +import de.chojo.pluginjam.servers.ServerRegistry; +import io.javalin.http.HttpCode; + +import static io.javalin.apibuilder.ApiBuilder.delete; +import static io.javalin.apibuilder.ApiBuilder.get; +import static io.javalin.apibuilder.ApiBuilder.patch; +import static io.javalin.apibuilder.ApiBuilder.path; +import static io.javalin.apibuilder.ApiBuilder.post; + +public class Server { + private final ServerRegistry registry; + + public Server(ServerRegistry registry) { + this.registry = registry; + } + + public void buildRoutes() { + path("server", () -> { + post("", ctx -> { + registry.register(ctx.bodyAsClass(Registration.class)); + ctx.status(HttpCode.ACCEPTED); + }); + + patch("", ctx -> { + registry.ping(ctx.bodyAsClass(Registration.class)); + ctx.status(HttpCode.ACCEPTED); + }); + + delete("", ctx -> { + var id = ctx.queryParam("id"); + var port = ctx.queryParam("port"); + var registration = new Registration(Integer.parseInt(id), "", Integer.parseInt(port), 0); + registry.unregister(registration); + ctx.status(HttpCode.ACCEPTED); + }); + get("", ctx -> { + ctx.json(registry.server()); + ctx.status(HttpCode.OK); + }); + }); + } +} diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts deleted file mode 100644 index 181cab2..0000000 --- a/plugin/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -plugins { - java -} - -group = "de.chojo" -version = "1.0" - -repositories { - maven("https://eldonexus.de/repository/maven-proxies/") -} - -dependencies { - compileOnly("io.papermc.paper:paper-api:1.18.2-R0.1-SNAPSHOT") -} - -tasks { - processResources { - from(sourceSets.main.get().resources.srcDirs) { - filesMatching("plugin.yml") { - expand( - "version" to project.version - ) - } - duplicatesStrategy = DuplicatesStrategy.INCLUDE - } - } -} diff --git a/plugin/src/main/java/de/chojo/gamejam/WelcomePlugin.java b/plugin/src/main/java/de/chojo/gamejam/WelcomePlugin.java deleted file mode 100644 index eb5cbdf..0000000 --- a/plugin/src/main/java/de/chojo/gamejam/WelcomePlugin.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-License-Identifier: AGPL-3.0-only - * - * Copyright (C) 2022 DevCord Team and Contributor - */ - -package de.chojo.gamejam; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.plugin.java.JavaPlugin; - -public class WelcomePlugin extends JavaPlugin implements Listener { - - private Component joinMessageComponent; - - @Override - public void onEnable() { - saveDefaultConfig(); - var message = getConfig().getString("message", "Welcome!"); - - getServer().getPluginManager().registerEvents(this, this); - joinMessageComponent = MiniMessage.miniMessage().deserialize(message); - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - event.getPlayer().sendMessage(joinMessageComponent); - } -} diff --git a/plugin/src/main/resources/config.yml b/plugin/src/main/resources/config.yml deleted file mode 100644 index d561a21..0000000 --- a/plugin/src/main/resources/config.yml +++ /dev/null @@ -1 +0,0 @@ -message: "" diff --git a/plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml deleted file mode 100644 index 3d4412c..0000000 --- a/plugin/src/main/resources/plugin.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: Devcord-Gamejam -author: Taucher2003 -version: ${version} -api-version: 1.18 -main: de.chojo.gamejam.WelcomePlugin diff --git a/settings.gradle.kts b/settings.gradle.kts index 92217a0..b4ed8db 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,7 @@ rootProject.name = "gamejam" include("bot") -include("api") -include("plugin") +include("plugin-api") +include("plugin-paper") +include("plugin-velocity") +include("plugin-paper:Readme.md") +findProject(":plugin-paper:Readme.md")?.name = "Readme.md"