-
-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Gist auto filesharing in help threads (#491)
* initial commit * initial commit * delete * worked on PR comments * added api key * Added default api key to config * fixed api key * changed to record * removed comments * added multiple uploads to one gist * Worked on pr comments * Fixed Sonarlint checks * Added documentation * Worked on pr comments * Accidentally pushed * pr comments * pr comments * pr comments * pr comments * pr comments
- Loading branch information
Showing
9 changed files
with
295 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
...src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
package org.togetherjava.tjbot.commands.filesharing; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import net.dv8tion.jda.api.entities.ChannelType; | ||
import net.dv8tion.jda.api.entities.Message; | ||
import net.dv8tion.jda.api.entities.ThreadChannel; | ||
import net.dv8tion.jda.api.entities.User; | ||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; | ||
import net.dv8tion.jda.api.interactions.components.buttons.Button; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.togetherjava.tjbot.commands.MessageReceiverAdapter; | ||
import org.togetherjava.tjbot.config.Config; | ||
|
||
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.*; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.function.Predicate; | ||
import java.util.regex.Pattern; | ||
|
||
/** | ||
* Listener that receives all sent help messages and uploads them to a share service if the message | ||
* contains a file with the given extension in the | ||
* {@link FileSharingMessageListener#extensionFilter}. | ||
*/ | ||
public class FileSharingMessageListener extends MessageReceiverAdapter { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(FileSharingMessageListener.class); | ||
private static final ObjectMapper JSON = new ObjectMapper(); | ||
|
||
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"); | ||
|
||
private final Predicate<String> isStagingChannelName; | ||
private final Predicate<String> isOverviewChannelName; | ||
|
||
|
||
public FileSharingMessageListener(@NotNull Config config) { | ||
super(Pattern.compile(".*")); | ||
|
||
gistApiKey = config.getGistApiKey(); | ||
isStagingChannelName = Pattern.compile(config.getHelpSystem().getStagingChannelPattern()) | ||
.asMatchPredicate(); | ||
isOverviewChannelName = Pattern.compile(config.getHelpSystem().getOverviewChannelPattern()) | ||
.asMatchPredicate(); | ||
} | ||
|
||
@Override | ||
public void onMessageReceived(@NotNull MessageReceivedEvent event) { | ||
User author = event.getAuthor(); | ||
if (author.isBot() || event.isWebhookMessage()) { | ||
return; | ||
} | ||
|
||
if (!isHelpThread(event)) { | ||
return; | ||
} | ||
|
||
|
||
List<Message.Attachment> attachments = event.getMessage() | ||
.getAttachments() | ||
.stream() | ||
.filter(this::isAttachmentRelevant) | ||
.toList(); | ||
|
||
CompletableFuture.runAsync(() -> { | ||
try { | ||
processAttachments(event, attachments); | ||
} catch (Exception e) { | ||
LOGGER.error("Unknown error while processing attachments", e); | ||
} | ||
}); | ||
} | ||
|
||
private boolean isAttachmentRelevant(@NotNull Message.Attachment attachment) { | ||
String extension = attachment.getFileExtension(); | ||
if (extension == null) { | ||
return false; | ||
} | ||
return extensionFilter.contains(extension); | ||
} | ||
|
||
|
||
private void processAttachments(@NotNull MessageReceivedEvent event, | ||
@NotNull List<Message.Attachment> attachments) { | ||
|
||
Map<String, GistFile> nameToFile = new ConcurrentHashMap<>(); | ||
|
||
List<CompletableFuture<Void>> tasks = new ArrayList<>(); | ||
for (Message.Attachment attachment : attachments) { | ||
CompletableFuture<Void> task = attachment.retrieveInputStream() | ||
.thenApply(this::readAttachment) | ||
.thenAccept( | ||
content -> nameToFile.put(getNameOf(attachment), new GistFile(content))); | ||
|
||
tasks.add(task); | ||
} | ||
|
||
tasks.forEach(CompletableFuture::join); | ||
|
||
GistFiles files = new GistFiles(nameToFile); | ||
GistRequest request = new GistRequest(event.getAuthor().getName(), false, files); | ||
String url = uploadToGist(request); | ||
sendResponse(event, url); | ||
} | ||
|
||
private @NotNull String readAttachment(@NotNull InputStream stream) { | ||
try { | ||
return new String(stream.readAllBytes(), StandardCharsets.UTF_8); | ||
} catch (IOException e) { | ||
throw new UncheckedIOException(e); | ||
} | ||
} | ||
|
||
private @NotNull String getNameOf(@NotNull Message.Attachment attachment) { | ||
String fileName = attachment.getFileName(); | ||
String fileExtension = attachment.getFileExtension(); | ||
|
||
if (fileExtension == null || fileExtension.equals("txt")) { | ||
fileExtension = "java"; | ||
} else if (fileExtension.equals("fxml")) { | ||
fileExtension = "xml"; | ||
} | ||
|
||
int extensionIndex = fileName.lastIndexOf('.'); | ||
if (extensionIndex != -1) { | ||
fileName = fileName.substring(0, extensionIndex); | ||
} | ||
|
||
fileName += "." + fileExtension; | ||
|
||
return fileName; | ||
} | ||
|
||
private @NotNull String uploadToGist(@NotNull 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: " + apiResponse.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.getHtmlUrl(); | ||
} | ||
|
||
private void sendResponse(@NotNull MessageReceivedEvent event, @NotNull String url) { | ||
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(); | ||
} | ||
|
||
private boolean isHelpThread(@NotNull MessageReceivedEvent event) { | ||
if (event.getChannelType() != ChannelType.GUILD_PUBLIC_THREAD) { | ||
return false; | ||
} | ||
|
||
ThreadChannel thread = event.getThreadChannel(); | ||
String rootChannelName = thread.getParentChannel().getName(); | ||
return isStagingChannelName.test(rootChannelName) | ||
|| isOverviewChannelName.test(rootChannelName); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFile.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.togetherjava.tjbot.commands.filesharing; | ||
|
||
import org.jetbrains.annotations.NotNull; | ||
|
||
/** | ||
* @see <a href="https://docs.github.com/en/rest/gists/gists#create-a-gist">Create a Gist via | ||
* API</a> | ||
*/ | ||
record GistFile(@NotNull String content) { | ||
} |
13 changes: 13 additions & 0 deletions
13
application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFiles.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package org.togetherjava.tjbot.commands.filesharing; | ||
|
||
import com.fasterxml.jackson.annotation.JsonAnyGetter; | ||
import org.jetbrains.annotations.NotNull; | ||
|
||
import java.util.Map; | ||
|
||
/** | ||
* @see <a href="https://docs.github.com/en/rest/gists/gists#create-a-gist">Create a Gist via | ||
* API</a> | ||
*/ | ||
record GistFiles(@NotNull @JsonAnyGetter Map<String, GistFile> nameToContent) { | ||
} |
12 changes: 12 additions & 0 deletions
12
application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.togetherjava.tjbot.commands.filesharing; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import org.jetbrains.annotations.NotNull; | ||
|
||
/** | ||
* @see <a href="https://docs.github.com/en/rest/gists/gists#create-a-gist">Create a Gist via | ||
* API</a> | ||
*/ | ||
record GistRequest(@NotNull String description, @JsonProperty("public") boolean isPublic, | ||
@NotNull GistFiles files) { | ||
} |
23 changes: 23 additions & 0 deletions
23
application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package org.togetherjava.tjbot.commands.filesharing; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import org.jetbrains.annotations.NotNull; | ||
|
||
/** | ||
* @see <a href="https://docs.github.com/en/rest/gists/gists#create-a-gist">Create a Gist via | ||
* API</a> | ||
*/ | ||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
final class GistResponse { | ||
@JsonProperty("html_url") | ||
private String htmlUrl; | ||
|
||
public @NotNull String getHtmlUrl() { | ||
return this.htmlUrl; | ||
} | ||
|
||
public void setHtmlUrl(@NotNull String htmlUrl) { | ||
this.htmlUrl = htmlUrl; | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
application/src/main/java/org/togetherjava/tjbot/commands/filesharing/package-info.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** | ||
* This package offers all the functionality for automatically uploading files to sharing services. | ||
* The core class is {@link org.togetherjava.tjbot.commands.filesharing.FileSharingMessageListener}. | ||
*/ | ||
package org.togetherjava.tjbot.commands.filesharing; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
*/ | ||
public final class Config { | ||
private final String token; | ||
private final String gistApiKey; | ||
private final String databasePath; | ||
private final String projectWebsite; | ||
private final String discordGuildInvite; | ||
|
@@ -31,6 +32,7 @@ public final class Config { | |
@SuppressWarnings("ConstructorWithTooManyParameters") | ||
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | ||
private Config(@JsonProperty("token") String token, | ||
@JsonProperty("gistApiKey") String gistApiKey, | ||
@JsonProperty("databasePath") String databasePath, | ||
@JsonProperty("projectWebsite") String projectWebsite, | ||
@JsonProperty("discordGuildInvite") String discordGuildInvite, | ||
|
@@ -45,6 +47,7 @@ private Config(@JsonProperty("token") String token, | |
@JsonProperty("wolframAlphaAppId") String wolframAlphaAppId, | ||
@JsonProperty("helpSystem") HelpSystemConfig helpSystem) { | ||
this.token = token; | ||
this.gistApiKey = gistApiKey; | ||
this.databasePath = databasePath; | ||
this.projectWebsite = projectWebsite; | ||
this.discordGuildInvite = discordGuildInvite; | ||
|
@@ -100,6 +103,18 @@ public String getToken() { | |
return token; | ||
} | ||
|
||
/** | ||
* Gets the API Key of GitHub to upload pastes via the API. | ||
* | ||
* @return the upload services API Key | ||
* @see <a href= | ||
* "https://docs.github.com/en/[email protected]/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token">Create | ||
* a GitHub key</a> | ||
*/ | ||
public String getGistApiKey() { | ||
return gistApiKey; | ||
} | ||
|
||
/** | ||
* Gets the path where the database of the application is located at. | ||
* | ||
|