Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "TJ-Bot",
"image": "mcr.microsoft.com/devcontainers/universal:2",
"hostRequirements": {
"cpus": 1,
"memory": "1gb",
"storage": "2gb"
},
"features": {
"ghcr.io/devcontainers/features/java:1": {
"version": "18.0.2.1-tem",
"jdkDistro": "tem",
"installGradle": true
},
"ghcr.io/devcontainers-contrib/features/pre-commit:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"vscjava.vscode-java-pack",
"vscjava.vscode-gradle",
"alexcvzz.vscode-sqlite",
"richardwillis.vscode-spotless-gradle"
],
"settings": {
"[java]": {
"spotlessGradle.format.enable": true,
"editor.defaultFormatter": "richardwillis.vscode-spotless-gradle"
}
}
}
},
"postCreateCommand": {
"config": "cp application/config.json.template application/config.json"
}
}
13 changes: 13 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: together-java
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
8 changes: 6 additions & 2 deletions application/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
buildscript {
dependencies {
classpath 'org.xerial:sqlite-jdbc:3.39.2.0'
classpath 'org.xerial:sqlite-jdbc:3.40.0.0'
}
}

Expand Down Expand Up @@ -57,6 +57,8 @@ dependencies {

implementation 'io.mikael:urlbuilder:2.0.9'

implementation 'org.jsoup:jsoup:1.15.3'

implementation 'org.scilab.forge:jlatexmath:1.0.7'
implementation 'org.scilab.forge:jlatexmath-font-greek:1.0.7'
implementation 'org.scilab.forge:jlatexmath-font-cyrillic:1.0.7'
Expand All @@ -68,9 +70,11 @@ dependencies {

implementation 'com.github.freva:ascii-table:1.8.0'

implementation 'io.github.url-detector:url-detector:0.1.23'

implementation 'com.github.ben-manes.caffeine:caffeine:3.1.1'

testImplementation 'org.mockito:mockito-core:4.8.0'
testImplementation 'org.mockito:mockito-core:4.9.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.togetherjava.tjbot.commands.basic.RoleSelectCommand;
import org.togetherjava.tjbot.commands.basic.SuggestionsUpDownVoter;
import org.togetherjava.tjbot.commands.basic.VcActivityCommand;
import org.togetherjava.tjbot.commands.bookmarks.*;
import org.togetherjava.tjbot.commands.code.CodeMessageAutoDetection;
import org.togetherjava.tjbot.commands.code.CodeMessageHandler;
import org.togetherjava.tjbot.commands.code.CodeMessageManualDetection;
Expand Down Expand Up @@ -67,6 +68,7 @@ private Features() {
*/
public static Collection<Feature> createFeatures(JDA jda, Database database, Config config) {
TagSystem tagSystem = new TagSystem(database);
BookmarksSystem bookmarksSystem = new BookmarksSystem(config, database);
ModerationActionsStore actionsStore = new ModerationActionsStore(database);
ModAuditLogWriter modAuditLogWriter = new ModAuditLogWriter(config);
ScamHistoryStore scamHistoryStore = new ScamHistoryStore(database);
Expand All @@ -90,6 +92,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features
.add(new AutoPruneHelperRoutine(config, helpSystemHelper, modAuditLogWriter, database));
features.add(new HelpThreadAutoArchiver(helpSystemHelper));
features.add(new LeftoverBookmarksCleanupRoutine(bookmarksSystem));

// Message receivers
features.add(new TopHelpersMessageListener(database, config));
Expand All @@ -107,6 +110,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(new RejoinModerationRoleListener(actionsStore, config));
features.add(new OnGuildLeaveCloseThreadListener(database));
features.add(new UserBannedDeleteRecentThreadsListener(database));
features.add(new LeftoverBookmarksListener(bookmarksSystem));

// Message context commands

Expand Down Expand Up @@ -139,6 +143,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(new ModMailCommand(jda, config));
features.add(new HelpThreadCommand(config, helpSystemHelper));
features.add(new ReportCommand(config));
features.add(new BookmarksCommand(bookmarksSystem));

// Mixtures
features.add(new HelpThreadOverviewUpdater(config, helpSystemHelper));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package org.togetherjava.tjbot.commands.bookmarks;

import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.togetherjava.tjbot.commands.CommandVisibility;
import org.togetherjava.tjbot.commands.SlashCommandAdapter;

import java.util.List;
import java.util.Objects;

/**
* The bookmarks command is used for managing and viewing bookmarks. A bookmark is a link to a help
* thread that can have a note so you can easily remember why you bookmarked a help thread. Writing
* to the database and showing the list/remove messages is not done by this class, that is handled
* by the {@link BookmarksSystem}. This class only checks if you are able to add a bookmark in the
* current channel and tells the {@link BookmarksSystem} to do the rest.
* <p>
* Usage:
*
* <pre>
* /bookmarks add [note]
* /bookmarks list
* /bookmarks remove
* </pre>
*/
public final class BookmarksCommand extends SlashCommandAdapter {

private static final Logger logger = LoggerFactory.getLogger(BookmarksCommand.class);

public static final String COMMAND_NAME = "bookmarks";
public static final String SUBCOMMAND_ADD = "add";
public static final String SUBCOMMAND_LIST = "list";
public static final String SUBCOMMAND_REMOVE = "remove";
public static final String ADD_BOOKMARK_NOTE_OPTION = "note";

private static final MessageEmbed NOT_A_HELP_THREAD_EMBED =
BookmarksSystem.createFailureEmbed("You can only bookmark help threads.");

private static final MessageEmbed ALREADY_BOOKMARKED_EMBED =
BookmarksSystem.createFailureEmbed("You have already bookmarked this channel.");

private static final MessageEmbed BOOKMARK_ADDED_EMBED =
BookmarksSystem.createSuccessEmbed("Your bookmark was added.");

private static final MessageEmbed BOOKMARK_LIMIT_USER_EMBED = BookmarksSystem
.createFailureEmbed(
"You have exceeded your bookmarks limit of `%d`. Please delete some of your other bookmarks."
.formatted(BookmarksSystem.MAX_BOOKMARK_COUNT_PER_USER));

private static final MessageEmbed BOOKMARK_LIMIT_TOTAL_EMBED = BookmarksSystem
.createWarningEmbed(
"""
You cannot add a bookmark right now because the total amount of bookmarks has exceeded its limit.
Please wait a bit until some of them have been deleted or contact a moderator.
Sorry for the inconvenience.
""");

private final BookmarksSystem bookmarksSystem;
private final BookmarksListRemoveHandler listRemoveHandler;

/**
* Creates a new instance and registers every sub command.
*
* @param bookmarksSystem The {@link BookmarksSystem} to request pagination and manage bookmarks
*/
public BookmarksCommand(BookmarksSystem bookmarksSystem) {
super(COMMAND_NAME, "Bookmark help threads so that you can easily look them up again",
CommandVisibility.GLOBAL);
this.bookmarksSystem = bookmarksSystem;
listRemoveHandler =
new BookmarksListRemoveHandler(bookmarksSystem, this::generateComponentId);

OptionData addNoteOption = new OptionData(OptionType.STRING, ADD_BOOKMARK_NOTE_OPTION,
"Your personal comment on this bookmark")
.setMaxLength(BookmarksSystem.MAX_NOTE_LENGTH)
.setRequired(false);

SubcommandData addSubCommand = new SubcommandData(SUBCOMMAND_ADD,
"Bookmark this help thread, so that you can easily look it up again")
.addOptions(addNoteOption);

SubcommandData listSubCommand =
new SubcommandData(SUBCOMMAND_LIST, "List all of your bookmarks");

SubcommandData removeSubCommand =
new SubcommandData(SUBCOMMAND_REMOVE, "Remove some of your bookmarks");

getData().addSubcommands(addSubCommand, listSubCommand, removeSubCommand);
}

@Override
public void onSlashCommand(SlashCommandInteractionEvent event) {
String subCommandName = Objects.requireNonNull(event.getSubcommandName());

switch (subCommandName) {
case SUBCOMMAND_ADD -> addBookmark(event);
case SUBCOMMAND_LIST -> listRemoveHandler.handleListRequest(event);
case SUBCOMMAND_REMOVE -> listRemoveHandler.handleRemoveRequest(event);
default -> throw new IllegalArgumentException("Unknown subcommand");
}
}

@Override
public void onButtonClick(ButtonInteractionEvent event, List<String> args) {
listRemoveHandler.onButtonClick(event, args);
}

@Override
public void onSelectMenuSelection(SelectMenuInteractionEvent event, List<String> args) {
listRemoveHandler.onSelectMenuSelection(event, args);
}

private void addBookmark(SlashCommandInteractionEvent event) {
long userID = event.getUser().getIdLong();
long channelID = event.getChannel().getIdLong();
String note = event.getOption(ADD_BOOKMARK_NOTE_OPTION, OptionMapping::getAsString);

if (!handleCanAddBookmark(event)) {
return;
}

bookmarksSystem.addBookmark(userID, channelID, note);

sendResponse(event, BOOKMARK_ADDED_EMBED);
}

private boolean handleCanAddBookmark(SlashCommandInteractionEvent event) {
MessageChannelUnion channel = event.getChannel();
long channelID = channel.getIdLong();
long userID = event.getUser().getIdLong();

if (!bookmarksSystem.isHelpThread(channel)) {
sendResponse(event, NOT_A_HELP_THREAD_EMBED);
return false;
}

if (bookmarksSystem.didUserBookmarkChannel(userID, channelID)) {
sendResponse(event, ALREADY_BOOKMARKED_EMBED);
return false;
}

long bookmarkCountTotal = bookmarksSystem.getTotalBookmarkCount();
if (bookmarkCountTotal == BookmarksSystem.WARN_BOOKMARK_COUNT_TOTAL) {
logger.warn("""
The bookmark limit will be reached soon (`{}/{}` bookmarks)!
If the limit is reached no new bookmarks can be added!
Please delete some bookmarks!
""", BookmarksSystem.WARN_BOOKMARK_COUNT_TOTAL,
BookmarksSystem.MAX_BOOKMARK_COUNT_TOTAL);
}
if (bookmarkCountTotal == BookmarksSystem.MAX_BOOKMARK_COUNT_TOTAL) {
logger.error("""
The bookmark limit of `{}` has been reached!
No new bookmarks can be added anymore!
Please delete some bookmarks!
""", BookmarksSystem.MAX_BOOKMARK_COUNT_TOTAL);
}
if (bookmarkCountTotal > BookmarksSystem.MAX_BOOKMARK_COUNT_TOTAL) {
sendResponse(event, BOOKMARK_LIMIT_TOTAL_EMBED);
return false;
}

long bookmarkCountUser = bookmarksSystem.getUserBookmarkCount(userID);
if (bookmarkCountUser >= BookmarksSystem.MAX_BOOKMARK_COUNT_PER_USER) {
sendResponse(event, BOOKMARK_LIMIT_USER_EMBED);
return false;
}

return true;
}

private void sendResponse(SlashCommandInteractionEvent event, MessageEmbed embed) {
event.replyEmbeds(embed).setEphemeral(true).queue();
}
}
Loading