diff --git a/application/config.json.template b/application/config.json.template index 0c776ed38c..780e844ed1 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -8,8 +8,8 @@ "modMailChannelPattern": "modmail", "mutedRolePattern": "Muted", "heavyModerationRolePattern": "Moderator", - "softModerationRolePattern": "Moderator|Staff Assistant", - "tagManageRolePattern": "Moderator|Staff Assistant|Top Helpers .+", + "softModerationRolePattern": "Moderator|Community Ambassador", + "tagManageRolePattern": "Moderator|Community Ambassador|Top Helpers .+", "suggestions": { "channelPattern": "tj_suggestions", "upVoteEmoteName": "peepo_yes", diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java index f4c0316ac5..67e29900f7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java @@ -2,16 +2,27 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.buttons.Button; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; +import org.togetherjava.tjbot.commands.UserInteractionType; +import org.togetherjava.tjbot.commands.UserInteractor; +import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator; +import org.togetherjava.tjbot.commands.componentids.ComponentIdInteractor; import org.togetherjava.tjbot.config.Config; import java.io.IOException; @@ -37,11 +48,14 @@ * contains a file with the given extension in the * {@link FileSharingMessageListener#extensionFilter}. */ -public class FileSharingMessageListener extends MessageReceiverAdapter { +public class FileSharingMessageListener extends MessageReceiverAdapter implements UserInteractor { private static final Logger LOGGER = LoggerFactory.getLogger(FileSharingMessageListener.class); private static final ObjectMapper JSON = new ObjectMapper(); + private final ComponentIdInteractor componentIdInteractor = + new ComponentIdInteractor(getInteractionType(), getName()); + private static final String SHARE_API = "https://api.github.com/gists"; private static final HttpClient CLIENT = HttpClient.newHttpClient(); @@ -51,6 +65,7 @@ public class FileSharingMessageListener extends MessageReceiverAdapter { private final Predicate isStagingChannelName; private final Predicate isOverviewChannelName; + private final Predicate isSoftModRole; /** * Creates a new instance. @@ -66,6 +81,7 @@ public FileSharingMessageListener(Config config) { .asMatchPredicate(); isOverviewChannelName = Pattern.compile(config.getHelpSystem().getOverviewChannelPattern()) .asMatchPredicate(); + isSoftModRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate(); } @Override @@ -128,8 +144,10 @@ private void processAttachments(MessageReceivedEvent event, GistFiles files = new GistFiles(nameToFile); GistRequest request = new GistRequest(event.getAuthor().getName(), false, files); - String url = uploadToGist(request); - sendResponse(event, url); + GistResponse response = uploadToGist(request); + String url = response.getHtmlUrl(); + String gistId = response.getGistId(); + sendResponse(event, url, gistId); } private String readAttachment(InputStream stream) { @@ -160,7 +178,7 @@ private String getNameOf(Message.Attachment attachment) { return fileName; } - private String uploadToGist(GistRequest jsonRequest) { + private GistResponse uploadToGist(GistRequest jsonRequest) { String body; try { body = JSON.writeValueAsString(jsonRequest); @@ -203,15 +221,21 @@ private String uploadToGist(GistRequest jsonRequest) { throw new IllegalStateException( "Attempting to upload file to gist, but unable to parse its JSON response.", e); } - return gistResponse.getHtmlUrl(); + return gistResponse; } - private void sendResponse(MessageReceivedEvent event, String url) { + private void sendResponse(MessageReceivedEvent event, String url, String gistId) { Message message = event.getMessage(); String messageContent = "I uploaded your attachments as **gist**. That way, they are easier to read for everyone, especially mobile users 👍"; - message.reply(messageContent).setActionRow(Button.link(url, "gist")).queue(); + Button gist = Button.link(url, "gist"); + + Button delete = Button.danger( + componentIdInteractor.generateComponentId(message.getAuthor().getId(), gistId), + Emoji.fromUnicode("🗑️")); + + message.reply(messageContent).setActionRow(gist, delete).queue(); } private boolean isHelpThread(MessageReceivedEvent event) { @@ -224,4 +248,77 @@ private boolean isHelpThread(MessageReceivedEvent event) { return isStagingChannelName.test(rootChannelName) || isOverviewChannelName.test(rootChannelName); } + + private void deleteGist(String gistId) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(SHARE_API + "/" + gistId)) + .header("Accept", "application/json") + .header("Authorization", "token " + gistApiKey) + .DELETE() + .build(); + + HttpResponse apiResponse; + try { + apiResponse = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException( + "Attempting to delete a gist, but the request got interrupted.", e); + } + + int status = apiResponse.statusCode(); + if (status == 404) { + String responseBody = apiResponse.body(); + LOGGER.warn("Gist API unexpected response while deleting gist: {}.", responseBody); + } + } + + @Override + public String getName() { + return "filesharing"; + } + + @Override + public void acceptComponentIdGenerator(ComponentIdGenerator generator) { + componentIdInteractor.acceptComponentIdGenerator(generator); + } + + @Override + public UserInteractionType getInteractionType() { + return UserInteractionType.OTHER; + } + + @Override + public void onButtonClick(ButtonInteractionEvent event, List args) { + Member interactionUser = event.getMember(); + String gistAuthorId = args.get(0); + boolean hasSoftModPermissions = + interactionUser.getRoles().stream().map(Role::getName).anyMatch(isSoftModRole); + + if (!gistAuthorId.equals(interactionUser.getId()) && !hasSoftModPermissions) { + event.reply("You do not have permission for this action.").setEphemeral(true).queue(); + return; + } + + Message message = event.getMessage(); + List