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
2 changes: 2 additions & 0 deletions application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ dependencies {
implementation 'io.github.url-detector:url-detector:0.1.23'

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

implementation 'org.kohsuke:github-api:1.314'

testImplementation 'org.mockito:mockito-core:5.1.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package org.togetherjava.tjbot.features.filesharing;

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.kohsuke.github.GHGist;
import org.kohsuke.github.GHGistBuilder;
import org.kohsuke.github.GitHubBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -28,18 +27,11 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.regex.Pattern;

Expand All @@ -48,17 +40,13 @@
* contains a file with the given extension in the
* {@link FileSharingMessageListener#extensionFilter}.
*/
public class FileSharingMessageListener extends MessageReceiverAdapter implements UserInteractor {

private static final Logger LOGGER = LoggerFactory.getLogger(FileSharingMessageListener.class);
private static final ObjectMapper JSON = new ObjectMapper();
public final class FileSharingMessageListener extends MessageReceiverAdapter
implements UserInteractor {
private static final Logger logger = LoggerFactory.getLogger(FileSharingMessageListener.class);

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();

private final String gistApiKey;
private final Set<String> extensionFilter = Set.of("txt", "java", "gradle", "xml", "kt", "json",
"fxml", "css", "c", "h", "cpp", "py", "yml");
Expand All @@ -82,11 +70,8 @@ public FileSharingMessageListener(Config config) {
@Override
public void onMessageReceived(MessageReceivedEvent event) {
User author = event.getAuthor();
if (author.isBot() || event.isWebhookMessage()) {
return;
}

if (!isHelpThread(event)) {
if (author.isBot() || event.isWebhookMessage() || !isHelpThread(event)) {
return;
}

Expand All @@ -104,13 +89,36 @@ public void onMessageReceived(MessageReceivedEvent event) {
try {
processAttachments(event, attachments);
} catch (Exception e) {
LOGGER.error(
logger.error(
"Unknown error while processing attachments. Channel: {}, Author: {}, Message ID: {}.",
event.getChannel().getName(), author.getId(), event.getMessageId(), e);
}
});
}

@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;
}

String gistId = args.get(1);

try {
new GitHubBuilder().withOAuthToken(gistApiKey).build().getGist(gistId).delete();

event.getMessage().delete().queue();
} catch (IOException e) {
logger.warn("Failed to delete gist with id {}", gistId, e);
}
}

private boolean isAttachmentRelevant(Message.Attachment attachment) {
String extension = attachment.getFileExtension();
if (extension == null) {
Expand All @@ -120,29 +128,28 @@ private boolean isAttachmentRelevant(Message.Attachment attachment) {
}

private void processAttachments(MessageReceivedEvent event,
List<Message.Attachment> attachments) {

Map<String, GistFile> nameToFile = new ConcurrentHashMap<>();
List<Message.Attachment> attachments) throws IOException {
GHGistBuilder gistBuilder = new GitHubBuilder().withOAuthToken(gistApiKey)
.build()
.createGist()
.public_(false)
.description("Uploaded by " + event.getAuthor().getAsTag());

List<CompletableFuture<Void>> tasks = new ArrayList<>();

for (Message.Attachment attachment : attachments) {
CompletableFuture<Void> task = attachment.getProxy()
.download()
.thenApply(this::readAttachment)
.thenAccept(
content -> nameToFile.put(getNameOf(attachment), new GistFile(content)));
.thenAccept(content -> gistBuilder.file(getNameOf(attachment), content));

tasks.add(task);
}

tasks.forEach(CompletableFuture::join);

GistFiles files = new GistFiles(nameToFile);
GistRequest request = new GistRequest(event.getAuthor().getName(), false, files);
GistResponse response = uploadToGist(request);
String url = response.getHtmlUrl();
String gistId = response.getGistId();
sendResponse(event, url, gistId);
GHGist gist = gistBuilder.create();
sendResponse(event, gist.getHtmlUrl().toString(), gist.getGistId());
}

private String readAttachment(InputStream stream) {
Expand Down Expand Up @@ -173,62 +180,15 @@ private String getNameOf(Message.Attachment attachment) {
return fileName;
}

private GistResponse uploadToGist(GistRequest jsonRequest) {
String body;
try {
body = JSON.writeValueAsString(jsonRequest);
} catch (JsonProcessingException e) {
throw new IllegalStateException(
"Attempting to upload a file to gist, but unable to create the JSON request.",
e);
}

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SHARE_API))
.header("Accept", "application/json")
.header("Authorization", "token " + gistApiKey)
.POST(HttpRequest.BodyPublishers.ofString(body))
.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 upload a file to gist, but the request got interrupted.", e);
}

int statusCode = apiResponse.statusCode();

if (statusCode < HttpURLConnection.HTTP_OK
|| statusCode >= HttpURLConnection.HTTP_MULT_CHOICE) {
throw new IllegalStateException("Gist API unexpected response: %s. Request JSON: %s"
.formatted(apiResponse.body(), body));
}

GistResponse gistResponse;
try {
gistResponse = JSON.readValue(apiResponse.body(), GistResponse.class);
} catch (JsonProcessingException e) {
throw new IllegalStateException(
"Attempting to upload file to gist, but unable to parse its JSON response.", e);
}
return gistResponse;
}

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 👍";
String messageContent = "I uploaded your attachments as **Gist**.";

Button gist = Button.link(url, "gist");
Button gist = Button.link(url, "Gist");

Button delete = Button.danger(
componentIdInteractor.generateComponentId(message.getAuthor().getId(), gistId),
Emoji.fromUnicode("🗑️"));
"Dismiss");

message.reply(messageContent).setActionRow(gist, delete).queue();
}
Expand All @@ -243,32 +203,6 @@ private boolean isHelpThread(MessageReceivedEvent event) {
return isHelpForumName.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";
Expand All @@ -284,27 +218,6 @@ 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");
Expand All @@ -314,5 +227,4 @@ public void onSelectMenuSelection(SelectMenuInteractionEvent event, List<String>
public void onModalSubmitted(ModalInteractionEvent event, List<String> args) {
throw new UnsupportedOperationException("Not used");
}

}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.