diff --git a/.env.api.example b/.env.api.example new file mode 100644 index 00000000..7f270567 --- /dev/null +++ b/.env.api.example @@ -0,0 +1,3 @@ +DATABASE_URL=postgresql://postgres:password@localhost:5432/cafeBot?schema=public +KAWAII_API_TOKEN= +NODE_ENV=bot_testing diff --git a/.env.web.example b/.env.web.example new file mode 100644 index 00000000..988b4f12 --- /dev/null +++ b/.env.web.example @@ -0,0 +1,5 @@ +DATABASE_URL=postgresql://postgres:password@localhost:5432/cafeBot?schema=public +BETTER_AUTH_URL=http://localhost:3000 +BETTER_AUTH_SECRET=THISISANOTHERSECRETTHATMUSTBE32CHARACTERSLONGATLEASTFORSOMEREASON +BETTER_AUTH_DISCORD_ID= +BETTER_AUTH_DISCORD_SECRET= diff --git a/.gitignore b/.gitignore index 1abdf3d8..e7ad065b 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,8 @@ gradle.properties # Files containing safety info .env +.env.api +.env.web beta.json release.json diff --git a/README.md b/README.md index 8c29b553..91c60a6e 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,7 @@ Commands are separated by sections. Each section has a different set of commands * `/twitch` - Add or remove twitch channels to be notified for! * `/version` - Get the current bot version! * `/who` - Get some information about yourself or another user! +* `/calendar` - Add, view, and parse calendars. You can now view your calendars through Discord! #### 5. Interaction Commands diff --git a/build.gradle.kts b/build.gradle.kts index bae3fe10..c5704a87 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -69,6 +69,8 @@ allprojects { archiveVersion.set(project.version as String) println("Compiling: " + project.name + "-" + project.version + ".jar") } + + mergeServiceFiles() } tasks.test { @@ -105,6 +107,8 @@ dependencies { implementation("org.apache.logging.log4j:log4j-core:2.25.3") implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.25.3") // JDA logging. + implementation("org.mnode.ical4j:ical4j:4.2.3") // Calendar Stuff - https://mvnrepository.com/artifact/org.mnode.ical4j/ical4j + implementation("com.github.twitch4j:twitch4j:1.25.0") // Twitch - https://github.com/twitch4j/twitch4j compileOnly("org.projectlombok:lombok:1.18.42") @@ -118,7 +122,12 @@ tasks.withType { exclude(dependency("io.github.xanthic.cache:.*:.*")) exclude(dependency("com.github.twitch4j:.*:.*")) exclude(dependency("com.squareup.okhttp3:.*:.*")) + exclude(dependency("org.mnode.ical4j:.*:.*")) } + + relocate("org.mnode.ical4j", "com.beanbeanjuice.libs.org.mnode.ical4j") + + mergeServiceFiles() } configure("processResources") { diff --git a/docker-compose.dry-run.yml b/docker-compose.dry-run.yml new file mode 100644 index 00000000..5e66ce14 --- /dev/null +++ b/docker-compose.dry-run.yml @@ -0,0 +1,68 @@ +name: cafeBot + +services: + db: + image: postgres:18-alpine + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: cafeBot + ports: + - "5432:5432" + volumes: + - cafeBot_data:/var/lib/postgresql/18/docker + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d cafeBot"] + interval: 5s + timeout: 5s + retries: 5 + + api: + image: git.beanbeanjuice.dev/beanbeanjuice/cafebot-api:dev-latest + env_file: + - .env.api + depends_on: + db: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:5000/api/v4/hello" ] + interval: 10s + timeout: 5s + retries: 20 + start_period: 20s + environment: + DATABASE_URL: postgres://postgres:password@db:5432/cafeBot # Override env file. + NODE_ENV: bot_testing + + web: + image: git.beanbeanjuice.dev/beanbeanjuice/cafebot-web:dev-latest + env_file: + - .env.web + ports: + - "3000:3000" + depends_on: + api: + condition: service_healthy + restart: unless-stopped + environment: + DATABASE_URL: postgres://postgres:password@db:5432/cafeBot # Override env file. + + bot: + build: + context: . + env_file: + - .env + depends_on: + api: + condition: service_healthy + volumes: + - ./logs:/bot/logs + environment: + CAFEBOT_LOG_LEVEL: INFO + CAFEBOT_API_URL: http://api:5000 + restart: unless-stopped + +volumes: + cafeBot_data: diff --git a/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/CafeAPI.java b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/CafeAPI.java index f049b9a8..309907c8 100644 --- a/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/CafeAPI.java +++ b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/CafeAPI.java @@ -1,5 +1,6 @@ package com.beanbeanjuice.cafebot.api.wrapper; +import com.beanbeanjuice.cafebot.api.wrapper.api.discord.CalendarApi; import com.beanbeanjuice.cafebot.api.wrapper.api.discord.MenuApi; import com.beanbeanjuice.cafebot.api.wrapper.api.discord.generic.BotSettingsApi; import com.beanbeanjuice.cafebot.api.wrapper.api.discord.server.*; @@ -22,6 +23,7 @@ public class CafeAPI { // Discord APIs private final MenuApi menuApi; + private final CalendarApi calendarApi; // User APIs private final BirthdayApi birthdayApi; @@ -51,12 +53,15 @@ public class CafeAPI { * @see Kawaii API Documentation */ public CafeAPI(String baseUrl, String token) { + Runtime.getRuntime().addShutdownHook(new Thread(RequestBuilder::shutdown)); + // General APIs this.botSettingsApi = new BotSettingsApi(baseUrl, token); this.greetingApi = new GreetingApi(baseUrl, token); // Discord APIs this.menuApi = new MenuApi(baseUrl, token); + this.calendarApi = new CalendarApi(baseUrl, token); // User APIs this.birthdayApi = new BirthdayApi(baseUrl, token); diff --git a/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/RequestBuilder.java b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/RequestBuilder.java index 5239bcd9..faadca45 100644 --- a/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/RequestBuilder.java +++ b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/RequestBuilder.java @@ -23,6 +23,21 @@ public class RequestBuilder { private Method method; private Object body; + private static final CloseableHttpAsyncClient CLIENT; + + static { + CLIENT = HttpAsyncClients.custom().build(); + CLIENT.start(); + } + + public static void shutdown() { + try { + CLIENT.close(); + } catch (Exception e) { + // log if needed + } + } + public static RequestBuilder builder() { return new RequestBuilder(); } @@ -65,9 +80,6 @@ public CompletableFuture queue() throws URISyntaxException { .setPath(route) .build(); - CloseableHttpAsyncClient client = HttpAsyncClients.custom().build(); - client.start(); - JsonMapper mapper = new JsonMapper(); CompletableFuture future = new CompletableFuture<>(); @@ -77,12 +89,11 @@ public CompletableFuture queue() throws URISyntaxException { request.setBody(json, ContentType.APPLICATION_JSON); } catch (Exception e) { future.completeExceptionally(e); - closeClient(client); return future; } } - client.execute(request, new FutureCallback<>() { + CLIENT.execute(request, new FutureCallback<>() { @Override public void completed(SimpleHttpResponse response) { try { @@ -108,39 +119,27 @@ public void completed(SimpleHttpResponse response) { jsonNode.has("error") ? jsonNode.get("error").toPrettyString() : jsonNode.toPrettyString()) ) ); + return; } future.complete(new BasicResponse(statusCode, jsonNode)); } catch (Exception e) { future.completeExceptionally(e); - } finally { - closeClient(client); } } @Override public void failed(Exception ex) { future.completeExceptionally(ex); - closeClient(client); } @Override public void cancelled() { future.cancel(true); - closeClient(client); } }); return future; } - private void closeClient(CloseableHttpAsyncClient client) { - try { - client.close(); - } catch (Exception e) { - // Log or ignore - } - } - - } diff --git a/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/api/discord/CalendarApi.java b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/api/discord/CalendarApi.java new file mode 100644 index 00000000..7515f679 --- /dev/null +++ b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/api/discord/CalendarApi.java @@ -0,0 +1,129 @@ +package com.beanbeanjuice.cafebot.api.wrapper.api.discord; + +import com.beanbeanjuice.cafebot.api.wrapper.RequestBuilder; +import com.beanbeanjuice.cafebot.api.wrapper.api.Api; +import com.beanbeanjuice.cafebot.api.wrapper.api.enums.OwnerType; +import com.beanbeanjuice.cafebot.api.wrapper.response.BasicResponse; +import com.beanbeanjuice.cafebot.api.wrapper.type.calendar.Calendar; +import com.beanbeanjuice.cafebot.api.wrapper.type.calendar.PartialCalendar; +import org.apache.hc.core5.http.Method; +import tools.jackson.databind.JsonNode; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class CalendarApi extends Api { + + public CalendarApi(String baseUrl, String token) { + super(baseUrl, token); + } + + public CompletableFuture getCalendar(String calendarId) { + try { + return RequestBuilder.builder() + .method(Method.GET) + .baseUrl(baseUrl) + .token(token) + .route(String.format("/api/v4/discord/calendars/%s", calendarId)) + .queue() + .thenApply(BasicResponse::getBody) + .thenApply((node) -> node.get("calendar")) + .thenApply(this::parseCalendar); + } catch (Exception e) { + throw new RuntimeException("Invalid route: " + e.getMessage()); + } + } + + public CompletableFuture> getGuildCalendars(String guildId) { + try { + return RequestBuilder.builder() + .method(Method.GET) + .baseUrl(baseUrl) + .token(token) + .route(String.format("/api/v4/discord/calendars/guild/%s", guildId)) + .queue() + .thenApply(BasicResponse::getBody) + .thenApply((node) -> node.get("calendars")) + .thenApply(this::parseCalendars); + } catch (Exception e) { + throw new RuntimeException("Invalid route: " + e.getMessage()); + } + } + + public CompletableFuture> getUserCalendars(String userId) { + try { + return RequestBuilder.builder() + .method(Method.GET) + .baseUrl(baseUrl) + .token(token) + .route(String.format("/api/v4/discord/calendars/user/%s", userId)) + .queue() + .thenApply(BasicResponse::getBody) + .thenApply((node) -> node.get("calendars")) + .thenApply(this::parseCalendars); + } catch (Exception e) { + throw new RuntimeException("Invalid route: " + e.getMessage()); + } + } + + public CompletableFuture createCalendar(PartialCalendar calendar) { + Map body = new HashMap<>(); + + body.put("ownerId", calendar.getOwnerId()); + body.put("ownerType", calendar.getOwnerType().toString()); + body.put("name", calendar.getName()); + body.put("url", calendar.getUrl()); + + try { + return RequestBuilder.builder() + .method(Method.POST) + .baseUrl(baseUrl) + .token(token) + .route("/api/v4/discord/calendars") + .body(body) + .queue() + .thenApply(BasicResponse::getBody) + .thenApply((node) -> node.get("calendar")) + .thenApply(this::parseCalendar); + } catch (Exception e) { + throw new RuntimeException("Invalid route: " + e.getMessage()); + } + } + + public CompletableFuture deleteCalendar(String calendarId, String callerId) { + try { + return RequestBuilder.builder() + .method(Method.DELETE) + .baseUrl(baseUrl) + .token(token) + .route(String.format("/api/v4/discord/calendars/%s?callerId=%s", calendarId, callerId)) + .queue() + .thenApply((res) -> null); + } catch (Exception e) { + throw new RuntimeException("Invalid route: " + e.getMessage()); + } + } + + private Calendar parseCalendar(JsonNode calendarNode) { + String id = calendarNode.get("id").asString(); + OwnerType ownerType = OwnerType.valueOf(calendarNode.get("ownerType").asString()); + String ownerId = (ownerType == OwnerType.DISCORD_USER) ? calendarNode.get("discordUserId").asString() : calendarNode.get("guildId").asString(); + + String name = calendarNode.get("name").asString(); + String url = calendarNode.get("url").asString(); + + return new Calendar(id, ownerType, ownerId, name, url); + } + + private List parseCalendars(JsonNode calendarsNode) { + List calendars = new ArrayList<>(); + + for (JsonNode calendarNode : calendarsNode) calendars.add(parseCalendar(calendarNode)); + + return calendars; + } + +} diff --git a/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/api/enums/OwnerType.java b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/api/enums/OwnerType.java new file mode 100644 index 00000000..af40b748 --- /dev/null +++ b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/api/enums/OwnerType.java @@ -0,0 +1,9 @@ +package com.beanbeanjuice.cafebot.api.wrapper.api.enums; + +public enum OwnerType { + + USER, + DISCORD_USER, + GUILD + +} diff --git a/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/type/calendar/Calendar.java b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/type/calendar/Calendar.java new file mode 100644 index 00000000..7c27f0d1 --- /dev/null +++ b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/type/calendar/Calendar.java @@ -0,0 +1,17 @@ +package com.beanbeanjuice.cafebot.api.wrapper.type.calendar; + +import com.beanbeanjuice.cafebot.api.wrapper.api.enums.OwnerType; +import lombok.Getter; + +@Getter +public class Calendar extends PartialCalendar { + + private final String id; + + public Calendar(String id, OwnerType ownerType, String ownerId, String name, String url) { + super(ownerType, ownerId, name, url); + + this.id = id; + } + +} diff --git a/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/type/calendar/PartialCalendar.java b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/type/calendar/PartialCalendar.java new file mode 100644 index 00000000..c0a0a939 --- /dev/null +++ b/modules/cafeBot-api-wrapper/src/main/java/com/beanbeanjuice/cafebot/api/wrapper/type/calendar/PartialCalendar.java @@ -0,0 +1,17 @@ +package com.beanbeanjuice.cafebot.api.wrapper.type.calendar; + +import com.beanbeanjuice.cafebot.api.wrapper.api.enums.OwnerType; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class PartialCalendar { + + private final OwnerType ownerType; + private final String ownerId; + + private final String name; + private final String url; + +} diff --git a/modules/cafeBot-api-wrapper/src/test/java/com/beanbeanjuice/cafebot/endpoints/discord/CalendarApiTest.java b/modules/cafeBot-api-wrapper/src/test/java/com/beanbeanjuice/cafebot/endpoints/discord/CalendarApiTest.java new file mode 100644 index 00000000..ce93283f --- /dev/null +++ b/modules/cafeBot-api-wrapper/src/test/java/com/beanbeanjuice/cafebot/endpoints/discord/CalendarApiTest.java @@ -0,0 +1,113 @@ +package com.beanbeanjuice.cafebot.endpoints.discord; + +import com.beanbeanjuice.cafebot.ApiTest; +import com.beanbeanjuice.cafebot.api.wrapper.api.enums.OwnerType; +import com.beanbeanjuice.cafebot.api.wrapper.type.calendar.Calendar; +import com.beanbeanjuice.cafebot.api.wrapper.type.calendar.PartialCalendar; +import org.junit.jupiter.api.*; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class CalendarApiTest extends ApiTest { + + private String userId; + private String guildId; + private Calendar userCalendar; + private Calendar guildCalendar; + + @BeforeEach + public void setup() throws ExecutionException, InterruptedException { + userId = generateSnowflake().toString(); + guildId = generateSnowflake().toString(); + + PartialCalendar partialUserCalendar = new PartialCalendar(OwnerType.DISCORD_USER, userId, "Test User Calendar", "https://user.example.ics"); + PartialCalendar partialGuildCalendar = new PartialCalendar(OwnerType.GUILD, guildId, "Test Guild Calendar", "https://guild.example.ics"); + + userCalendar = cafeAPI.getCalendarApi().createCalendar(partialUserCalendar).get(); + guildCalendar = cafeAPI.getCalendarApi().createCalendar(partialGuildCalendar).get(); + } + + @Test + @DisplayName("can create user calendar") + public void canCreateUserCalendar() throws ExecutionException, InterruptedException { + String tempUserId = generateSnowflake().toString(); + PartialCalendar partialCalendar = new PartialCalendar(OwnerType.DISCORD_USER, tempUserId, "Example User Calendar", "https://some.calendar.ics"); + + Calendar calendar = cafeAPI.getCalendarApi().createCalendar(partialCalendar).get(); + + Assertions.assertEquals("Example User Calendar", calendar.getName()); + Assertions.assertEquals("https://some.calendar.ics", calendar.getUrl()); + Assertions.assertEquals(OwnerType.DISCORD_USER, calendar.getOwnerType()); + Assertions.assertEquals(tempUserId, calendar.getOwnerId()); + } + + @Test + @DisplayName("can create guild calendar") + public void canCreateGuildCalendar() throws ExecutionException, InterruptedException { + String tempGuildId = generateSnowflake().toString(); + PartialCalendar partialCalendar = new PartialCalendar(OwnerType.GUILD, tempGuildId, "Example Guild Calendar", "https://another.calendar.ics"); + + Calendar calendar = cafeAPI.getCalendarApi().createCalendar(partialCalendar).get(); + + Assertions.assertEquals("Example Guild Calendar", calendar.getName()); + Assertions.assertEquals("https://another.calendar.ics", calendar.getUrl()); + Assertions.assertEquals(OwnerType.GUILD, calendar.getOwnerType()); + Assertions.assertEquals(tempGuildId, calendar.getOwnerId()); + } + + @Test + @DisplayName("can get specific user calendar") + public void canGetSpecificUserCalendar() throws ExecutionException, InterruptedException { + Calendar calendar = cafeAPI.getCalendarApi().getCalendar(userCalendar.getId()).get(); + + Assertions.assertEquals(calendar.getId(), userCalendar.getId()); + Assertions.assertEquals(calendar.getOwnerType(), userCalendar.getOwnerType()); + Assertions.assertEquals(calendar.getOwnerId(), userCalendar.getOwnerId()); + Assertions.assertEquals(calendar.getName(), userCalendar.getName()); + Assertions.assertEquals(calendar.getUrl(), userCalendar.getUrl()); + } + + @Test + @DisplayName("can get specific guild calendar") + public void canGetSpecificGuildCalendar() throws ExecutionException, InterruptedException { + Calendar calendar = cafeAPI.getCalendarApi().getCalendar(guildCalendar.getId()).get(); + + Assertions.assertEquals(calendar.getId(), guildCalendar.getId()); + Assertions.assertEquals(calendar.getOwnerType(), guildCalendar.getOwnerType()); + Assertions.assertEquals(calendar.getOwnerId(), guildCalendar.getOwnerId()); + Assertions.assertEquals(calendar.getName(), guildCalendar.getName()); + Assertions.assertEquals(calendar.getUrl(), guildCalendar.getUrl()); + } + + @Test + @DisplayName("can get multiple user calendars") + public void canGetMultipleUserCalendars() throws ExecutionException, InterruptedException { + List calendars = cafeAPI.getCalendarApi().getUserCalendars(userId).get(); + + Assertions.assertEquals(1, calendars.size()); + Assertions.assertEquals("Test User Calendar", calendars.get(0).getName()); + Assertions.assertEquals("https://user.example.ics", calendars.get(0).getUrl()); + } + + @Test + @DisplayName("can get multiple guild calendars") + public void canGetMultipleGuildCalendars() throws ExecutionException, InterruptedException { + List calendars = cafeAPI.getCalendarApi().getGuildCalendars(guildId).get(); + + Assertions.assertEquals(1, calendars.size()); + Assertions.assertEquals("Test Guild Calendar", calendars.get(0).getName()); + Assertions.assertEquals("https://guild.example.ics", calendars.get(0).getUrl()); + } + + @Test + @DisplayName("can delete calendar") + public void canDeleteCalendar() throws ExecutionException, InterruptedException { + cafeAPI.getCalendarApi().deleteCalendar(userCalendar.getId(), userCalendar.getOwnerId()).get(); + + Assertions.assertThrows(ExecutionException.class, () -> { + cafeAPI.getCalendarApi().getCalendar(userCalendar.getId()).get(); + }); + } + +} diff --git a/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java b/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java index 9e1c42ff..e1119d06 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java +++ b/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java @@ -16,6 +16,7 @@ import com.beanbeanjuice.cafebot.commands.games.TicTacToeCommand; import com.beanbeanjuice.cafebot.commands.games.game.GameCommand; import com.beanbeanjuice.cafebot.commands.generic.*; +import com.beanbeanjuice.cafebot.commands.generic.calendar.CalendarCommand; import com.beanbeanjuice.cafebot.commands.generic.twitch.TwitchCommand; import com.beanbeanjuice.cafebot.commands.interaction.*; import com.beanbeanjuice.cafebot.commands.interaction.generic.InteractionCommand; @@ -162,6 +163,7 @@ private void setupCommands() { new SupportCommand(this), new WhoCommand(this), new TwitchCommand(this), + new CalendarCommand(this), // Cafe new BalanceCommand(this), diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdaySetSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdaySetSubCommand.java index bea67879..edaad339 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdaySetSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdaySetSubCommand.java @@ -94,8 +94,8 @@ public OptionData[] getOptions() { } @Override - public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { - HashMap> autoCompleteMap = new HashMap<>(); + public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { + HashMap> autoCompleteMap = new HashMap<>(); autoCompleteMap.put("timezone", new ArrayList<>()); for (String timezone : TimeZone.getAvailableIDs()) diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarAddSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarAddSubCommand.java new file mode 100644 index 00000000..89bb2f65 --- /dev/null +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarAddSubCommand.java @@ -0,0 +1,106 @@ +package com.beanbeanjuice.cafebot.commands.generic.calendar; + +import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.api.wrapper.api.enums.OwnerType; +import com.beanbeanjuice.cafebot.api.wrapper.api.exception.ApiRequestException; +import com.beanbeanjuice.cafebot.api.wrapper.type.calendar.PartialCalendar; +import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; +import com.beanbeanjuice.cafebot.utility.handlers.calendar.CalendarHandler; +import com.beanbeanjuice.cafebot.utility.helper.Helper; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import tools.jackson.databind.JsonNode; + +import java.util.concurrent.CompletionException; + +public class CalendarAddSubCommand extends Command implements ISubCommand { + + public CalendarAddSubCommand(CafeBot bot) { + super(bot); + } + + @Override + public void handle(SlashCommandInteractionEvent event) { + OwnerType type = OwnerType.valueOf(event.getOption("type").getAsString()); + String name = event.getOption("name").getAsString(); + String url = event.getOption("url").getAsString(); + + if (type == OwnerType.GUILD && !event.isFromGuild()) { + event.getHook().sendMessageEmbeds(Helper.errorEmbed( + "Invalid Command!", + "In order to set a server calendar, you need to use this command in a Discord server!" + )).queue(); + return; + } + + if (type == OwnerType.GUILD && event.isFromGuild() && event.getMember() != null && !event.getMember().hasPermission(Permission.MANAGE_SERVER)) { + event.getHook().sendMessageEmbeds(Helper.errorEmbed("No Permission", "What are you doing back here?? Get **out**!")).queue(); + return; + } + + String ownerId = (type == OwnerType.GUILD) ? event.getGuild().getId() : event.getUser().getId(); + PartialCalendar partialCalendar = new PartialCalendar(type, ownerId, name, url); + + bot.getCafeAPI().getCalendarApi().createCalendar(partialCalendar).thenAccept((calendar) -> { + event.getHook().sendMessageEmbeds(Helper.successEmbed( + "Calendar Added!", + "Successfully added your calendar! Use `/calender get` to preview your calendar!" + )).queue(); + }).exceptionally((ex) -> { + handleError(ex, event); + throw new CompletionException(ex.getCause()); + }); + } + + private void handleError(Throwable ex, SlashCommandInteractionEvent event) { + if (ex.getCause() instanceof ApiRequestException apiRequestException) { + JsonNode errorBody = apiRequestException.getBody().get("error"); + + if (errorBody.has("calendars")) { + event.getHook().sendMessageEmbeds(Helper.errorEmbed( + "Too Many Calendars!", + "You have added too many calendars... You can only have 3 active ones!" + )).queue(); + return; + } + + if (errorBody.has("url")) { + event.getHook().sendMessageEmbeds(Helper.errorEmbed( + "Invalid URL", + "You must use a *valid* calendar URL!" + )).queue(); + return; + } + } + + event.getHook().sendMessageEmbeds(Helper.errorEmbed( + "Error Adding Calendar", + "I'm sorry... I don't know *what* went wrong..." + )).queue(); + } + + @Override + public String getName() { + return "add"; + } + + @Override + public String getDescription() { + return "Add a calendar!"; + } + + @Override + public OptionData[] getOptions() { + return new OptionData[] { + new OptionData(OptionType.STRING, "type", "The type of calendar you want to add.", true) + .addChoice("User (The calendar belongs to you)", "DISCORD_USER") + .addChoice("Server (The calendar belongs to the server)", "GUILD"), + new OptionData(OptionType.STRING, "name", "The calendar name!", true), + new OptionData(OptionType.STRING, "url", "The calendar url! Make sure it ends in \".ics\"!", true), + }; + } + +} diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarCommand.java new file mode 100644 index 00000000..36847e4e --- /dev/null +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarCommand.java @@ -0,0 +1,61 @@ +package com.beanbeanjuice.cafebot.commands.generic.calendar; + +import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.ICommand; +import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; +import net.dv8tion.jda.api.Permission; + +public class CalendarCommand extends Command implements ICommand { + + public CalendarCommand(CafeBot bot) { + super(bot); + } + + @Override + public String getName() { + return "calendar"; + } + + @Override + public String getDescription() { + return "All things to do with calendars!"; + } + + @Override + public CommandCategory getCategory() { + return CommandCategory.GENERIC; + } + + @Override + public Permission[] getPermissions() { + return new Permission[0]; + } + + @Override + public boolean isEphemeral() { + return true; + } + + @Override + public boolean isNSFW() { + return false; + } + + @Override + public boolean allowDM() { + return true; + } + + @Override + public ISubCommand[] getSubCommands() { + return new ISubCommand[] { + new CalendarGetSubCommand(bot), + new CalendarListSubCommand(bot), + new CalendarAddSubCommand(bot), + new CalendarDeleteSubCommand(bot) + }; + } + +} diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarDeleteSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarDeleteSubCommand.java new file mode 100644 index 00000000..e3cc5805 --- /dev/null +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarDeleteSubCommand.java @@ -0,0 +1,97 @@ +package com.beanbeanjuice.cafebot.commands.generic.calendar; + +import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.api.wrapper.api.enums.OwnerType; +import com.beanbeanjuice.cafebot.api.wrapper.api.exception.ApiRequestException; +import com.beanbeanjuice.cafebot.api.wrapper.type.calendar.Calendar; +import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; +import com.beanbeanjuice.cafebot.utility.helper.Helper; +import net.dv8tion.jda.api.Permission; +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.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import tools.jackson.databind.JsonNode; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class CalendarDeleteSubCommand extends Command implements ISubCommand { + + public CalendarDeleteSubCommand(CafeBot bot) { + super(bot); + } + + @Override + public void handle(SlashCommandInteractionEvent event) { + String calendarId = event.getOption("id").getAsString().split("ID: ")[1]; + + bot.getCafeAPI().getCalendarApi().getCalendar(calendarId).thenAccept(calendar -> { + if (calendar.getOwnerType() == OwnerType.GUILD && event.isFromGuild() && event.getMember() != null && !event.getMember().hasPermission(Permission.MANAGE_SERVER)) { + event.getHook().sendMessageEmbeds(Helper.errorEmbed("No Permission", "What are you doing back here?? Get **out**!")).queue(); + return; + } + + String callerId = (calendar.getOwnerType() == OwnerType.GUILD && event.isFromGuild()) ? event.getGuild().getId() : event.getUser().getId(); + + bot.getCafeAPI().getCalendarApi().deleteCalendar(calendarId, callerId).thenAccept(result -> { + event.getHook().sendMessageEmbeds(Helper.successEmbed("Calendar Deleted!", "We won't see that pesky calendar any longer!")).queue(); + }).exceptionally((ex) -> { + handleError(ex, event); + throw new CompletionException(ex.getCause()); + }); + }).exceptionally((ex) -> { + handleError(ex, event); + throw new CompletionException(ex.getCause()); + }); + } + + private void handleError(Throwable ex, SlashCommandInteractionEvent event) { + if (ex.getCause() instanceof ApiRequestException apiRequestException) { + JsonNode errorNode = apiRequestException.getBody().get("error"); + + if (errorNode.has("callerId")) { + event.getHook().sendMessageEmbeds(Helper.errorEmbed("You're joking right?", "Oh well.. I thought you were smarter than that... since you tried to use an exploit I made sure to talk to my boss! <:cafeBot_angry:1171726164092518441>")).queue(); + return; + } + } + + event.getHook().sendMessageEmbeds(Helper.errorEmbed("Error", "Could not delete the calendar!")).queue(); + } + + @Override + public String getName() { + return "delete"; + } + + @Override + public String getDescription() { + return "Delete a calendar!"; + } + + @Override + public OptionData[] getOptions() { + return new OptionData[] { + new OptionData(OptionType.STRING, "id", "The ID of the calendar you want to delete!", true, true), + }; + } + + @Override + public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { + HashMap> autoCompleteMap = new HashMap<>(); + + var userCalendarFutures = bot.getCafeAPI().getCalendarApi().getUserCalendars(event.getUser().getId()); + var guildCalenderFutures = (!event.isFromGuild()) ? CompletableFuture.completedFuture(new ArrayList()) : bot.getCafeAPI().getCalendarApi().getGuildCalendars(event.getGuild().getId()); + + return userCalendarFutures.thenCombine(guildCalenderFutures, (userCalendars, guildCalendars) -> { + userCalendars.addAll(guildCalendars); + autoCompleteMap.put("id", userCalendars.stream().map((calendar) -> String.format("%s - ID: %s", calendar.getName(), calendar.getId())).toList()); + return autoCompleteMap; + }); + } + +} diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarGetSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarGetSubCommand.java new file mode 100644 index 00000000..5e70ed8c --- /dev/null +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarGetSubCommand.java @@ -0,0 +1,82 @@ +package com.beanbeanjuice.cafebot.commands.generic.calendar; + +import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.api.wrapper.type.calendar.Calendar; +import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; +import com.beanbeanjuice.cafebot.utility.handlers.calendar.CalendarHandler; +import com.beanbeanjuice.cafebot.utility.helper.Helper; +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.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.TimeZone; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class CalendarGetSubCommand extends Command implements ISubCommand { + + public CalendarGetSubCommand(CafeBot bot) { + super(bot); + } + + @Override + public void handle(SlashCommandInteractionEvent event) { + String[] split = event.getOption("id").getAsString().split("ID: "); + String calendarId = (split.length == 2) ? split[1] : split[0]; + ZoneId zoneId = TimeZone.getTimeZone(event.getOption("timezone").getAsString()).toZoneId(); + + bot.getCafeAPI().getCalendarApi().getCalendar(calendarId).thenAccept(calendar -> { + String message = CalendarHandler.getCalendarMessage(calendar.getUrl(), zoneId); + event.getHook().sendMessage(message).queue(); + }).exceptionally((ex) -> { + handleError(ex, event); + throw new CompletionException(ex.getCause()); + }); + } + + private void handleError(Throwable ex, SlashCommandInteractionEvent event) { + event.getHook().sendMessageEmbeds(Helper.errorEmbed("Error Getting Calendar", "I... couldn't find the calendar... is this an error??")).queue(); + } + + @Override + public String getName() { + return "get"; + } + + @Override + public String getDescription() { + return "Get your calendars!"; + } + + @Override + public OptionData[] getOptions() { + return new OptionData[] { + new OptionData(OptionType.STRING, "id", "The ID of the calendar you want to view!", true, true), + new OptionData(OptionType.STRING, "timezone", "The timezone you want the calendar in", true, true) + }; + } + + @Override + public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { + HashMap> autoCompleteMap = new HashMap<>(); + autoCompleteMap.put("timezone", new ArrayList<>()); + + for (String timezone : TimeZone.getAvailableIDs()) autoCompleteMap.get("timezone").add(timezone); + + var userCalendarFutures = bot.getCafeAPI().getCalendarApi().getUserCalendars(event.getUser().getId()); + var guildCalenderFutures = (!event.isFromGuild()) ? CompletableFuture.completedFuture(new ArrayList()) : bot.getCafeAPI().getCalendarApi().getGuildCalendars(event.getGuild().getId()); + + return userCalendarFutures.thenCombine(guildCalenderFutures, (userCalendars, guildCalendars) -> { + userCalendars.addAll(guildCalendars); + autoCompleteMap.put("id", userCalendars.stream().map((calendar) -> String.format("%s - ID: %s", calendar.getName(), calendar.getId())).toList()); + return autoCompleteMap; + }); + } + +} diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarListSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarListSubCommand.java new file mode 100644 index 00000000..083d5767 --- /dev/null +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/calendar/CalendarListSubCommand.java @@ -0,0 +1,62 @@ +package com.beanbeanjuice.cafebot.commands.generic.calendar; + +import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; +import com.beanbeanjuice.cafebot.utility.helper.Helper; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.Optional; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; + +public class CalendarListSubCommand extends Command implements ISubCommand { + + public CalendarListSubCommand(CafeBot bot) { + super(bot); + } + + @Override + public void handle(SlashCommandInteractionEvent event) { + User user = Optional.ofNullable(event.getOption("user")).map(OptionMapping::getAsUser).orElse(event.getUser()); + + bot.getCafeAPI().getCalendarApi().getUserCalendars(user.getId()).thenAccept(calendars -> { + EmbedBuilder eb = new EmbedBuilder(); + eb.setTitle("Calendar List"); + eb.setFooter("You can view guild calendars by typing /calendar get!"); + eb.setColor(Helper.getRandomColor()); + eb.setDescription(calendars.stream().map((calendar) -> String.format("**%s** (ID: %s)", calendar.getName(), calendar.getId())).collect(Collectors.joining("\n"))); + + if (calendars.isEmpty()) eb.appendDescription("No calendars found!"); + + event.getHook().sendMessageEmbeds(eb.build()).queue(); + }).exceptionally((ex) -> { + event.getHook().sendMessageEmbeds(Helper.errorEmbed("Error Getting Calendars", "I.. don't know what went wrong...")).queue(); + throw new CompletionException(ex.getCause()); + }); + } + + @Override + public String getName() { + return "list"; + } + + @Override + public String getDescription() { + return "List all of the calendars for a specific user!"; + } + + @Override + public OptionData[] getOptions() { + return new OptionData[] { + new OptionData(OptionType.USER, "user", "The user you want to see the calendar of!", false) + }; + } + +} diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/interaction/generic/InteractionUnblockSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/interaction/generic/InteractionUnblockSubCommand.java index 38082515..73c48cba 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/interaction/generic/InteractionUnblockSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/interaction/generic/InteractionUnblockSubCommand.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -70,9 +71,9 @@ public OptionData[] getOptions() { } @Override - public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { + public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { return bot.getCafeAPI().getInteractionsApi().getBlockedUsers(event.getUser().getId()).thenApply((blockList) -> { - HashMap> map = new HashMap<>(); + HashMap> map = new HashMap<>(); map.put("user-id", new ArrayList<>()); for (String userId : blockList) map.get("user-id").add(userId); diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/settings/polls/PollDeleteSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/settings/polls/PollDeleteSubCommand.java index 898ccd67..ed67925f 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/settings/polls/PollDeleteSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/settings/polls/PollDeleteSubCommand.java @@ -51,9 +51,9 @@ public OptionData[] getOptions() { } @Override - public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { + public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { return bot.getCafeAPI().getPollApi().getPolls(event.getGuild().getId(), true, false).thenApply((polls) -> { - HashMap> autoCompleteMap = new HashMap<>(); + HashMap> autoCompleteMap = new HashMap<>(); List ids = polls.stream().map(Poll::getId).map(Object::toString).toList(); diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/settings/raffles/RaffleDeleteSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/settings/raffles/RaffleDeleteSubCommand.java index f0e29d42..15195321 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/settings/raffles/RaffleDeleteSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/settings/raffles/RaffleDeleteSubCommand.java @@ -51,9 +51,9 @@ public OptionData[] getOptions() { } @Override - public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { + public CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { return bot.getCafeAPI().getRaffleApi().getRaffles(event.getGuild().getId(), true, false).thenApply((raffles) -> { - HashMap> autoCompleteMap = new HashMap<>(); + HashMap> autoCompleteMap = new HashMap<>(); List ids = raffles.stream().map(Raffle::getId).map(Object::toString).toList(); diff --git a/src/main/java/com/beanbeanjuice/cafebot/utility/commands/CommandHandler.java b/src/main/java/com/beanbeanjuice/cafebot/utility/commands/CommandHandler.java index 5c64799f..ebbb75b4 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/utility/commands/CommandHandler.java +++ b/src/main/java/com/beanbeanjuice/cafebot/utility/commands/CommandHandler.java @@ -225,7 +225,7 @@ private void handleAutoComplete(final List options, final CommandAutoCom event.replyChoices(options).queue(); } - private List getOptions(final ArrayList autoCompleteOptions, final String focusedOptionValue) { + private List getOptions(final List autoCompleteOptions, final String focusedOptionValue) { return autoCompleteOptions .stream() .filter((choiceString) -> choiceString.toUpperCase().contains(focusedOptionValue.toUpperCase())) diff --git a/src/main/java/com/beanbeanjuice/cafebot/utility/commands/ISubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/utility/commands/ISubCommand.java index 8fe9c396..e06edc25 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/utility/commands/ISubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/utility/commands/ISubCommand.java @@ -4,8 +4,8 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.concurrent.CompletableFuture; public interface ISubCommand { @@ -18,7 +18,7 @@ public interface ISubCommand { default OptionData[] getOptions() { return new OptionData[0]; } - default CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { return null; } + default CompletableFuture>> getAutoComplete(CommandAutoCompleteInteractionEvent event) { return null; } default boolean isModal() { return false; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/utility/handlers/calendar/CalendarEvent.java b/src/main/java/com/beanbeanjuice/cafebot/utility/handlers/calendar/CalendarEvent.java new file mode 100644 index 00000000..a33e76ba --- /dev/null +++ b/src/main/java/com/beanbeanjuice/cafebot/utility/handlers/calendar/CalendarEvent.java @@ -0,0 +1,22 @@ +package com.beanbeanjuice.cafebot.utility.handlers.calendar; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.Instant; +import java.util.Optional; + +@Getter +@RequiredArgsConstructor +public class CalendarEvent { + + private final String name; + private final String description; + private final Instant start; + private final Instant end; + + public Optional getDescription() { + return Optional.ofNullable(description); + } + +} diff --git a/src/main/java/com/beanbeanjuice/cafebot/utility/handlers/calendar/CalendarHandler.java b/src/main/java/com/beanbeanjuice/cafebot/utility/handlers/calendar/CalendarHandler.java new file mode 100644 index 00000000..12fe464e --- /dev/null +++ b/src/main/java/com/beanbeanjuice/cafebot/utility/handlers/calendar/CalendarHandler.java @@ -0,0 +1,103 @@ +package com.beanbeanjuice.cafebot.utility.handlers.calendar; + +import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.data.ParserException; +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.Component; +import net.fortuna.ical4j.model.Period; +import net.fortuna.ical4j.model.component.CalendarComponent; +import net.fortuna.ical4j.model.component.VEvent; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +public class CalendarHandler { + + private static List getEvents(String calendarUrl) throws MalformedURLException { + URL url = new URL(calendarUrl); + try (InputStream in = url.openStream()) { + CalendarBuilder builder = new CalendarBuilder(); + Calendar calendar = builder.build(in); + + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime nextWeek = now.plusWeeks(1); + + Period period = new Period<>(now, nextWeek); + + List events = new ArrayList<>(); + + for (CalendarComponent component : calendar.getComponents(Component.VEVENT)) { + VEvent event = (VEvent) component; + Set> occurrences = + event.calculateRecurrenceSet(period); + + + for (Period occurrence : occurrences) { + + String summary = (event.getSummary() != null) ? event.getSummary().getValue() : null; + String description = (event.getDescription() != null) ? event.getDescription().getValue() : null; + + events.add(new CalendarEvent(summary, description, occurrence.getStart().toInstant(), occurrence.getEnd().toInstant())); + } + } + + return events.stream().sorted(Comparator.comparing(CalendarEvent::getStart)).toList(); + } catch (ParserException | IOException e) { + throw new RuntimeException(e); + } + } + + public static String getCalendarMessage(String calendarUrl, ZoneId zoneId) { + StringBuilder sb = new StringBuilder(); + + List events; + + try { + events = getEvents(calendarUrl); + } catch (MalformedURLException ex) { + return "Error Getting Calendar"; + } + + // Group events by local date + Map> eventsByDay = + events.stream() + .collect(Collectors.groupingBy( + event -> event.getStart() + .atZone(zoneId) + .format(DateTimeFormatter.ofPattern("EEEE (MMMM d, yyyy)")), + LinkedHashMap::new, + Collectors.toList() + )); + + sb.append("# Calendar (Next 7 Days)\n"); + + for (Map.Entry> entry : eventsByDay.entrySet()) { + String dayName = entry.getKey(); + List dayEvents = entry.getValue(); + + sb.append("## ").append(dayName).append("\n\n"); + + for (CalendarEvent event : dayEvents) { + sb.append("* **").append(event.getName()).append("** - ") + .append("") + .append(" -> ") + .append("") + .append("\n"); + + event.getDescription().ifPresent(desc -> + sb.append("*").append(desc).append("*") + ); + } + } + + return sb.toString(); + } + +}