diff --git a/PP.md b/PP.md index a82deaac35..ae5cce3537 100644 --- a/PP.md +++ b/PP.md @@ -36,7 +36,7 @@ In certain circumstances, you have the following data protection rights: ## Usage of Data -**TJ-Bot** may use stored data, as defined below, to offer different features and services. No usage of data outside of the aformentioned cases will happen and the data is not shared with any third-party site or service. +**TJ-Bot** may use stored data, as defined below, to offer different features and services. No usage of data outside of the aforementioned cases will happen and the data is not shared with any third-party site or service. ### Databases @@ -55,8 +55,11 @@ For example, **TJ-Bot** may associate your `user_id` with a `message_id` and a ` **TJ-Bot** may further store data that you explicitly provided for **TJ-Bot** to offer its services. For example the reason of a moderative action when using its moderation commands. +Furthermore, upon utilization of our help service, `user_id`s and `channel_id`s are stored to track when/how many questions a user asks. The data may be stored for up to **30** days. + The stored data is not linked to any information that is personally identifiable. + No other personal information outside of the above mentioned one will be stored. In particular, **TJ-Bot** does not store the content of sent messages. ### Log Files diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java index 0ce308e748..1ea24f590b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -61,7 +61,7 @@ private Features() { ModerationActionsStore actionsStore = new ModerationActionsStore(database); ModAuditLogWriter modAuditLogWriter = new ModAuditLogWriter(config); ScamHistoryStore scamHistoryStore = new ScamHistoryStore(database); - HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config); + HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database); // NOTE The system can add special system relevant commands also by itself, // hence this list may not necessarily represent the full list of all commands actually @@ -75,6 +75,7 @@ private Features() { features.add(new RemindRoutine(database)); features.add(new ScamHistoryPurgeRoutine(scamHistoryStore)); features.add(new BotMessageCleanup(config)); + features.add(new HelpThreadMetadataPurger(database)); features.add(new HelpThreadActivityUpdater(helpSystemHelper)); features .add(new AutoPruneHelperRoutine(config, helpSystemHelper, modAuditLogWriter, database)); @@ -88,6 +89,7 @@ private Features() { // Event receivers features.add(new RejoinModerationRoleListener(actionsStore, config)); + features.add(new OnGuildLeaveCloseThreadListener(database)); // Slash commands features.add(new LogLevelCommand()); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java index c0acedb06c..7e30e86b09 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java @@ -46,7 +46,6 @@ public final class AskCommand extends SlashCommandAdapter { private static final String TITLE_OPTION = "title"; private static final String CATEGORY_OPTION = "category"; - private final HelpSystemHelper helper; /** @@ -120,6 +119,7 @@ private boolean handleIsValidTitle(@NotNull CharSequence title, @NotNull IReplyC private @NotNull RestAction handleEvent(@NotNull InteractionHook eventHook, @NotNull ThreadChannel threadChannel, @NotNull Member author, @NotNull String title, @NotNull String category, @NotNull Guild guild) { + helper.writeHelpThreadToDatabase(author, threadChannel); return sendInitialMessage(guild, threadChannel, author, title, category) .flatMap(any -> notifyUser(eventHook, threadChannel)) .flatMap(any -> helper.sendExplanationMessage(threadChannel)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java index f5dfa9733b..083a93c228 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java @@ -11,6 +11,9 @@ import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.HelpSystemConfig; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.HelpThreads; +import org.togetherjava.tjbot.db.generated.tables.records.HelpThreadsRecord; import java.awt.Color; import java.io.InputStream; @@ -47,14 +50,16 @@ public final class HelpSystemHelper { private final Predicate isStagingChannelName; private final String stagingChannelPattern; private final String categoryRoleSuffix; + private final Database database; /** * Creates a new instance. * * @param config the config to use */ - public HelpSystemHelper(@NotNull Config config) { + public HelpSystemHelper(@NotNull Config config, @NotNull Database database) { HelpSystemConfig helpConfig = config.getHelpSystem(); + this.database = database; overviewChannelPattern = helpConfig.getOverviewChannelPattern(); isOverviewChannelName = Pattern.compile(overviewChannelPattern).asMatchPredicate(); @@ -99,6 +104,18 @@ RestAction sendExplanationMessage(@NotNull MessageChannel threadChannel return action.setEmbeds(embeds); } + public void writeHelpThreadToDatabase(Member author, ThreadChannel threadChannel) { + database.write(content -> { + HelpThreadsRecord helpThreadsRecord = content.newRecord(HelpThreads.HELP_THREADS) + .setAuthorId(author.getIdLong()) + .setChannelId(threadChannel.getIdLong()) + .setCreatedAt(threadChannel.getTimeCreated().toInstant()); + if (helpThreadsRecord.update() == 0) { + helpThreadsRecord.insert(); + } + }); + } + private static @NotNull MessageEmbed embedWith(@NotNull CharSequence message) { return embedWith(message, null); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadMetadataPurger.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadMetadataPurger.java new file mode 100644 index 0000000000..94333bf044 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadMetadataPurger.java @@ -0,0 +1,43 @@ +package org.togetherjava.tjbot.commands.help; + +import net.dv8tion.jda.api.JDA; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.Routine; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.HelpThreads; +import java.time.Instant; +import java.time.Period; +import java.util.concurrent.TimeUnit; + +/** + * Purge Routine to get rid of old thread creations in the database. + */ +public class HelpThreadMetadataPurger implements Routine { + private final Database database; + private static final Logger logger = LoggerFactory.getLogger(HelpThreadMetadataPurger.class); + private static final Period DELETE_MESSAGE_RECORDS_AFTER = Period.ofDays(30); + + public HelpThreadMetadataPurger(@NotNull Database database) { + this.database = database; + } + + @Override + public @NotNull Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_RATE, 0, 4, TimeUnit.HOURS); + } + + @Override + public void runRoutine(@NotNull JDA jda) { + int recordsDeleted = + database.writeAndProvide(content -> content.deleteFrom(HelpThreads.HELP_THREADS)) + .where(HelpThreads.HELP_THREADS.CREATED_AT + .lessOrEqual(Instant.now().minus(DELETE_MESSAGE_RECORDS_AFTER))) + .execute(); + if (recordsDeleted > 0) { + logger.debug("{} old thread channels deleted because they are older than {}.", + recordsDeleted, DELETE_MESSAGE_RECORDS_AFTER); + } + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java index 3652ef0a4a..22a241fbe3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java @@ -157,6 +157,7 @@ private Optional getLastHelpThreadIfOnCooldown(long userId) { private @NotNull RestAction handleEvent(@NotNull ThreadChannel threadChannel, @NotNull Message message, @NotNull String title) { Member author = message.getMember(); + helper.writeHelpThreadToDatabase(author, threadChannel); userIdToLastHelpThread.put(author.getIdLong(), new HelpThread(threadChannel.getIdLong(), author.getIdLong(), Instant.now())); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java new file mode 100644 index 0000000000..0fa1e72a74 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java @@ -0,0 +1,62 @@ +package org.togetherjava.tjbot.commands.help; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.ThreadChannel; +import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.EventReceiver; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.HelpThreads; + +import javax.annotation.Nonnull; +import java.util.*; + +/** + * Remove all thread channels associated to a user when they leave the guild. + */ +public class OnGuildLeaveCloseThreadListener extends ListenerAdapter implements EventReceiver { + private static final Logger logger = + LoggerFactory.getLogger(OnGuildLeaveCloseThreadListener.class); + private final Database database; + + public OnGuildLeaveCloseThreadListener(@NotNull Database database) { + this.database = database; + } + + @Override + public void onGuildMemberRemove(@Nonnull GuildMemberRemoveEvent leaveEvent) { + Set channelIds = getThreadsCreatedByLeaver(leaveEvent.getUser().getIdLong()); + for (long channelId : channelIds) { + closeThread(channelId, leaveEvent); + } + } + + public Set getThreadsCreatedByLeaver(long leaverId) { + return new HashSet<>(database + .readTransaction(context -> context.select(HelpThreads.HELP_THREADS.CHANNEL_ID)) + .from(HelpThreads.HELP_THREADS) + .where(HelpThreads.HELP_THREADS.AUTHOR_ID.eq(leaverId)) + .fetch(databaseMapper -> databaseMapper.getValue(HelpThreads.HELP_THREADS.CHANNEL_ID))); + } + + public void closeThread(long channelId, @NotNull GuildMemberRemoveEvent leaveEvent) { + ThreadChannel threadChannel = leaveEvent.getGuild().getThreadChannelById(channelId); + if (threadChannel == null) { + logger.warn( + "Attempted to archive thread id: '{}' but could not find thread in guild: '{}'.", + channelId, leaveEvent.getGuild().getName()); + return; + } + MessageEmbed embed = new EmbedBuilder().setTitle("OP left") + .setDescription("Closing thread...") + .setColor(HelpSystemHelper.AMBIENT_COLOR) + .build(); + threadChannel.sendMessageEmbeds(embed) + .flatMap(any -> threadChannel.getManager().setArchived(true)) + .queue(); + } +} diff --git a/application/src/main/resources/db/V11__Add_Help_Thread_Metadata.sql b/application/src/main/resources/db/V11__Add_Help_Thread_Metadata.sql new file mode 100644 index 0000000000..bb89e5a607 --- /dev/null +++ b/application/src/main/resources/db/V11__Add_Help_Thread_Metadata.sql @@ -0,0 +1,6 @@ +CREATE TABLE help_threads +( + channel_id BIGINT NOT NULL PRIMARY KEY, + author_id BIGINT NOT NULL, + created_at TIMESTAMP NOT NULL +) \ No newline at end of file