Skip to content

Commit 5cfeb68

Browse files
authored
Merge pull request #552 from danthe1st/answer-overflow-mark-solution
Answer Overflow integration
2 parents 97a6d0e + 6363d2c commit 5cfeb68

8 files changed

Lines changed: 121 additions & 25 deletions

File tree

src/main/java/net/discordjug/javabot/data/config/SystemsConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ public class SystemsConfig {
2424
* The Key used for the bing-search-api.
2525
*/
2626
private String azureSubscriptionKey = "";
27+
28+
/**
29+
* The API key of the Answer Overflow API.
30+
*/
31+
private String answerOverflowApiKey = "";
2732

2833
/**
2934
* The DSN for the Sentry API.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package net.discordjug.javabot.systems.help;
2+
3+
import java.net.URI;
4+
import java.net.http.HttpClient;
5+
import java.net.http.HttpRequest;
6+
import java.net.http.HttpRequest.BodyPublishers;
7+
import java.net.http.HttpResponse.BodyHandlers;
8+
9+
import lombok.extern.slf4j.Slf4j;
10+
import net.discordjug.javabot.data.config.BotConfig;
11+
import org.springframework.stereotype.Service;
12+
13+
/**
14+
* A service class for interacting with the Answer Overflow bot.
15+
*/
16+
@Service
17+
@Slf4j
18+
public class AnswerOverflowService {
19+
private final String apiKey;
20+
21+
public AnswerOverflowService(BotConfig config) {
22+
apiKey = config.getSystems().getAnswerOverflowApiKey();
23+
}
24+
25+
/**
26+
* Marks a message as the answer of a forum post with Answer Overflow.
27+
* @param postId The Discord ID of the forum post
28+
* @param messageId The Discord ID of the message that should be marked as the answer.
29+
*/
30+
public void markAnswer(long postId, long messageId) {
31+
if (apiKey == null || apiKey.isBlank()) {
32+
return;
33+
}
34+
35+
try (HttpClient client = HttpClient.newHttpClient()) {
36+
client.sendAsync(
37+
HttpRequest.newBuilder(URI.create("https://www.answeroverflow.com/api/v1/messages/" + postId))
38+
.header("x-api-key", apiKey)
39+
.header("Content-Type", "application/json")
40+
.POST(BodyPublishers.ofString("""
41+
{
42+
"solutionId": "%d"
43+
}
44+
""".formatted(messageId)))
45+
.build(),
46+
BodyHandlers.ofString())
47+
.thenAccept(response -> {
48+
if (response.statusCode() != 200) {
49+
log.warn("Answer Overflow responded with unexpected status code {}, post ID: {}, message ID: {}, body: {}", response.statusCode(), postId, messageId, response.body());
50+
}
51+
})
52+
.exceptionally(e -> {
53+
log.error("An exception occured trying to mark a post as an answer.", e);
54+
return null;
55+
});
56+
}
57+
}
58+
}

src/main/java/net/discordjug/javabot/systems/help/HelpListener.java

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import xyz.dynxsty.dih4jda.util.ComponentIdBuilder;
3131

3232
import java.util.*;
33+
import java.util.stream.Collectors;
3334
import java.util.stream.Stream;
3435

3536
/**
@@ -79,6 +80,7 @@ protected boolean removeEldestEntry(Map.Entry<Long, Long> eldest) {
7980
return System.currentTimeMillis() > eldest.getValue() || size() >= 32;
8081
}
8182
};
83+
private final AnswerOverflowService answerOverflowService;
8284

8385
@Override
8486
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
@@ -121,12 +123,8 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) {
121123
return;
122124
}
123125
// cache messages
124-
List<Message> messages = new ArrayList<>();
125-
messages.add(event.getMessage());
126-
if (HELP_POST_MESSAGES.containsKey(post.getIdLong())) {
127-
messages.addAll(HELP_POST_MESSAGES.get(post.getIdLong()));
128-
}
129-
HELP_POST_MESSAGES.put(post.getIdLong(), messages);
126+
HELP_POST_MESSAGES.computeIfAbsent(post.getIdLong(), _ -> new ArrayList<>())
127+
.add(event.getMessage());
130128
// suggest to close post on "problem solved"-messages
131129
replyCloseSuggestionIfPatternMatches(event.getMessage());
132130
}
@@ -291,17 +289,34 @@ private void handleThanksCloseButton(@NotNull ButtonInteractionEvent event, Help
291289
event.getMessage().delete().queue(s -> {
292290
experienceService.addMessageBasedHelpXP(post, true);
293291
// thank all helpers
294-
getButtonStream(event.getMessage())
295-
.filter(b -> b.getCustomId() != null)
296-
.filter(b -> b.isDisabled() || (b.getCustomId().equals(additionalButtonId)))
297-
.forEach(b -> manager.thankHelper(
298-
event.getGuild(),
299-
post,
300-
Long.parseLong(ComponentIdBuilder.split(b.getCustomId())[2])
301-
));
292+
293+
Set<Long> helpersToThank = getButtonStream(event.getMessage())
294+
.filter(b -> b.getCustomId() != null)
295+
.filter(b -> b.isDisabled() || (b.getCustomId().equals(additionalButtonId)))
296+
.map(b -> Long.parseLong(ComponentIdBuilder.split(b.getCustomId())[2]))
297+
.collect(Collectors.toSet());
298+
299+
for (Long helperId : helpersToThank) {
300+
manager.thankHelper(event.getGuild(), post, helperId);
301+
}
302+
markAnswer(post, helpersToThank);
303+
HELP_POST_MESSAGES.remove(post.getIdLong());
302304
});
303305
}
304306

307+
private void markAnswer(ThreadChannel post, Set<Long> helpers) {
308+
List<Message> messages = HELP_POST_MESSAGES.get(post.getIdLong());
309+
if (messages == null) {
310+
return;
311+
}
312+
for (Message msg : messages.reversed()) {
313+
if (helpers.contains(msg.getAuthor().getIdLong())) {
314+
answerOverflowService.markAnswer(post.getIdLong(), msg.getIdLong());
315+
return;
316+
}
317+
}
318+
}
319+
305320
private void handleReplyGuidelines(@NotNull IReplyCallback callback, @NotNull ForumChannel channel) {
306321
callback.replyEmbeds(new EmbedBuilder().setTitle("Help Guidelines")
307322
.setDescription(channel.getTopic())

src/main/java/net/discordjug/javabot/systems/qotw/jobs/QOTWCloseSubmissionsJob.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import net.discordjug.javabot.data.config.GuildConfig;
66
import net.discordjug.javabot.data.config.SystemsConfig;
77
import net.discordjug.javabot.data.config.guild.QOTWConfig;
8+
import net.discordjug.javabot.systems.help.AnswerOverflowService;
89
import net.discordjug.javabot.systems.notification.NotificationService;
910
import net.discordjug.javabot.systems.qotw.QOTWPointsService;
1011
import net.discordjug.javabot.systems.qotw.dao.QuestionQueueRepository;
@@ -63,6 +64,7 @@ public class QOTWCloseSubmissionsJob {
6364
private final ExecutorService asyncPool;
6465
private final QOTWPointsService pointsService;
6566
private final NotificationService notificationService;
67+
private final AnswerOverflowService answerOverflowService;
6668
private final BotConfig botConfig;
6769

6870
/**
@@ -131,7 +133,7 @@ private void sendReviewInformation(Guild guild, QOTWConfig qotwConfig, TextChann
131133
s.retrieveAuthor(author -> {
132134
submission.removeThreadMember(author).queue();
133135
if (author.getIdLong() == qotwConfig.getQotwSampleAnswerUserId()) {
134-
SubmissionManager manager = new SubmissionManager(botConfig.get(guild).getQotwConfig(), pointsService, questionQueueRepository, notificationService, asyncPool);
136+
SubmissionManager manager = new SubmissionManager(botConfig.get(guild).getQotwConfig(), pointsService, questionQueueRepository, notificationService, asyncPool, answerOverflowService);
135137
manager.copySampleAnswerSubmission(submission, author);
136138
} else {
137139
thread

src/main/java/net/discordjug/javabot/systems/qotw/jobs/QOTWUserReminderJob.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import net.discordjug.javabot.data.config.BotConfig;
44
import net.discordjug.javabot.data.config.guild.QOTWConfig;
5+
import net.discordjug.javabot.systems.help.AnswerOverflowService;
56
import net.discordjug.javabot.systems.notification.NotificationService;
67
import net.discordjug.javabot.systems.qotw.QOTWPointsService;
78
import net.discordjug.javabot.systems.qotw.dao.QuestionQueueRepository;
@@ -33,6 +34,7 @@ public class QOTWUserReminderJob {
3334
private final QOTWPointsService pointsService;
3435
private final NotificationService notificationService;
3536
private final QuestionQueueRepository questionQueueRepository;
37+
private final AnswerOverflowService answerOverflowService;
3638
private final ExecutorService asyncPool;
3739

3840
/**
@@ -42,7 +44,7 @@ public class QOTWUserReminderJob {
4244
public void execute() {
4345
for (Guild guild : jda.getGuilds()) {
4446
QOTWConfig config = botConfig.get(guild).getQotwConfig();
45-
List<QOTWSubmission> submissions = new SubmissionManager(config, pointsService, questionQueueRepository, notificationService, asyncPool).getActiveSubmissions();
47+
List<QOTWSubmission> submissions = new SubmissionManager(config, pointsService, questionQueueRepository, notificationService, asyncPool, answerOverflowService).getActiveSubmissions();
4648
for (QOTWSubmission submission : submissions) {
4749
submission.retrieveAuthor(author -> {
4850
UserPreference preference = userPreferenceService.getOrCreate(author.getIdLong(), Preference.QOTW_REMINDER);

src/main/java/net/discordjug/javabot/systems/qotw/submissions/SubmissionInteractionManager.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import lombok.RequiredArgsConstructor;
99
import net.discordjug.javabot.annotations.AutoDetectableComponentHandler;
1010
import net.discordjug.javabot.data.config.BotConfig;
11+
import net.discordjug.javabot.systems.help.AnswerOverflowService;
1112
import net.discordjug.javabot.systems.notification.NotificationService;
1213
import net.discordjug.javabot.systems.qotw.QOTWPointsService;
1314
import net.discordjug.javabot.systems.qotw.dao.QuestionQueueRepository;
@@ -29,11 +30,12 @@ public class SubmissionInteractionManager implements ButtonHandler, StringSelect
2930
private final NotificationService notificationService;
3031
private final BotConfig botConfig;
3132
private final QuestionQueueRepository questionQueueRepository;
33+
private final AnswerOverflowService answerOverflowService;
3234
private final ExecutorService asyncPool;
3335

3436
@Override
3537
public void handleButton(@NotNull ButtonInteractionEvent event, Button button) {
36-
SubmissionManager manager = new SubmissionManager(botConfig.get(event.getGuild()).getQotwConfig(), pointsService, questionQueueRepository, notificationService, asyncPool);
38+
SubmissionManager manager = new SubmissionManager(botConfig.get(event.getGuild()).getQotwConfig(), pointsService, questionQueueRepository, notificationService, asyncPool, answerOverflowService);
3739
String[] id = ComponentIdBuilder.split(event.getComponentId());
3840
switch (id[1]) {
3941
case "submit" -> manager.handleSubmission(event, Integer.parseInt(id[2])).queue();
@@ -43,7 +45,7 @@ public void handleButton(@NotNull ButtonInteractionEvent event, Button button) {
4345

4446
@Override
4547
public void handleStringSelectMenu(@NotNull StringSelectInteractionEvent event, @NotNull List<String> values) {
46-
SubmissionManager manager = new SubmissionManager(botConfig.get(event.getGuild()).getQotwConfig(), pointsService, questionQueueRepository, notificationService, asyncPool);
48+
SubmissionManager manager = new SubmissionManager(botConfig.get(event.getGuild()).getQotwConfig(), pointsService, questionQueueRepository, notificationService, asyncPool, answerOverflowService);
4749
String[] id = ComponentIdBuilder.split(event.getComponentId());
4850
switch (id[1]) {
4951
case "review" -> manager.handleSelectReview(event, id[2]);

src/main/java/net/discordjug/javabot/systems/qotw/submissions/SubmissionManager.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.RequiredArgsConstructor;
55
import lombok.extern.slf4j.Slf4j;
66
import net.discordjug.javabot.data.config.guild.QOTWConfig;
7+
import net.discordjug.javabot.systems.help.AnswerOverflowService;
78
import net.discordjug.javabot.systems.notification.NotificationService;
89
import net.discordjug.javabot.systems.qotw.QOTWPointsService;
910
import net.discordjug.javabot.systems.qotw.dao.QuestionQueueRepository;
@@ -39,6 +40,8 @@
3940
import java.util.concurrent.CompletableFuture;
4041
import java.util.concurrent.ExecutorService;
4142

43+
import club.minnced.discord.webhook.receive.ReadonlyMessage;
44+
4245
/**
4346
* Handles & manages QOTW Submissions by using Discords {@link ThreadChannel}s.
4447
*/
@@ -57,6 +60,7 @@ public class SubmissionManager {
5760
private final QuestionQueueRepository questionQueueRepository;
5861
private final NotificationService notificationService;
5962
private final ExecutorService asyncPool;
63+
private final AnswerOverflowService answerOverflowService;
6064

6165
/**
6266
* Handles the "Submit your Answer" Button interaction.
@@ -221,14 +225,19 @@ private void sendToQOTWAnswerArchive(ThreadChannel thread, User author, Accepted
221225
ThreadChannel newestPost = newestPostOptional.get();
222226
WebhookUtil.ensureWebhookExists(newestPost.getParentChannel().asForumChannel(), wh ->
223227
getMessagesByUser(thread, author).thenAccept(messages -> {
224-
for (Message message : messages) {
225-
boolean lastMessage = messages.indexOf(message) + 1 == messages.size();
228+
for (int i = 0; i < messages.size(); i++) {
229+
Message message = messages.get(i);
230+
boolean lastMessage = i + 1 == messages.size();
226231
if (message.getAuthor().isBot() || message.getType() != MessageType.DEFAULT) continue;
232+
ReadonlyMessage firstPostedMessage;
227233
if (message.getContentRaw().length() > 2000) {
228-
WebhookUtil.mirrorMessageToWebhook(wh, message, message.getContentRaw().substring(0, 2000), newestPost.getIdLong(), null, null).join();
234+
firstPostedMessage = WebhookUtil.mirrorMessageToWebhook(wh, message, message.getContentRaw().substring(0, 2000), newestPost.getIdLong(), null, null).join();
229235
WebhookUtil.mirrorMessageToWebhook(wh, message, message.getContentRaw().substring(2000), newestPost.getIdLong(), null, lastMessage ? List.of(buildAuthorEmbed(author, type)) : null).join();
230236
} else {
231-
WebhookUtil.mirrorMessageToWebhook(wh, message, message.getContentRaw(), newestPost.getIdLong(), null, lastMessage ? List.of(buildAuthorEmbed(author, type)) : null).join();
237+
firstPostedMessage = WebhookUtil.mirrorMessageToWebhook(wh, message, message.getContentRaw(), newestPost.getIdLong(), null, lastMessage ? List.of(buildAuthorEmbed(author, type)) : null).join();
238+
}
239+
if (i == 0 && (type == AcceptedAnswerType.BEST_ANSWER || type == AcceptedAnswerType.SAMPLE_ANSWER)) {
240+
answerOverflowService.markAnswer(firstPostedMessage.getChannelId(), firstPostedMessage.getId());
232241
}
233242
}
234243
}).exceptionally(err->{

src/main/java/net/discordjug/javabot/systems/qotw/submissions/subcommands/QOTWReviewSubcommand.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import net.discordjug.javabot.data.config.BotConfig;
44
import net.discordjug.javabot.data.config.guild.QOTWConfig;
5+
import net.discordjug.javabot.systems.help.AnswerOverflowService;
56
import net.discordjug.javabot.systems.notification.NotificationService;
67
import net.discordjug.javabot.systems.qotw.QOTWPointsService;
78
import net.discordjug.javabot.systems.qotw.dao.QuestionQueueRepository;
@@ -34,7 +35,7 @@ public class QOTWReviewSubcommand extends SlashCommand.Subcommand {
3435
private final QuestionQueueRepository questionQueueRepository;
3536
private final BotConfig botConfig;
3637
private final ExecutorService asyncPool;
37-
38+
private final AnswerOverflowService answerOverflowService;
3839

3940
/**
4041
* The constructor of this class, which sets the corresponding {@link SubcommandData}.
@@ -43,13 +44,15 @@ public class QOTWReviewSubcommand extends SlashCommand.Subcommand {
4344
* @param questionQueueRepository The {@link QuestionQueueRepository}.
4445
* @param botConfig The main configuration of the bot
4546
* @param asyncPool The main thread pool for asynchronous operations
47+
* @param answerOverflowService The {@link AnswerOverflowService} for interacting with the Answer Overflow bot.
4648
*/
47-
public QOTWReviewSubcommand(QOTWPointsService pointsService, NotificationService notificationService, QuestionQueueRepository questionQueueRepository, BotConfig botConfig, ExecutorService asyncPool) {
49+
public QOTWReviewSubcommand(QOTWPointsService pointsService, NotificationService notificationService, QuestionQueueRepository questionQueueRepository, BotConfig botConfig, ExecutorService asyncPool, AnswerOverflowService answerOverflowService) {
4850
this.pointsService = pointsService;
4951
this.notificationService = notificationService;
5052
this.questionQueueRepository = questionQueueRepository;
5153
this.botConfig = botConfig;
5254
this.asyncPool = asyncPool;
55+
this.answerOverflowService = answerOverflowService;
5356
setCommandData(new SubcommandData("review", "Administrative command for reviewing QOTW-submissions")
5457
.addOptions(
5558
new OptionData(OptionType.CHANNEL, "submission", "A users' submission", true)
@@ -80,7 +83,7 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
8083
event.deferReply().queue();
8184
QOTWSubmission submission = new QOTWSubmission(submissionThread);
8285
submission.retrieveAuthor(author -> {
83-
SubmissionManager manager = new SubmissionManager(qotwConfig, pointsService, questionQueueRepository, notificationService, asyncPool);
86+
SubmissionManager manager = new SubmissionManager(qotwConfig, pointsService, questionQueueRepository, notificationService, asyncPool, answerOverflowService);
8487
if (state.contains("ACCEPT")) {
8588
manager.acceptSubmission(submissionThread, author, event.getMember(), state.equals("ACCEPT_BEST"));
8689
Responses.success(event.getHook(), "Submission Accepted", "Successfully accepted submission by " + author.getAsMention()).queue();

0 commit comments

Comments
 (0)