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
4 changes: 2 additions & 2 deletions application/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand All @@ -51,6 +65,7 @@ public class FileSharingMessageListener extends MessageReceiverAdapter {

private final Predicate<String> isStagingChannelName;
private final Predicate<String> isOverviewChannelName;
private final Predicate<String> isSoftModRole;

/**
* Creates a new instance.
Expand All @@ -66,6 +81,7 @@ public FileSharingMessageListener(Config config) {
.asMatchPredicate();
isOverviewChannelName = Pattern.compile(config.getHelpSystem().getOverviewChannelPattern())
.asMatchPredicate();
isSoftModRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate();
}

@Override
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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<String> 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<String> 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<Button> buttons = message.getButtons();
event.editComponents(ActionRow.of(buttons.stream().map(Button::asDisabled).toList()))
.queue();

String gistId = args.get(1);
deleteGist(gistId);
}

@Override
public void onSelectMenuSelection(SelectMenuInteractionEvent event, List<String> args) {
throw new UnsupportedOperationException("Not used");
}

@Override
public void onModalSubmitted(ModalInteractionEvent event, List<String> args) {
throw new UnsupportedOperationException("Not used");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,23 @@ final class GistResponse {
@JsonProperty("html_url")
private String htmlUrl;

@JsonProperty("id")
private String gistId;

public String getGistId() {
return gistId;
}

public void setGistId(String gistId) {
this.gistId = gistId;
}

public String getHtmlUrl() {
return htmlUrl;
}

public void setHtmlUrl(String htmlUrl) {
this.htmlUrl = htmlUrl;
}

}