From 752853be3a3079491959f7af1243171f93ed765c Mon Sep 17 00:00:00 2001 From: yaansz Date: Sat, 28 Oct 2023 13:57:14 -0300 Subject: [PATCH 1/9] feat: adding calendar interface --- .../softawii/capivara/core/DroneManager.java | 1 + .../capivara/listeners/CalendarGroup.java | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/main/java/com/softawii/capivara/listeners/CalendarGroup.java diff --git a/src/main/java/com/softawii/capivara/core/DroneManager.java b/src/main/java/com/softawii/capivara/core/DroneManager.java index b1db2b7..d88af94 100644 --- a/src/main/java/com/softawii/capivara/core/DroneManager.java +++ b/src/main/java/com/softawii/capivara/core/DroneManager.java @@ -39,6 +39,7 @@ import java.awt.*; import java.util.ArrayList; import java.util.Collection; +import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java new file mode 100644 index 0000000..278d29b --- /dev/null +++ b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java @@ -0,0 +1,41 @@ +package com.softawii.capivara.listeners; + +import com.softawii.curupira.annotations.IArgument; +import com.softawii.curupira.annotations.ICommand; +import com.softawii.curupira.annotations.IGroup; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; + +@IGroup(name = "Calendar", description = "Calendar events", hidden = false) +public class CalendarGroup { + + @ICommand(name = "Subscribe", description = "Subscribe to a Google Calendar", permissions = {Permission.ADMINISTRATOR}) + @IArgument(name = "Calendar ID", description = "The ID of the calendar to subscribe to", required = true) + @IArgument(name = "Name", description = "The name to use for the calendar", required = true) + @IArgument(name = "Channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) + @IArgument(name = "Role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) + public static void subscribe(SlashCommandInteractionEvent event) { + event.reply("Not implemented yet").queue(); + } + + @ICommand(name = "Unsubscribe", description = "Unsubscribe from a Google Calendar", permissions = {Permission.ADMINISTRATOR}) + @IArgument(name = "Name", description = "The name to use for the calendar", required = true) + public static void unsubscribe(SlashCommandInteractionEvent event) { + event.reply("Not implemented yet").queue(); + } + + @ICommand(name = "list", description = "List all events from a Google Calendar") + @IArgument(name = "Name", description = "The name to use for the calendar", required = false) + public static void list(SlashCommandInteractionEvent event) { + event.reply("Not implemented yet").queue(); + } + + @ICommand(name = "Update", description = "Update configuration of a calendar", permissions = {Permission.ADMINISTRATOR}) + @IArgument(name = "Name", description = "The name to use for the calendar", required = true) + @IArgument(name = "Channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) + @IArgument(name = "Role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) + public static void update(SlashCommandInteractionEvent event) { + event.reply("Not implemented yet").queue(); + } +} From b61c0e6d9ebcdd14813e75fed1dec6fdb361fd98 Mon Sep 17 00:00:00 2001 From: yaansz Date: Sat, 28 Oct 2023 16:04:18 -0300 Subject: [PATCH 2/9] feat: starting with google api interface --- build.gradle | 4 + .../capivara/core/CalendarManager.java | 35 ++++++ .../softawii/capivara/entity/Calendar.java | 103 ++++++++++++++++++ .../messages/CalendarSubscriptionMessage.java | 9 ++ .../CalendarSubscriptionMessageType.java | 6 + .../capivara/listeners/CalendarGroup.java | 86 ++++++++------- .../capivara/services/CalendarService.java | 51 +++++++++ .../threads/CalendarSubscriptionThread.java | 100 +++++++++++++++++ src/main/resources/application.properties | 3 +- 9 files changed, 355 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/softawii/capivara/core/CalendarManager.java create mode 100644 src/main/java/com/softawii/capivara/entity/Calendar.java create mode 100644 src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessage.java create mode 100644 src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessageType.java create mode 100644 src/main/java/com/softawii/capivara/services/CalendarService.java create mode 100644 src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java diff --git a/build.gradle b/build.gradle index 22306e8..288798e 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,10 @@ dependencies { implementation("com.github.Softawii:curupira:v0.3.0") implementation("com.github.Softawii:curupira:v0.3.0:all") + + implementation 'com.google.api-client:google-api-client:2.0.0' + implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1' + implementation 'com.google.apis:google-api-services-calendar:v3-rev20220715-2.0.0' } tasks.register('deploy') { diff --git a/src/main/java/com/softawii/capivara/core/CalendarManager.java b/src/main/java/com/softawii/capivara/core/CalendarManager.java new file mode 100644 index 0000000..881ee4b --- /dev/null +++ b/src/main/java/com/softawii/capivara/core/CalendarManager.java @@ -0,0 +1,35 @@ +package com.softawii.capivara.core; + +import com.softawii.capivara.entity.Calendar; +import com.softawii.capivara.services.CalendarService; +import com.softawii.capivara.threads.CalendarSubscriptionThread; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Component; + +@Component +public class CalendarManager { + + private final Logger LOGGER = LogManager.getLogger(CalendarManager.class); + private final JDA jda; + private final CalendarService service; + private final CalendarSubscriptionThread subscriber; + + public CalendarManager(JDA jda, CalendarService service, CalendarSubscriptionThread subscriber) { + this.jda = jda; + this.service = service; + this.subscriber = subscriber; + } + + public void createCalendar(String googleCalendarId, String name, GuildChannelUnion channel, Role role) { + Long guildId = channel.getGuild().getIdLong(); + Long channelId = channel.getIdLong(); + Long roleId = role != null ? role.getIdLong() : null; + Calendar calendar = new Calendar(guildId, googleCalendarId, name, channelId, roleId); + + service.create(calendar); + } +} diff --git a/src/main/java/com/softawii/capivara/entity/Calendar.java b/src/main/java/com/softawii/capivara/entity/Calendar.java new file mode 100644 index 0000000..3fc334a --- /dev/null +++ b/src/main/java/com/softawii/capivara/entity/Calendar.java @@ -0,0 +1,103 @@ +package com.softawii.capivara.entity; + +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import java.io.Serializable; + +@Entity +public class Calendar { + + @EmbeddedId + private CalendarKey calendarKey; + + @Embeddable + public static class CalendarKey implements Serializable { + + @Column + private Long guildId; + + @Column + private String calendarName; + + public CalendarKey() { + } + + public CalendarKey(Long guildId, String calendarName) { + this.guildId = guildId; + this.calendarName = calendarName; + } + + public Long getGuildId() { + return guildId; + } + + public void setGuildId(Long guildId) { + this.guildId = guildId; + } + + public String getCalendarName() { + return calendarName; + } + + public void setCalendarName(String calendarName) { + this.calendarName = calendarName; + } + + } + + public Calendar(Long guildId, String googleCalendarId, String name, Long channelId, Long roleId) { + this.calendarKey = new CalendarKey(guildId, name); + this.googleCalendarId = googleCalendarId; + this.channelId = channelId; + this.roleId = roleId; + } + + @Column + private String googleCalendarId; + + @Column + private Long channelId; + + @Column + private Long roleId; + + public Calendar() { + } + + public CalendarKey getCalendarKey() { + return this.calendarKey; + } + + public void setCalendarKey(CalendarKey calendarKey) { + this.calendarKey = calendarKey; + } + + public String getGoogleCalendarId() { + return googleCalendarId; + } + + public void setGoogleCalendarId(String googleCalendarId) { + this.googleCalendarId = googleCalendarId; + } + + public Long getChannelId() { + return channelId; + } + + public void setChannelId(Long channelId) { + this.channelId = channelId; + } + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } +} diff --git a/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessage.java b/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessage.java new file mode 100644 index 0000000..0858e24 --- /dev/null +++ b/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessage.java @@ -0,0 +1,9 @@ +package com.softawii.capivara.entity.messages; + +public class CalendarSubscriptionMessage { + + private CalendarSubscriptionMessageType type; + private String eventId; + + +} diff --git a/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessageType.java b/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessageType.java new file mode 100644 index 0000000..345132c --- /dev/null +++ b/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessageType.java @@ -0,0 +1,6 @@ +package com.softawii.capivara.entity.messages; + +public enum CalendarSubscriptionMessageType { + CHECK, + NOTIFY +} diff --git a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java index 278d29b..a92af38 100644 --- a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java +++ b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java @@ -1,41 +1,45 @@ -package com.softawii.capivara.listeners; - -import com.softawii.curupira.annotations.IArgument; -import com.softawii.curupira.annotations.ICommand; -import com.softawii.curupira.annotations.IGroup; -import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import net.dv8tion.jda.api.interactions.commands.OptionType; - -@IGroup(name = "Calendar", description = "Calendar events", hidden = false) -public class CalendarGroup { - - @ICommand(name = "Subscribe", description = "Subscribe to a Google Calendar", permissions = {Permission.ADMINISTRATOR}) - @IArgument(name = "Calendar ID", description = "The ID of the calendar to subscribe to", required = true) - @IArgument(name = "Name", description = "The name to use for the calendar", required = true) - @IArgument(name = "Channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) - @IArgument(name = "Role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) - public static void subscribe(SlashCommandInteractionEvent event) { - event.reply("Not implemented yet").queue(); - } - - @ICommand(name = "Unsubscribe", description = "Unsubscribe from a Google Calendar", permissions = {Permission.ADMINISTRATOR}) - @IArgument(name = "Name", description = "The name to use for the calendar", required = true) - public static void unsubscribe(SlashCommandInteractionEvent event) { - event.reply("Not implemented yet").queue(); - } - - @ICommand(name = "list", description = "List all events from a Google Calendar") - @IArgument(name = "Name", description = "The name to use for the calendar", required = false) - public static void list(SlashCommandInteractionEvent event) { - event.reply("Not implemented yet").queue(); - } - - @ICommand(name = "Update", description = "Update configuration of a calendar", permissions = {Permission.ADMINISTRATOR}) - @IArgument(name = "Name", description = "The name to use for the calendar", required = true) - @IArgument(name = "Channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) - @IArgument(name = "Role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) - public static void update(SlashCommandInteractionEvent event) { - event.reply("Not implemented yet").queue(); - } -} +package com.softawii.capivara.listeners; + +import com.softawii.curupira.annotations.IArgument; +import com.softawii.curupira.annotations.ICommand; +import com.softawii.curupira.annotations.IGroup; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@IGroup(name = "Calendar", description = "Calendar events", hidden = false) +public class CalendarGroup { + + @ICommand(name = "Subscribe", description = "Subscribe to a Google Calendar", permissions = {Permission.ADMINISTRATOR}) + @IArgument(name = "Calendar ID", description = "The ID of the calendar to subscribe to", required = true) + @IArgument(name = "Name", description = "The name to use for the calendar", required = true) + @IArgument(name = "Channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) + @IArgument(name = "Role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) + public static void subscribe(SlashCommandInteractionEvent event) { + event.reply("Not implemented yet").queue(); + } + + @ICommand(name = "Unsubscribe", description = "Unsubscribe from a Google Calendar", permissions = {Permission.ADMINISTRATOR}) + @IArgument(name = "Name", description = "The name to use for the calendar", required = true) + public static void unsubscribe(SlashCommandInteractionEvent event) { + event.reply("Not implemented yet").queue(); + } + + @ICommand(name = "list", description = "List all events from a Google Calendar") + @IArgument(name = "Name", description = "The name to use for the calendar", required = false) + public static void list(SlashCommandInteractionEvent event) { + event.reply("Not implemented yet").queue(); + } + + @ICommand(name = "Update", description = "Update configuration of a calendar", permissions = {Permission.ADMINISTRATOR}) + @IArgument(name = "Name", description = "The name to use for the calendar", required = true) + @IArgument(name = "Channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) + @IArgument(name = "Role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) + public static void update(SlashCommandInteractionEvent event) { + event.reply("Not implemented yet").queue(); + } +} diff --git a/src/main/java/com/softawii/capivara/services/CalendarService.java b/src/main/java/com/softawii/capivara/services/CalendarService.java new file mode 100644 index 0000000..5d53a2c --- /dev/null +++ b/src/main/java/com/softawii/capivara/services/CalendarService.java @@ -0,0 +1,51 @@ +package com.softawii.capivara.services; + +import com.softawii.capivara.entity.Calendar; +import com.softawii.capivara.repository.CalendarRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class CalendarService { + + private final CalendarRepository repository; + + public CalendarService(CalendarRepository repository) { + this.repository = repository; + } + + public Page findAll(Pageable pageable) { + return repository.findAll(pageable); + } + + public Optional find(Calendar.CalendarKey calendarKey) { + return repository.findById(calendarKey); + } + + public void create(Calendar calendar) { + if (repository.existsById(calendar.getCalendarKey())) { + // TODO: create new exception + throw new RuntimeException("Calendar already exists with this name and guild"); + } + repository.save(calendar); + } + + public void update(Calendar calendar) { + if (!repository.existsById(calendar.getCalendarKey())) { + // TODO: create new exception + throw new RuntimeException("Calendar does not exist with this name and guild"); + } + repository.save(calendar); + } + + public void delete(Calendar.CalendarKey calendarKey) { + repository.deleteById(calendarKey); + } + + public void exists(Calendar.CalendarKey calendarKey) { + repository.existsById(calendarKey); + } +} diff --git a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java new file mode 100644 index 0000000..f7bed76 --- /dev/null +++ b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java @@ -0,0 +1,100 @@ +package com.softawii.capivara.threads; + +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.services.calendar.model.Event; +import com.google.api.services.calendar.model.Events; +import com.softawii.capivara.core.CalendarManager; +import com.softawii.capivara.entity.Calendar; +import com.softawii.capivara.services.CalendarService; +import net.dv8tion.jda.api.JDA; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.init.Jackson2RepositoryPopulatorFactoryBean; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.List; +import java.util.Timer; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; + +@Component +public class CalendarSubscriptionThread implements Runnable { + + /** + * The queue of events to be processed, in order. + * No need to synchronize, as it is a thread-safe queue. + * @see java.util.concurrent.BlockingQueue + * + * Enqueue is made by any thread, dequeue is made by the thread that created the queue. + * @see java.util.concurrent.BlockingQueue#put + * @see java.util.concurrent.BlockingQueue#take + * + * If there is no event to be processed, the thread will wait until there is one. + */ + private final Logger LOGGER = LogManager.getLogger(CalendarSubscriptionThread.class); + private final BlockingQueue queue; + private final ScheduledExecutorService scheduler; + private final JDA jda; + private final CalendarService calendarService; + + public CalendarSubscriptionThread(JDA jda, CalendarService calendarService) { + this.jda = jda; + this.queue = new LinkedBlockingQueue<>(); + this.scheduler = Executors.newScheduledThreadPool(1); + this.calendarService = calendarService; + } + + public void niceName() { + PageRequest request = PageRequest.of(0, 100); + Page calendarPage = this.calendarService.findAll(request); + while (calendarPage.hasContent()) { + calendarPage.forEach(calendar -> { + Calendar.CalendarKey calendarKey = calendar.getCalendarKey(); + String name = calendarKey.getCalendarName(); + Long guildId = calendarKey.getGuildId(); + }); + + request = request.next(); + calendarPage = this.calendarService.findAll(request); + } + } + + private void subscribe(Calendar calendar) throws GeneralSecurityException, IOException { + NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + GsonFactory factory = GsonFactory.getDefaultInstance(); + + com.google.api.services.calendar.Calendar service = new com.google.api.services.calendar.Calendar.Builder(httpTransport, factory, null).setApplicationName("Capivara").build(); + String pageToken = null; + do { + Events events = service.events().list(calendar.getGoogleCalendarId()).setPageToken(pageToken).execute(); + List items = events.getItems(); + for (Event event : items) { + LOGGER.info(event.getSummary()); + } + pageToken = events.getNextPageToken(); + } while (pageToken != null); + } + + @Override + public void run() { + while (true) { + try { + String event = queue.take(); + // Process the event + } catch (InterruptedException e) { + // The thread was interrupted, just exit + return; + } + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 84b44f6..ed58461 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,5 +4,6 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.datasource.username=sa spring.datasource.password=sa hibernate.hbm2ddl.auto=update -curupira.reset=false +curupira.reset=true +token=ODMyMDgzMTg0ODgzMjA0MDk2.G5LzHU.yvnujWpYu5T59QhA6AqbYkmaKyIj5vYgeDlwKE logging.level.root=debug \ No newline at end of file From fe6263198db9c7e21c8dd9642a51b65d50ef7ce3 Mon Sep 17 00:00:00 2001 From: FerroEduardo <47820549+FerroEduardo@users.noreply.github.com> Date: Sat, 28 Oct 2023 18:03:33 -0300 Subject: [PATCH 3/9] feat(google-calendar): add calendar bean and example of usage --- .../com/softawii/capivara/TestGoogle.java | 100 ++++++++++++++++++ .../capivara/config/SpringConfig.java | 25 +++++ .../repository/CalendarRepository.java | 7 ++ .../threads/CalendarSubscriptionThread.java | 24 ++--- .../softawii/capivara/utils/CalendarUtil.java | 35 ++++++ src/main/resources/application.properties | 3 +- 6 files changed, 177 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/softawii/capivara/TestGoogle.java create mode 100644 src/main/java/com/softawii/capivara/repository/CalendarRepository.java create mode 100644 src/main/java/com/softawii/capivara/utils/CalendarUtil.java diff --git a/src/main/java/com/softawii/capivara/TestGoogle.java b/src/main/java/com/softawii/capivara/TestGoogle.java new file mode 100644 index 0000000..e1036ca --- /dev/null +++ b/src/main/java/com/softawii/capivara/TestGoogle.java @@ -0,0 +1,100 @@ +package com.softawii.capivara; + +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.util.DateTime; +import com.google.api.services.calendar.Calendar; +import com.google.api.services.calendar.model.Event; +import com.google.api.services.calendar.model.EventDateTime; +import com.google.api.services.calendar.model.Events; +import com.softawii.capivara.utils.CalendarUtil; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.Instant; +import java.util.List; + +public class TestGoogle { + public static void main(String[] args) throws GeneralSecurityException, IOException { + String calendarId = ""; + String googleApiKey = System.getenv("googleApiKey"); + NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + GsonFactory factory = GsonFactory.getDefaultInstance(); + + Calendar service = new Calendar.Builder(httpTransport, factory, null) + .setApplicationName("Capivara") + .setGoogleClientRequestInitializer(request -> request.set("key", googleApiKey)) + .build(); + + DateTime minDateTime = CalendarUtil.getMinDateTime(0); + DateTime maxDateTime = CalendarUtil.getMaxDateTime(0); + Instant now = Instant.now(); + System.out.printf("minDateTime: %s :: maxDateTime: %s%n", minDateTime, maxDateTime); + String pageToken = null; + System.out.println("Events ---------------------"); + do { + try { + Events events = service + .events() + .list(calendarId) + .setSingleEvents(true) + .setTimeMin(minDateTime) + .setTimeMax(maxDateTime) + .setOrderBy("startTime") + .setPageToken(pageToken) + .execute(); + List items = events.getItems(); + for (Event event : items) { + String title = event.getSummary(); + String description = event.getDescription(); + + EventDateTime eventStart = event.getStart(); + EventDateTime eventEnd = event.getEnd(); + DateTime dateStart; + DateTime dateEnd; + boolean isAllDayEvent = eventStart.getDate() != null && eventEnd.getDate() != null; + if (isAllDayEvent) { + dateStart = eventStart.getDate(); + dateEnd = eventEnd.getDate(); + } else { + dateStart = eventStart.getDateTime(); + dateEnd = eventEnd.getDateTime(); + } + boolean alreadyStarted = dateStart.getValue() <= now.toEpochMilli(); + boolean alreadyEnded = dateEnd.getValue() <= now.toEpochMilli(); + System.out.printf(""" + ID: %s + Title: %s + Description: %s + Start: %s + End: %s + All day event: %s + Already started: %s + Already ended: %s + ------------------------- + """, + event.getId(), + title, + description, + dateStart, + dateEnd, + isAllDayEvent ? "yes" : "no", + alreadyStarted ? "yes" : "no", + alreadyEnded ? "yes" : "no" + ); + } + pageToken = events.getNextPageToken(); + } catch (GoogleJsonResponseException e) { + System.out.println(e.getDetails()); + if ((e.getStatusCode() == 403 && !e.getStatusMessage().equals("Forbidden")) || e.getStatusCode() == 429) { + System.out.println("rate limited"); + } else { + e.printStackTrace(); + } + } + } while (pageToken != null); + } + +} diff --git a/src/main/java/com/softawii/capivara/config/SpringConfig.java b/src/main/java/com/softawii/capivara/config/SpringConfig.java index 0835ba6..ef350b5 100644 --- a/src/main/java/com/softawii/capivara/config/SpringConfig.java +++ b/src/main/java/com/softawii/capivara/config/SpringConfig.java @@ -1,5 +1,9 @@ package com.softawii.capivara.config; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.services.calendar.Calendar; import com.softawii.capivara.utils.CapivaraExceptionHandler; import com.softawii.curupira.core.Curupira; import net.dv8tion.jda.api.JDA; @@ -40,6 +44,9 @@ public class SpringConfig { @Value("${token}") private String discordToken; + @Value("${google.calendar.api_token}") + private String googleCalendarApiToken; + public SpringConfig(Environment env) { this.env = env; } @@ -126,4 +133,22 @@ public PlatformTransactionManager transactionManager() { public PersistenceExceptionTranslationPostProcessor exceptionTranslation() { return new PersistenceExceptionTranslationPostProcessor(); } + + @Bean + public Calendar googleCalendar() { + NetHttpTransport httpTransport; + try { + httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize GoogleNetHttpTransport", e); + } + GsonFactory factory = GsonFactory.getDefaultInstance(); + + Calendar service = new Calendar.Builder(httpTransport, factory, null) + .setApplicationName("Capivara") + .setGoogleClientRequestInitializer(request -> request.set("key", googleCalendarApiToken)) + .build(); + + return service; + } } diff --git a/src/main/java/com/softawii/capivara/repository/CalendarRepository.java b/src/main/java/com/softawii/capivara/repository/CalendarRepository.java new file mode 100644 index 0000000..bfad8db --- /dev/null +++ b/src/main/java/com/softawii/capivara/repository/CalendarRepository.java @@ -0,0 +1,7 @@ +package com.softawii.capivara.repository; + +import com.softawii.capivara.entity.Calendar; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CalendarRepository extends JpaRepository { +} diff --git a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java index f7bed76..f6ecd37 100644 --- a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java +++ b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java @@ -1,12 +1,7 @@ package com.softawii.capivara.threads; -import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; -import com.google.api.client.http.HttpRequestInitializer; -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.gson.GsonFactory; import com.google.api.services.calendar.model.Event; import com.google.api.services.calendar.model.Events; -import com.softawii.capivara.core.CalendarManager; import com.softawii.capivara.entity.Calendar; import com.softawii.capivara.services.CalendarService; import net.dv8tion.jda.api.JDA; @@ -14,14 +9,10 @@ import org.apache.logging.log4j.Logger; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.repository.init.Jackson2RepositoryPopulatorFactoryBean; import org.springframework.stereotype.Component; import java.io.IOException; -import java.security.GeneralSecurityException; import java.util.List; -import java.util.Timer; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; @@ -45,10 +36,12 @@ public class CalendarSubscriptionThread implements Runnable { private final BlockingQueue queue; private final ScheduledExecutorService scheduler; private final JDA jda; + private final com.google.api.services.calendar.Calendar googleCalendarService; private final CalendarService calendarService; - public CalendarSubscriptionThread(JDA jda, CalendarService calendarService) { + public CalendarSubscriptionThread(JDA jda, CalendarService calendarService, com.google.api.services.calendar.Calendar googleCalendarService) { this.jda = jda; + this.googleCalendarService = googleCalendarService; this.queue = new LinkedBlockingQueue<>(); this.scheduler = Executors.newScheduledThreadPool(1); this.calendarService = calendarService; @@ -69,14 +62,13 @@ public void niceName() { } } - private void subscribe(Calendar calendar) throws GeneralSecurityException, IOException { - NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); - GsonFactory factory = GsonFactory.getDefaultInstance(); - - com.google.api.services.calendar.Calendar service = new com.google.api.services.calendar.Calendar.Builder(httpTransport, factory, null).setApplicationName("Capivara").build(); + private void subscribe(Calendar calendar) throws IOException { String pageToken = null; do { - Events events = service.events().list(calendar.getGoogleCalendarId()).setPageToken(pageToken).execute(); + Events events = googleCalendarService.events() + .list(calendar.getGoogleCalendarId()) + .setPageToken(pageToken) + .execute(); List items = events.getItems(); for (Event event : items) { LOGGER.info(event.getSummary()); diff --git a/src/main/java/com/softawii/capivara/utils/CalendarUtil.java b/src/main/java/com/softawii/capivara/utils/CalendarUtil.java new file mode 100644 index 0000000..f189656 --- /dev/null +++ b/src/main/java/com/softawii/capivara/utils/CalendarUtil.java @@ -0,0 +1,35 @@ +package com.softawii.capivara.utils; + +import com.google.api.client.util.DateTime; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class CalendarUtil { + + public static final DateTimeFormatter RFC_3339_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX"); + public static final ZoneId ZONE_ID = ZoneId.of("GMT"); + + public static DateTime getMinDateTime(int daysToIncrement) { + LocalTime time = LocalTime.MIN; + LocalDate date = LocalDate.now(ZONE_ID).plusDays(daysToIncrement); + + ZonedDateTime zonedDateTime = ZonedDateTime.of(date, time, ZONE_ID); + String rfc3339String = zonedDateTime.format(RFC_3339_FORMATTER); + + return DateTime.parseRfc3339(rfc3339String); + } + + public static DateTime getMaxDateTime(int daysToIncrement) { + LocalTime time = LocalTime.MAX; + LocalDate date = LocalDate.now(ZONE_ID).plusDays(daysToIncrement); + + ZonedDateTime zonedDateTime = ZonedDateTime.of(date, time, ZONE_ID); + String rfc3339String = zonedDateTime.format(RFC_3339_FORMATTER); + + return DateTime.parseRfc3339(rfc3339String); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ed58461..81a3cf3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,5 +5,6 @@ spring.datasource.username=sa spring.datasource.password=sa hibernate.hbm2ddl.auto=update curupira.reset=true -token=ODMyMDgzMTg0ODgzMjA0MDk2.G5LzHU.yvnujWpYu5T59QhA6AqbYkmaKyIj5vYgeDlwKE +token= +google.calendar.api_token= logging.level.root=debug \ No newline at end of file From d03aa66227b061828a0b4b27d577078cfb67ccec Mon Sep 17 00:00:00 2001 From: yaansz Date: Sun, 29 Oct 2023 20:02:02 -0300 Subject: [PATCH 4/9] feat: creating timer --- .../capivara/core/CalendarManager.java | 3 +- .../entity/internal/CalendarSubscriber.java | 165 ++++++++++++++++++ .../exceptions/DuplicatedKeyException.java | 23 +++ .../capivara/listeners/CalendarGroup.java | 2 + .../capivara/services/CalendarService.java | 5 +- .../threads/CalendarSubscriptionThread.java | 32 ++-- 6 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java create mode 100644 src/main/java/com/softawii/capivara/exceptions/DuplicatedKeyException.java diff --git a/src/main/java/com/softawii/capivara/core/CalendarManager.java b/src/main/java/com/softawii/capivara/core/CalendarManager.java index 881ee4b..bd164c4 100644 --- a/src/main/java/com/softawii/capivara/core/CalendarManager.java +++ b/src/main/java/com/softawii/capivara/core/CalendarManager.java @@ -1,6 +1,7 @@ package com.softawii.capivara.core; import com.softawii.capivara.entity.Calendar; +import com.softawii.capivara.exceptions.DuplicatedKeyException; import com.softawii.capivara.services.CalendarService; import com.softawii.capivara.threads.CalendarSubscriptionThread; import net.dv8tion.jda.api.JDA; @@ -24,7 +25,7 @@ public CalendarManager(JDA jda, CalendarService service, CalendarSubscriptionThr this.subscriber = subscriber; } - public void createCalendar(String googleCalendarId, String name, GuildChannelUnion channel, Role role) { + public void createCalendar(String googleCalendarId, String name, GuildChannelUnion channel, Role role) throws DuplicatedKeyException { Long guildId = channel.getGuild().getIdLong(); Long channelId = channel.getIdLong(); Long roleId = role != null ? role.getIdLong() : null; diff --git a/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java b/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java new file mode 100644 index 0000000..c26bc30 --- /dev/null +++ b/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java @@ -0,0 +1,165 @@ +package com.softawii.capivara.entity.internal; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.util.DateTime; +import com.google.api.services.calendar.model.Event; +import com.google.api.services.calendar.model.EventDateTime; +import com.google.api.services.calendar.model.Events; +import com.softawii.capivara.core.CalendarManager; +import com.softawii.capivara.entity.Calendar; +import com.softawii.capivara.utils.CalendarUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.text.ParseException; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +public class CalendarSubscriber { + private final Logger LOGGER = LogManager.getLogger(CalendarSubscriber.class); + private String googleCalendarId; + private List consumers; + private com.google.api.services.calendar.Calendar calendarService; + private HashMap events; + + private class EventWrapper extends TimerTask { + private Event event; + private final Timer timer; + + public EventWrapper(Event event) { + this.event = event; + this.timer = new Timer(); + setTimer(); + } + + private void setTimer() { + EventDateTime eventStart = this.event.getStart(); + EventDateTime eventEnd = this.event.getEnd(); + Instant now = Instant.now(); + DateTime dateStart; + DateTime dateEnd; + boolean isAllDayEvent = eventStart.getDate() != null && eventEnd.getDate() != null; + + if (isAllDayEvent) { + dateStart = eventStart.getDate(); + dateEnd = eventEnd.getDate(); + } else { + dateStart = eventStart.getDateTime(); + dateEnd = eventEnd.getDateTime(); + } + + boolean alreadyStarted = dateStart.getValue() <= now.toEpochMilli(); + boolean alreadyEnded = dateEnd.getValue() <= now.toEpochMilli(); + + if(!alreadyEnded && !alreadyStarted) { + Date scheduled = new Date(dateStart.getValue()); + LOGGER.info("Event scheduled: " + this.event.getSummary() + ", date: " + scheduled); + this.timer.schedule(this, scheduled); + } + } + + public void setEvent(Event event) { + if(this.event.getStart() != event.getStart()) { + this.timer.cancel(); + this.timer.purge(); + setTimer(); + } + + this.event = event; + } + + @Override + public void run() { + LOGGER.info("Event started: " + this.event.getSummary()); + } + } + + public CalendarSubscriber(String googleCalendarId, com.google.api.services.calendar.Calendar calendarService, Calendar.CalendarKey consumer) { + this.googleCalendarId = googleCalendarId; + this.calendarService = calendarService; + this.events = new HashMap<>(); + this.consumers = new ArrayList<>(); + this.consumers.add(consumer); + update(); + } + + public void subscribe(Calendar.CalendarKey consumer) { + if (!this.consumers.contains(consumer)) { + this.consumers.add(consumer); + } + } + + public void unsubscribe(Calendar.CalendarKey consumer) { + this.consumers.remove(consumer); + } + + private List getEvents() { + List items = new ArrayList<>(); + String pageToken = null; + do { + try { + Events events = calendarService + .events() + .list(googleCalendarId) + .setSingleEvents(true) + .setTimeMin(CalendarUtil.getMinDateTime(-1)) + .setTimeMax(CalendarUtil.getMaxDateTime(1)) + .setOrderBy("startTime") + .setPageToken(pageToken) + .execute(); + items.addAll(events.getItems()); + pageToken = events.getNextPageToken(); + } catch (GoogleJsonResponseException e) { + if ((e.getStatusCode() == 403 && !e.getStatusMessage().equals("Forbidden")) || e.getStatusCode() == 429) { + LOGGER.error(e.getDetails().getMessage() + " - rate limit - " + e.getDetails().getCode()); + return null; + } else { + LOGGER.error(e.getDetails().getMessage()); + return null; + } + } catch (IOException e) { + LOGGER.error(e.getMessage()); + return null; + } + } while (pageToken != null); + + return items; + } + + private void checkForUpdates(List events) { + // Removing events + Set localKeys = this.events.keySet(); + Set remoteKeys = events.stream().map(Event::getId).collect(Collectors.toSet()); + localKeys.removeAll(remoteKeys); + + // Removing events + for (String key : localKeys) { + EventWrapper eventWrapper = this.events.get(key); + LOGGER.info("Event removed: " + eventWrapper.event.getSummary()); + eventWrapper.cancel(); + eventWrapper.timer.purge(); + this.events.remove(key); + } + + // Updating events + for (Event event : events) { + String eventId = event.getId(); + EventWrapper eventWrapper = this.events.get(eventId); + if (eventWrapper == null) { + this.events.put(eventId, new EventWrapper(event)); + LOGGER.info("Event added: " + event.getSummary()); + } else { + eventWrapper.setEvent(event); + } + } + } + + public void update() { + List events = getEvents(); + if (events != null) { + checkForUpdates(events); + } + } +} diff --git a/src/main/java/com/softawii/capivara/exceptions/DuplicatedKeyException.java b/src/main/java/com/softawii/capivara/exceptions/DuplicatedKeyException.java new file mode 100644 index 0000000..ed11e59 --- /dev/null +++ b/src/main/java/com/softawii/capivara/exceptions/DuplicatedKeyException.java @@ -0,0 +1,23 @@ +package com.softawii.capivara.exceptions; + +public class DuplicatedKeyException extends Exception { + + public DuplicatedKeyException() { + } + + public DuplicatedKeyException(String message) { + super(message); + } + + public DuplicatedKeyException(String message, Throwable cause) { + super(message, cause); + } + + public DuplicatedKeyException(Throwable cause) { + super(cause); + } + + public DuplicatedKeyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java index a92af38..b0ebe08 100644 --- a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java +++ b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java @@ -8,6 +8,8 @@ import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionType; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/softawii/capivara/services/CalendarService.java b/src/main/java/com/softawii/capivara/services/CalendarService.java index 5d53a2c..5f18e61 100644 --- a/src/main/java/com/softawii/capivara/services/CalendarService.java +++ b/src/main/java/com/softawii/capivara/services/CalendarService.java @@ -1,6 +1,7 @@ package com.softawii.capivara.services; import com.softawii.capivara.entity.Calendar; +import com.softawii.capivara.exceptions.DuplicatedKeyException; import com.softawii.capivara.repository.CalendarRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -25,10 +26,10 @@ public Optional find(Calendar.CalendarKey calendarKey) { return repository.findById(calendarKey); } - public void create(Calendar calendar) { + public void create(Calendar calendar) throws DuplicatedKeyException { if (repository.existsById(calendar.getCalendarKey())) { // TODO: create new exception - throw new RuntimeException("Calendar already exists with this name and guild"); + throw new DuplicatedKeyException("Calendar already exists with this name and guild, use update instead"); } repository.save(calendar); } diff --git a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java index f6ecd37..261ea6a 100644 --- a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java +++ b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java @@ -3,6 +3,7 @@ import com.google.api.services.calendar.model.Event; import com.google.api.services.calendar.model.Events; import com.softawii.capivara.entity.Calendar; +import com.softawii.capivara.entity.internal.CalendarSubscriber; import com.softawii.capivara.services.CalendarService; import net.dv8tion.jda.api.JDA; import org.apache.logging.log4j.LogManager; @@ -38,6 +39,7 @@ public class CalendarSubscriptionThread implements Runnable { private final JDA jda; private final com.google.api.services.calendar.Calendar googleCalendarService; private final CalendarService calendarService; +// private final HashMap public CalendarSubscriptionThread(JDA jda, CalendarService calendarService, com.google.api.services.calendar.Calendar googleCalendarService) { this.jda = jda; @@ -45,36 +47,26 @@ public CalendarSubscriptionThread(JDA jda, CalendarService calendarService, com. this.queue = new LinkedBlockingQueue<>(); this.scheduler = Executors.newScheduledThreadPool(1); this.calendarService = calendarService; + + startupSubscriber(); + + Thread thread = new Thread(this); + thread.start(); + LOGGER.info("CalendarSubscriptionThread started"); } - public void niceName() { + private void startupSubscriber() { PageRequest request = PageRequest.of(0, 100); Page calendarPage = this.calendarService.findAll(request); while (calendarPage.hasContent()) { - calendarPage.forEach(calendar -> { - Calendar.CalendarKey calendarKey = calendar.getCalendarKey(); - String name = calendarKey.getCalendarName(); - Long guildId = calendarKey.getGuildId(); - }); - + calendarPage.forEach(this::subscribe); request = request.next(); calendarPage = this.calendarService.findAll(request); } } - private void subscribe(Calendar calendar) throws IOException { - String pageToken = null; - do { - Events events = googleCalendarService.events() - .list(calendar.getGoogleCalendarId()) - .setPageToken(pageToken) - .execute(); - List items = events.getItems(); - for (Event event : items) { - LOGGER.info(event.getSummary()); - } - pageToken = events.getNextPageToken(); - } while (pageToken != null); + private void subscribe(Calendar calendar) { + new CalendarSubscriber(calendar.getGoogleCalendarId(), this.googleCalendarService, calendar.getCalendarKey()); } @Override From f1f4708d0de2612dcb1fdb93b8fec3a76ad6cb1e Mon Sep 17 00:00:00 2001 From: FerroEduardo <47820549+FerroEduardo@users.noreply.github.com> Date: Mon, 30 Oct 2023 11:48:04 -0300 Subject: [PATCH 5/9] feat: fix calendar commands --- .../capivara/listeners/CalendarGroup.java | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java index b0ebe08..f772fd7 100644 --- a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java +++ b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java @@ -4,43 +4,37 @@ import com.softawii.curupira.annotations.ICommand; import com.softawii.curupira.annotations.IGroup; import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.entities.Role; -import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionType; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -@IGroup(name = "Calendar", description = "Calendar events", hidden = false) +@IGroup(name = "calendar", description = "Calendar events", hidden = false) public class CalendarGroup { - @ICommand(name = "Subscribe", description = "Subscribe to a Google Calendar", permissions = {Permission.ADMINISTRATOR}) - @IArgument(name = "Calendar ID", description = "The ID of the calendar to subscribe to", required = true) - @IArgument(name = "Name", description = "The name to use for the calendar", required = true) - @IArgument(name = "Channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) - @IArgument(name = "Role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) + @ICommand(name = "subscribe", description = "Subscribe to a Google Calendar", permissions = {Permission.ADMINISTRATOR}) + @IArgument(name = "calendar-id", description = "The ID of the calendar to subscribe to", required = true) + @IArgument(name = "name", description = "The name to use for the calendar", required = true) + @IArgument(name = "channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) + @IArgument(name = "role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) public static void subscribe(SlashCommandInteractionEvent event) { event.reply("Not implemented yet").queue(); } - @ICommand(name = "Unsubscribe", description = "Unsubscribe from a Google Calendar", permissions = {Permission.ADMINISTRATOR}) - @IArgument(name = "Name", description = "The name to use for the calendar", required = true) + @ICommand(name = "unsubscribe", description = "Unsubscribe from a Google Calendar", permissions = {Permission.ADMINISTRATOR}) + @IArgument(name = "name", description = "The name to use for the calendar", required = true) public static void unsubscribe(SlashCommandInteractionEvent event) { event.reply("Not implemented yet").queue(); } @ICommand(name = "list", description = "List all events from a Google Calendar") - @IArgument(name = "Name", description = "The name to use for the calendar", required = false) + @IArgument(name = "name", description = "The name to use for the calendar", required = false) public static void list(SlashCommandInteractionEvent event) { event.reply("Not implemented yet").queue(); } - @ICommand(name = "Update", description = "Update configuration of a calendar", permissions = {Permission.ADMINISTRATOR}) - @IArgument(name = "Name", description = "The name to use for the calendar", required = true) - @IArgument(name = "Channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) - @IArgument(name = "Role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) + @ICommand(name = "update", description = "Update configuration of a calendar", permissions = {Permission.ADMINISTRATOR}) + @IArgument(name = "name", description = "The name to use for the calendar", required = true) + @IArgument(name = "channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) + @IArgument(name = "role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) public static void update(SlashCommandInteractionEvent event) { event.reply("Not implemented yet").queue(); } From ad8dc94424c52c0e0090c66a6ec4ec49bbc9c670 Mon Sep 17 00:00:00 2001 From: FerroEduardo <47820549+FerroEduardo@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:43:04 -0300 Subject: [PATCH 6/9] feat: add subscription, automatic event message dispatch and google calendar service TimerTask cannot be reused --- .../capivara/core/CalendarManager.java | 3 +- .../entity/internal/CalendarSubscriber.java | 164 +++++++++--------- .../capivara/listeners/CalendarGroup.java | 29 +++- .../services/GoogleCalendarService.java | 106 +++++++++++ .../threads/CalendarSubscriptionThread.java | 59 +++++-- 5 files changed, 258 insertions(+), 103 deletions(-) create mode 100644 src/main/java/com/softawii/capivara/services/GoogleCalendarService.java diff --git a/src/main/java/com/softawii/capivara/core/CalendarManager.java b/src/main/java/com/softawii/capivara/core/CalendarManager.java index bd164c4..fe8d374 100644 --- a/src/main/java/com/softawii/capivara/core/CalendarManager.java +++ b/src/main/java/com/softawii/capivara/core/CalendarManager.java @@ -31,6 +31,7 @@ public void createCalendar(String googleCalendarId, String name, GuildChannelUni Long roleId = role != null ? role.getIdLong() : null; Calendar calendar = new Calendar(guildId, googleCalendarId, name, channelId, roleId); - service.create(calendar); + this.service.create(calendar); + this.subscriber.subscribe(calendar); } } diff --git a/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java b/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java index c26bc30..130b633 100644 --- a/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java +++ b/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java @@ -1,145 +1,124 @@ package com.softawii.capivara.entity.internal; -import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.google.api.client.util.DateTime; import com.google.api.services.calendar.model.Event; import com.google.api.services.calendar.model.EventDateTime; -import com.google.api.services.calendar.model.Events; -import com.softawii.capivara.core.CalendarManager; import com.softawii.capivara.entity.Calendar; -import com.softawii.capivara.utils.CalendarUtil; +import com.softawii.capivara.services.GoogleCalendarService; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.io.IOException; -import java.text.ParseException; -import java.time.Instant; import java.util.*; import java.util.stream.Collectors; public class CalendarSubscriber { - private final Logger LOGGER = LogManager.getLogger(CalendarSubscriber.class); - private String googleCalendarId; - private List consumers; - private com.google.api.services.calendar.Calendar calendarService; - private HashMap events; + private final Logger LOGGER = LogManager.getLogger(CalendarSubscriber.class); + private final String googleCalendarId; + private final List consumers; + private final GoogleCalendarService googleCalendarService; + private final HashMap events; + private final JDA jda; private class EventWrapper extends TimerTask { - private Event event; - private final Timer timer; + private static final Logger LOGGER = LogManager.getLogger(EventWrapper.class); + private final Event event; + private final Timer timer; public EventWrapper(Event event) { this.event = event; - this.timer = new Timer(); - setTimer(); + this.timer = new Timer("EventWrapper-" + event.getId()); + schedule(); } - private void setTimer() { + private void schedule() { EventDateTime eventStart = this.event.getStart(); EventDateTime eventEnd = this.event.getEnd(); - Instant now = Instant.now(); - DateTime dateStart; - DateTime dateEnd; - boolean isAllDayEvent = eventStart.getDate() != null && eventEnd.getDate() != null; + boolean isAllDayEvent = eventStart.getDate() != null && eventEnd.getDate() != null; + DateTime dateStart; if (isAllDayEvent) { dateStart = eventStart.getDate(); - dateEnd = eventEnd.getDate(); } else { dateStart = eventStart.getDateTime(); - dateEnd = eventEnd.getDateTime(); } - boolean alreadyStarted = dateStart.getValue() <= now.toEpochMilli(); - boolean alreadyEnded = dateEnd.getValue() <= now.toEpochMilli(); - - if(!alreadyEnded && !alreadyStarted) { - Date scheduled = new Date(dateStart.getValue()); - LOGGER.info("Event scheduled: " + this.event.getSummary() + ", date: " + scheduled); - this.timer.schedule(this, scheduled); - } + Date scheduled = new Date(dateStart.getValue()); + this.timer.schedule(this, scheduled); + LOGGER.info("Event scheduled: " + this.event.getSummary() + ", date: " + scheduled); } - public void setEvent(Event event) { - if(this.event.getStart() != event.getStart()) { - this.timer.cancel(); - this.timer.purge(); - setTimer(); - } - - this.event = event; + public void purge() { + this.timer.cancel(); + this.timer.purge(); } @Override public void run() { LOGGER.info("Event started: " + this.event.getSummary()); + EventDateTime eventStart = this.event.getStart(); + boolean isAllDayEvent = eventStart.getDate() != null; + DateTime dateStart; + + if (isAllDayEvent) { + dateStart = eventStart.getDate(); + } else { + dateStart = eventStart.getDateTime(); + } + + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setTitle(event.getSummary()); + embedBuilder.setDescription(event.getDescription()); + embedBuilder.addField("Hora", dateStart.toStringRfc3339(), false); + + MessageEmbed embed = embedBuilder.build(); + dispatchMessage(embed); } } - public CalendarSubscriber(String googleCalendarId, com.google.api.services.calendar.Calendar calendarService, Calendar.CalendarKey consumer) { + public CalendarSubscriber(String googleCalendarId, GoogleCalendarService googleCalendarService, JDA jda) { this.googleCalendarId = googleCalendarId; - this.calendarService = calendarService; + this.googleCalendarService = googleCalendarService; + this.jda = jda; this.events = new HashMap<>(); this.consumers = new ArrayList<>(); - this.consumers.add(consumer); - update(); } - public void subscribe(Calendar.CalendarKey consumer) { + public void subscribe(Calendar consumer) { if (!this.consumers.contains(consumer)) { this.consumers.add(consumer); } } - public void unsubscribe(Calendar.CalendarKey consumer) { + public void unsubscribe(Calendar consumer) { this.consumers.remove(consumer); } - private List getEvents() { - List items = new ArrayList<>(); - String pageToken = null; - do { - try { - Events events = calendarService - .events() - .list(googleCalendarId) - .setSingleEvents(true) - .setTimeMin(CalendarUtil.getMinDateTime(-1)) - .setTimeMax(CalendarUtil.getMaxDateTime(1)) - .setOrderBy("startTime") - .setPageToken(pageToken) - .execute(); - items.addAll(events.getItems()); - pageToken = events.getNextPageToken(); - } catch (GoogleJsonResponseException e) { - if ((e.getStatusCode() == 403 && !e.getStatusMessage().equals("Forbidden")) || e.getStatusCode() == 429) { - LOGGER.error(e.getDetails().getMessage() + " - rate limit - " + e.getDetails().getCode()); - return null; - } else { - LOGGER.error(e.getDetails().getMessage()); - return null; - } - } catch (IOException e) { - LOGGER.error(e.getMessage()); - return null; - } - } while (pageToken != null); + public boolean isThereAnyConsumer() { + return !this.consumers.isEmpty(); + } - return items; + public void purge() { + this.events.values().forEach(EventWrapper::purge); + this.events.clear(); } private void checkForUpdates(List events) { // Removing events - Set localKeys = this.events.keySet(); + Set localKeys = new HashSet<>(this.events.keySet()); Set remoteKeys = events.stream().map(Event::getId).collect(Collectors.toSet()); localKeys.removeAll(remoteKeys); - // Removing events + // Removing events that are not being listed for (String key : localKeys) { EventWrapper eventWrapper = this.events.get(key); LOGGER.info("Event removed: " + eventWrapper.event.getSummary()); - eventWrapper.cancel(); - eventWrapper.timer.purge(); + eventWrapper.purge(); this.events.remove(key); } @@ -151,15 +130,36 @@ private void checkForUpdates(List events) { this.events.put(eventId, new EventWrapper(event)); LOGGER.info("Event added: " + event.getSummary()); } else { - eventWrapper.setEvent(event); + eventWrapper.purge(); + this.events.put(eventId, new EventWrapper(event)); + LOGGER.info("Event updated: " + event.getSummary()); } } } public void update() { - List events = getEvents(); - if (events != null) { + LOGGER.info("Updating calendar events for calendar '{}'", this.googleCalendarId); + List events = this.googleCalendarService.getEvents(this.googleCalendarId, true, true); + if (!events.isEmpty()) { checkForUpdates(events); } } + + protected void dispatchMessage(MessageEmbed embed) { + // TODO: bulk request + // TODO: automatic delete wrapper after dispatch? + // - maybe a list of wrappers to remove and a faster schedule? + this.consumers.forEach(calendar -> { + TextChannel textChannel = this.jda.getTextChannelById(calendar.getChannelId()); + Role role = calendar.getRoleId() != null ? this.jda.getRoleById(calendar.getRoleId()) : null; + if (textChannel != null) { + MessageCreateAction messageAction = textChannel.sendMessageEmbeds(embed); + if (role != null) { + messageAction = messageAction.mentionRoles(role.getId()); + } + + messageAction.submit(); + } + }); + } } diff --git a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java index f772fd7..fcd34f9 100644 --- a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java +++ b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java @@ -1,22 +1,46 @@ package com.softawii.capivara.listeners; +import com.softawii.capivara.core.CalendarManager; +import com.softawii.capivara.exceptions.DuplicatedKeyException; import com.softawii.curupira.annotations.IArgument; import com.softawii.curupira.annotations.ICommand; import com.softawii.curupira.annotations.IGroup; import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; @IGroup(name = "calendar", description = "Calendar events", hidden = false) +@Component public class CalendarGroup { + private static CalendarManager calendarManager; + + @Autowired + public void setCalendarManager(CalendarManager calendarManager) { + CalendarGroup.calendarManager = calendarManager; + } + @ICommand(name = "subscribe", description = "Subscribe to a Google Calendar", permissions = {Permission.ADMINISTRATOR}) @IArgument(name = "calendar-id", description = "The ID of the calendar to subscribe to", required = true) @IArgument(name = "name", description = "The name to use for the calendar", required = true) @IArgument(name = "channel", description = "The channel where events will be announced", required = true, type = OptionType.CHANNEL) @IArgument(name = "role", description = "The role to be pinged when events begin", required = false, type = OptionType.ROLE) public static void subscribe(SlashCommandInteractionEvent event) { - event.reply("Not implemented yet").queue(); + String googleCalendarId = event.getOption("calendar-id").getAsString(); + String name = event.getOption("name").getAsString(); + GuildChannelUnion channel = event.getOption("channel").getAsChannel(); + Role role = event.getOption("role") != null ? event.getOption("role").getAsRole() : null; + try { + calendarManager.createCalendar(googleCalendarId, name, channel, role); + } catch (DuplicatedKeyException e) { + event.reply("Calendar already exists with this name and guild, use update instead").queue(); + return; + } + event.reply("event subscribed").queue(); } @ICommand(name = "unsubscribe", description = "Unsubscribe from a Google Calendar", permissions = {Permission.ADMINISTRATOR}) @@ -26,7 +50,6 @@ public static void unsubscribe(SlashCommandInteractionEvent event) { } @ICommand(name = "list", description = "List all events from a Google Calendar") - @IArgument(name = "name", description = "The name to use for the calendar", required = false) public static void list(SlashCommandInteractionEvent event) { event.reply("Not implemented yet").queue(); } @@ -38,4 +61,6 @@ public static void list(SlashCommandInteractionEvent event) { public static void update(SlashCommandInteractionEvent event) { event.reply("Not implemented yet").queue(); } + + // TODO: 30/10/2023 autocomplete of names when unsubscribing and updating } diff --git a/src/main/java/com/softawii/capivara/services/GoogleCalendarService.java b/src/main/java/com/softawii/capivara/services/GoogleCalendarService.java new file mode 100644 index 0000000..e03956b --- /dev/null +++ b/src/main/java/com/softawii/capivara/services/GoogleCalendarService.java @@ -0,0 +1,106 @@ +package com.softawii.capivara.services; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.util.DateTime; +import com.google.api.services.calendar.Calendar; +import com.google.api.services.calendar.model.Event; +import com.google.api.services.calendar.model.EventDateTime; +import com.google.api.services.calendar.model.Events; +import com.softawii.capivara.utils.CalendarUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +@Service +public class GoogleCalendarService { + + private final Logger LOGGER = LogManager.getLogger(GoogleCalendarService.class); + + private final Calendar calendar; + + public GoogleCalendarService(Calendar calendar) { + this.calendar = calendar; + } + + public List getEvents(String googleCalendarId, boolean removeStarted, boolean removeEnded) { + LOGGER.info("Fetching events from calendar '{}' - removeStarted: {} - removeEnded: {}", googleCalendarId, removeStarted, removeEnded); + Instant now = Instant.now(); + List items = new ArrayList<>(); + String pageToken = null; + do { + try { + Events rawEvents = calendar + .events() + .list(googleCalendarId) + .setSingleEvents(true) + .setTimeMin(CalendarUtil.getMinDateTime(-1)) + .setTimeMax(CalendarUtil.getMaxDateTime(1)) + .setOrderBy("startTime") + .setPageToken(pageToken) + .execute(); + + List events = rawEvents.getItems() + .stream() + .filter(event -> { + if (removeStarted && this.hasEventStarted(now, event)) { + return false; + } + if (removeEnded && this.hasEventEnded(now, event)) { + return false; + } + + return true; + }) + .toList(); + + items.addAll(events); + pageToken = rawEvents.getNextPageToken(); + } catch (GoogleJsonResponseException e) { + if ((e.getStatusCode() == 403 && !e.getStatusMessage().equals("Forbidden")) || e.getStatusCode() == 429) { + LOGGER.error(e.getDetails().getMessage() + " - rate limit - " + e.getDetails().getCode()); + } else { + LOGGER.error(e.getDetails().getMessage()); + } + return List.of(); + } catch (IOException e) { + LOGGER.error(e.getMessage()); + return List.of(); + } + } while (pageToken != null); + + return items; + } + + private boolean hasEventStarted(Instant now, Event event) { + EventDateTime eventStart = event.getStart(); + DateTime dateStart; + + boolean isAllDayEvent = eventStart.getDate() != null; + if (isAllDayEvent) { + dateStart = eventStart.getDate(); + } else { + dateStart = eventStart.getDateTime(); + } + + return dateStart.getValue() <= now.toEpochMilli(); + } + + private boolean hasEventEnded(Instant now, Event event) { + EventDateTime eventEnd = event.getEnd(); + DateTime dateEnd; + + boolean isAllDayEvent = eventEnd.getDate() != null; + if (isAllDayEvent) { + dateEnd = eventEnd.getDate(); + } else { + dateEnd = eventEnd.getDateTime(); + } + + return dateEnd.getValue() <= now.toEpochMilli(); + } +} diff --git a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java index 261ea6a..8357776 100644 --- a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java +++ b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java @@ -1,10 +1,9 @@ package com.softawii.capivara.threads; -import com.google.api.services.calendar.model.Event; -import com.google.api.services.calendar.model.Events; import com.softawii.capivara.entity.Calendar; import com.softawii.capivara.entity.internal.CalendarSubscriber; import com.softawii.capivara.services.CalendarService; +import com.softawii.capivara.services.GoogleCalendarService; import net.dv8tion.jda.api.JDA; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -12,12 +11,9 @@ import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Component; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.*; @Component public class CalendarSubscriptionThread implements Runnable { @@ -36,23 +32,31 @@ public class CalendarSubscriptionThread implements Runnable { private final Logger LOGGER = LogManager.getLogger(CalendarSubscriptionThread.class); private final BlockingQueue queue; private final ScheduledExecutorService scheduler; - private final JDA jda; - private final com.google.api.services.calendar.Calendar googleCalendarService; - private final CalendarService calendarService; -// private final HashMap + private final JDA jda; + private final GoogleCalendarService googleCalendarService; + private final CalendarService calendarService; + private final Map subscribers; - public CalendarSubscriptionThread(JDA jda, CalendarService calendarService, com.google.api.services.calendar.Calendar googleCalendarService) { + public CalendarSubscriptionThread(JDA jda, CalendarService calendarService, GoogleCalendarService googleCalendarService) { this.jda = jda; this.googleCalendarService = googleCalendarService; this.queue = new LinkedBlockingQueue<>(); - this.scheduler = Executors.newScheduledThreadPool(1); + this.scheduler = Executors.newSingleThreadScheduledExecutor(); this.calendarService = calendarService; + this.subscribers = new HashMap<>(); + this.setup(); + LOGGER.info("CalendarSubscriptionThread started"); + } + private void setup() { startupSubscriber(); + this.scheduler.scheduleAtFixedRate(() -> { + LOGGER.info("Updating calendar events"); + this.subscribers.values().forEach(CalendarSubscriber::update); + }, 0, 1, TimeUnit.MINUTES); - Thread thread = new Thread(this); + Thread thread = new Thread(this, "CalendarSubscriptionThread"); thread.start(); - LOGGER.info("CalendarSubscriptionThread started"); } private void startupSubscriber() { @@ -65,8 +69,27 @@ private void startupSubscriber() { } } - private void subscribe(Calendar calendar) { - new CalendarSubscriber(calendar.getGoogleCalendarId(), this.googleCalendarService, calendar.getCalendarKey()); + public void subscribe(Calendar calendar) { + CalendarSubscriber subscriber = this.subscribers.get(calendar.getGoogleCalendarId()); + if (subscriber == null) { + subscriber = new CalendarSubscriber(calendar.getGoogleCalendarId(), this.googleCalendarService, jda); + this.subscribers.put(calendar.getGoogleCalendarId(), subscriber); + } + + subscriber.subscribe(calendar); + } + + public void unsubscribe(Calendar calendar) { + CalendarSubscriber subscriber = this.subscribers.get(calendar.getGoogleCalendarId()); + if (subscriber == null) { + throw new RuntimeException("There is no subscription available"); + } + + subscriber.unsubscribe(calendar); + if (!subscriber.isThereAnyConsumer()) { + subscriber.purge(); + this.subscribers.remove(calendar.getGoogleCalendarId()); + } } @Override From 2f3a869d36f7655f4029d9592db0ded48f25032d Mon Sep 17 00:00:00 2001 From: yaansz Date: Wed, 1 Nov 2023 00:08:33 -0300 Subject: [PATCH 7/9] feat: removing thread queue --- .../capivara/core/CalendarManager.java | 6 ++-- .../entity/internal/CalendarSubscriber.java | 7 ++-- .../messages/CalendarSubscriptionMessage.java | 9 ----- .../CalendarSubscriptionMessageType.java | 6 ---- ....java => CalendarSubscriptionManager.java} | 35 ++----------------- 5 files changed, 11 insertions(+), 52 deletions(-) delete mode 100644 src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessage.java delete mode 100644 src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessageType.java rename src/main/java/com/softawii/capivara/threads/{CalendarSubscriptionThread.java => CalendarSubscriptionManager.java} (70%) diff --git a/src/main/java/com/softawii/capivara/core/CalendarManager.java b/src/main/java/com/softawii/capivara/core/CalendarManager.java index fe8d374..5e0a418 100644 --- a/src/main/java/com/softawii/capivara/core/CalendarManager.java +++ b/src/main/java/com/softawii/capivara/core/CalendarManager.java @@ -3,7 +3,7 @@ import com.softawii.capivara.entity.Calendar; import com.softawii.capivara.exceptions.DuplicatedKeyException; import com.softawii.capivara.services.CalendarService; -import com.softawii.capivara.threads.CalendarSubscriptionThread; +import com.softawii.capivara.threads.CalendarSubscriptionManager; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; @@ -17,9 +17,9 @@ public class CalendarManager { private final Logger LOGGER = LogManager.getLogger(CalendarManager.class); private final JDA jda; private final CalendarService service; - private final CalendarSubscriptionThread subscriber; + private final CalendarSubscriptionManager subscriber; - public CalendarManager(JDA jda, CalendarService service, CalendarSubscriptionThread subscriber) { + public CalendarManager(JDA jda, CalendarService service, CalendarSubscriptionManager subscriber) { this.jda = jda; this.service = service; this.subscriber = subscriber; diff --git a/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java b/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java index 130b633..1db0a0f 100644 --- a/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java +++ b/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java @@ -87,6 +87,9 @@ public CalendarSubscriber(String googleCalendarId, GoogleCalendarService googleC this.jda = jda; this.events = new HashMap<>(); this.consumers = new ArrayList<>(); + + // Forcing Get Event Info + this.update(); } public void subscribe(Calendar consumer) { @@ -137,7 +140,7 @@ private void checkForUpdates(List events) { } } - public void update() { + public synchronized void update() { LOGGER.info("Updating calendar events for calendar '{}'", this.googleCalendarId); List events = this.googleCalendarService.getEvents(this.googleCalendarId, true, true); if (!events.isEmpty()) { @@ -145,7 +148,7 @@ public void update() { } } - protected void dispatchMessage(MessageEmbed embed) { + protected synchronized void dispatchMessage(MessageEmbed embed) { // TODO: bulk request // TODO: automatic delete wrapper after dispatch? // - maybe a list of wrappers to remove and a faster schedule? diff --git a/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessage.java b/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessage.java deleted file mode 100644 index 0858e24..0000000 --- a/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessage.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.softawii.capivara.entity.messages; - -public class CalendarSubscriptionMessage { - - private CalendarSubscriptionMessageType type; - private String eventId; - - -} diff --git a/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessageType.java b/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessageType.java deleted file mode 100644 index 345132c..0000000 --- a/src/main/java/com/softawii/capivara/entity/messages/CalendarSubscriptionMessageType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.softawii.capivara.entity.messages; - -public enum CalendarSubscriptionMessageType { - CHECK, - NOTIFY -} diff --git a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionManager.java similarity index 70% rename from src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java rename to src/main/java/com/softawii/capivara/threads/CalendarSubscriptionManager.java index 8357776..18199bf 100644 --- a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionThread.java +++ b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionManager.java @@ -16,31 +16,18 @@ import java.util.concurrent.*; @Component -public class CalendarSubscriptionThread implements Runnable { +public class CalendarSubscriptionManager { - /** - * The queue of events to be processed, in order. - * No need to synchronize, as it is a thread-safe queue. - * @see java.util.concurrent.BlockingQueue - * - * Enqueue is made by any thread, dequeue is made by the thread that created the queue. - * @see java.util.concurrent.BlockingQueue#put - * @see java.util.concurrent.BlockingQueue#take - * - * If there is no event to be processed, the thread will wait until there is one. - */ - private final Logger LOGGER = LogManager.getLogger(CalendarSubscriptionThread.class); - private final BlockingQueue queue; + private final Logger LOGGER = LogManager.getLogger(CalendarSubscriptionManager.class); private final ScheduledExecutorService scheduler; private final JDA jda; private final GoogleCalendarService googleCalendarService; private final CalendarService calendarService; private final Map subscribers; - public CalendarSubscriptionThread(JDA jda, CalendarService calendarService, GoogleCalendarService googleCalendarService) { + public CalendarSubscriptionManager(JDA jda, CalendarService calendarService, GoogleCalendarService googleCalendarService) { this.jda = jda; this.googleCalendarService = googleCalendarService; - this.queue = new LinkedBlockingQueue<>(); this.scheduler = Executors.newSingleThreadScheduledExecutor(); this.calendarService = calendarService; this.subscribers = new HashMap<>(); @@ -54,9 +41,6 @@ private void setup() { LOGGER.info("Updating calendar events"); this.subscribers.values().forEach(CalendarSubscriber::update); }, 0, 1, TimeUnit.MINUTES); - - Thread thread = new Thread(this, "CalendarSubscriptionThread"); - thread.start(); } private void startupSubscriber() { @@ -91,17 +75,4 @@ public void unsubscribe(Calendar calendar) { this.subscribers.remove(calendar.getGoogleCalendarId()); } } - - @Override - public void run() { - while (true) { - try { - String event = queue.take(); - // Process the event - } catch (InterruptedException e) { - // The thread was interrupted, just exit - return; - } - } - } } From 7bd8ba551b1a0b02c13fdb0a812bf88ad3ae181b Mon Sep 17 00:00:00 2001 From: yaansz Date: Wed, 1 Nov 2023 00:18:04 -0300 Subject: [PATCH 8/9] feat: updating list command --- .../com/softawii/capivara/core/CalendarManager.java | 10 ++++++++++ .../softawii/capivara/listeners/CalendarGroup.java | 13 ++++++++++++- .../capivara/repository/CalendarRepository.java | 2 ++ .../softawii/capivara/services/CalendarService.java | 9 +++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/softawii/capivara/core/CalendarManager.java b/src/main/java/com/softawii/capivara/core/CalendarManager.java index 5e0a418..f1e01c9 100644 --- a/src/main/java/com/softawii/capivara/core/CalendarManager.java +++ b/src/main/java/com/softawii/capivara/core/CalendarManager.java @@ -11,6 +11,8 @@ import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; +import java.util.List; + @Component public class CalendarManager { @@ -34,4 +36,12 @@ public void createCalendar(String googleCalendarId, String name, GuildChannelUni this.service.create(calendar); this.subscriber.subscribe(calendar); } + + public Calendar getCalendar(String name, Long guildId) { + return this.service.findByNameAndGuildId(name, guildId); + } + + public List getCalendarNames(Long guildId) { + return this.service.getCalendarNames(guildId); + } } diff --git a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java index fcd34f9..6a191e9 100644 --- a/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java +++ b/src/main/java/com/softawii/capivara/listeners/CalendarGroup.java @@ -13,6 +13,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.List; + @IGroup(name = "calendar", description = "Calendar events", hidden = false) @Component public class CalendarGroup { @@ -50,8 +52,17 @@ public static void unsubscribe(SlashCommandInteractionEvent event) { } @ICommand(name = "list", description = "List all events from a Google Calendar") + @IArgument(name = "name", description = "The name to use for the calendar", required = false) public static void list(SlashCommandInteractionEvent event) { - event.reply("Not implemented yet").queue(); + String name = event.getOption("name") != null ? event.getOption("name").getAsString() : null; + + if(name == null) { + List calendarNames = calendarManager.getCalendarNames(event.getGuild().getIdLong()); + event.reply("Available calendars: " + String.join(", ", calendarNames)).queue(); + } + else { + event.reply("Not implemented yet").queue(); + } } @ICommand(name = "update", description = "Update configuration of a calendar", permissions = {Permission.ADMINISTRATOR}) diff --git a/src/main/java/com/softawii/capivara/repository/CalendarRepository.java b/src/main/java/com/softawii/capivara/repository/CalendarRepository.java index bfad8db..38ddf85 100644 --- a/src/main/java/com/softawii/capivara/repository/CalendarRepository.java +++ b/src/main/java/com/softawii/capivara/repository/CalendarRepository.java @@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface CalendarRepository extends JpaRepository { + Calendar findByCalendarKey(Calendar.CalendarKey calendarKey); + List findAllByCalendarKeyGuildId(Long guildId); } diff --git a/src/main/java/com/softawii/capivara/services/CalendarService.java b/src/main/java/com/softawii/capivara/services/CalendarService.java index 5f18e61..b60ab1b 100644 --- a/src/main/java/com/softawii/capivara/services/CalendarService.java +++ b/src/main/java/com/softawii/capivara/services/CalendarService.java @@ -7,6 +7,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import java.util.List; import java.util.Optional; @Service @@ -49,4 +50,12 @@ public void delete(Calendar.CalendarKey calendarKey) { public void exists(Calendar.CalendarKey calendarKey) { repository.existsById(calendarKey); } + + public Calendar findByNameAndGuildId(String name, Long guildId) { + return repository.findByCalendarKey(new Calendar.CalendarKey(guildId, name)); + } + + public List getCalendarNames(Long guildId) { + return repository.findAllByCalendarKeyGuildId(guildId).stream().map(Calendar::getCalendarKey).map(Calendar.CalendarKey::getCalendarName).toList(); + } } From cd9049094603ea9af99fb19d65f5bf51c55212b2 Mon Sep 17 00:00:00 2001 From: yaansz Date: Thu, 2 Nov 2023 00:11:49 -0300 Subject: [PATCH 9/9] feat: fixing scheduler --- .../com/softawii/capivara/TestGoogle.java | 100 ------------------ .../entity/internal/CalendarSubscriber.java | 37 +++++-- .../repository/CalendarRepository.java | 4 + .../threads/CalendarSubscriptionManager.java | 8 +- 4 files changed, 35 insertions(+), 114 deletions(-) delete mode 100644 src/main/java/com/softawii/capivara/TestGoogle.java diff --git a/src/main/java/com/softawii/capivara/TestGoogle.java b/src/main/java/com/softawii/capivara/TestGoogle.java deleted file mode 100644 index e1036ca..0000000 --- a/src/main/java/com/softawii/capivara/TestGoogle.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.softawii.capivara; - -import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; -import com.google.api.client.googleapis.json.GoogleJsonResponseException; -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.gson.GsonFactory; -import com.google.api.client.util.DateTime; -import com.google.api.services.calendar.Calendar; -import com.google.api.services.calendar.model.Event; -import com.google.api.services.calendar.model.EventDateTime; -import com.google.api.services.calendar.model.Events; -import com.softawii.capivara.utils.CalendarUtil; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.time.Instant; -import java.util.List; - -public class TestGoogle { - public static void main(String[] args) throws GeneralSecurityException, IOException { - String calendarId = ""; - String googleApiKey = System.getenv("googleApiKey"); - NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); - GsonFactory factory = GsonFactory.getDefaultInstance(); - - Calendar service = new Calendar.Builder(httpTransport, factory, null) - .setApplicationName("Capivara") - .setGoogleClientRequestInitializer(request -> request.set("key", googleApiKey)) - .build(); - - DateTime minDateTime = CalendarUtil.getMinDateTime(0); - DateTime maxDateTime = CalendarUtil.getMaxDateTime(0); - Instant now = Instant.now(); - System.out.printf("minDateTime: %s :: maxDateTime: %s%n", minDateTime, maxDateTime); - String pageToken = null; - System.out.println("Events ---------------------"); - do { - try { - Events events = service - .events() - .list(calendarId) - .setSingleEvents(true) - .setTimeMin(minDateTime) - .setTimeMax(maxDateTime) - .setOrderBy("startTime") - .setPageToken(pageToken) - .execute(); - List items = events.getItems(); - for (Event event : items) { - String title = event.getSummary(); - String description = event.getDescription(); - - EventDateTime eventStart = event.getStart(); - EventDateTime eventEnd = event.getEnd(); - DateTime dateStart; - DateTime dateEnd; - boolean isAllDayEvent = eventStart.getDate() != null && eventEnd.getDate() != null; - if (isAllDayEvent) { - dateStart = eventStart.getDate(); - dateEnd = eventEnd.getDate(); - } else { - dateStart = eventStart.getDateTime(); - dateEnd = eventEnd.getDateTime(); - } - boolean alreadyStarted = dateStart.getValue() <= now.toEpochMilli(); - boolean alreadyEnded = dateEnd.getValue() <= now.toEpochMilli(); - System.out.printf(""" - ID: %s - Title: %s - Description: %s - Start: %s - End: %s - All day event: %s - Already started: %s - Already ended: %s - ------------------------- - """, - event.getId(), - title, - description, - dateStart, - dateEnd, - isAllDayEvent ? "yes" : "no", - alreadyStarted ? "yes" : "no", - alreadyEnded ? "yes" : "no" - ); - } - pageToken = events.getNextPageToken(); - } catch (GoogleJsonResponseException e) { - System.out.println(e.getDetails()); - if ((e.getStatusCode() == 403 && !e.getStatusMessage().equals("Forbidden")) || e.getStatusCode() == 429) { - System.out.println("rate limited"); - } else { - e.printStackTrace(); - } - } - } while (pageToken != null); - } - -} diff --git a/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java b/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java index 1db0a0f..348b193 100644 --- a/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java +++ b/src/main/java/com/softawii/capivara/entity/internal/CalendarSubscriber.java @@ -27,15 +27,21 @@ public class CalendarSubscriber { private class EventWrapper extends TimerTask { private static final Logger LOGGER = LogManager.getLogger(EventWrapper.class); - private final Event event; - private final Timer timer; + private Event event; + private Timer timer; public EventWrapper(Event event) { this.event = event; - this.timer = new Timer("EventWrapper-" + event.getId()); schedule(); } + public boolean startChanged(Event event) { + return !this.event.getStart().equals(event.getStart()); + } + public void setEvent(Event event) { + this.event = event; + } + private void schedule() { EventDateTime eventStart = this.event.getStart(); EventDateTime eventEnd = this.event.getEnd(); @@ -49,13 +55,16 @@ private void schedule() { } Date scheduled = new Date(dateStart.getValue()); + this.timer = new Timer("EventWrapper-" + event.getId()); this.timer.schedule(this, scheduled); LOGGER.info("Event scheduled: " + this.event.getSummary() + ", date: " + scheduled); } public void purge() { - this.timer.cancel(); - this.timer.purge(); + if(this.timer != null) { + this.timer.cancel(); + this.timer.purge(); + } } @Override @@ -92,13 +101,13 @@ public CalendarSubscriber(String googleCalendarId, GoogleCalendarService googleC this.update(); } - public void subscribe(Calendar consumer) { + public synchronized void subscribe(Calendar consumer) { if (!this.consumers.contains(consumer)) { this.consumers.add(consumer); } } - public void unsubscribe(Calendar consumer) { + public synchronized void unsubscribe(Calendar consumer) { this.consumers.remove(consumer); } @@ -106,7 +115,7 @@ public boolean isThereAnyConsumer() { return !this.consumers.isEmpty(); } - public void purge() { + public synchronized void purge() { this.events.values().forEach(EventWrapper::purge); this.events.clear(); } @@ -133,9 +142,15 @@ private void checkForUpdates(List events) { this.events.put(eventId, new EventWrapper(event)); LOGGER.info("Event added: " + event.getSummary()); } else { - eventWrapper.purge(); - this.events.put(eventId, new EventWrapper(event)); - LOGGER.info("Event updated: " + event.getSummary()); + if(eventWrapper.startChanged(event)) { + LOGGER.info("Event updated: " + event.getSummary()); + eventWrapper.purge(); + this.events.put(eventId, new EventWrapper(event)); + LOGGER.info("Event re-added: " + event.getSummary()); + } else { + eventWrapper.setEvent(event); + LOGGER.info("Event updated (not change in hour): " + event.getSummary()); + } } } } diff --git a/src/main/java/com/softawii/capivara/repository/CalendarRepository.java b/src/main/java/com/softawii/capivara/repository/CalendarRepository.java index 38ddf85..000eb5f 100644 --- a/src/main/java/com/softawii/capivara/repository/CalendarRepository.java +++ b/src/main/java/com/softawii/capivara/repository/CalendarRepository.java @@ -2,7 +2,11 @@ import com.softawii.capivara.entity.Calendar; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository public interface CalendarRepository extends JpaRepository { Calendar findByCalendarKey(Calendar.CalendarKey calendarKey); List findAllByCalendarKeyGuildId(Long guildId); diff --git a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionManager.java b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionManager.java index 18199bf..491dea4 100644 --- a/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionManager.java +++ b/src/main/java/com/softawii/capivara/threads/CalendarSubscriptionManager.java @@ -13,7 +13,9 @@ import java.util.HashMap; import java.util.Map; -import java.util.concurrent.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; @Component public class CalendarSubscriptionManager { @@ -53,7 +55,7 @@ private void startupSubscriber() { } } - public void subscribe(Calendar calendar) { + public synchronized void subscribe(Calendar calendar) { CalendarSubscriber subscriber = this.subscribers.get(calendar.getGoogleCalendarId()); if (subscriber == null) { subscriber = new CalendarSubscriber(calendar.getGoogleCalendarId(), this.googleCalendarService, jda); @@ -63,7 +65,7 @@ public void subscribe(Calendar calendar) { subscriber.subscribe(calendar); } - public void unsubscribe(Calendar calendar) { + public synchronized void unsubscribe(Calendar calendar) { CalendarSubscriber subscriber = this.subscribers.get(calendar.getGoogleCalendarId()); if (subscriber == null) { throw new RuntimeException("There is no subscription available");