Skip to content

Commit 00e00fa

Browse files
committed
Expand the !wiki command to enable searching the wiki.
1 parent ef028a4 commit 00e00fa

File tree

3 files changed

+268
-7
lines changed

3 files changed

+268
-7
lines changed

src/main/java/org/javacord/bot/commands/WikiCommand.java

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,158 @@
22

33
import de.btobastian.sdcf4j.Command;
44
import de.btobastian.sdcf4j.CommandExecutor;
5+
import org.javacord.api.DiscordApi;
56
import org.javacord.api.entity.channel.TextChannel;
67
import org.javacord.api.entity.message.embed.EmbedBuilder;
8+
import org.javacord.api.util.logging.ExceptionLogger;
79
import org.javacord.bot.Constants;
10+
import org.javacord.bot.util.wiki.parser.WikiPage;
11+
import org.javacord.bot.util.wiki.parser.WikiParser;
12+
13+
import java.io.IOException;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
import java.util.function.Predicate;
17+
import java.util.regex.Pattern;
18+
import java.util.stream.Collectors;
819

920
/**
1021
* The !wiki command which is used to link to Javacord's wiki.
1122
*/
1223
public class WikiCommand implements CommandExecutor {
1324

25+
private static final Pattern HTML_TAG = Pattern.compile("<[^>]++>");
26+
1427
/**
1528
* Executes the {@code !wiki} command.
1629
*
1730
* @param channel The channel where the command was issued.
31+
* @throws IOException If the connection to the wiki failed.
1832
*/
1933
@Command(aliases = {"!wiki"}, async = true)
20-
public void onCommand(TextChannel channel) {
21-
EmbedBuilder embed = new EmbedBuilder()
22-
.setTitle("Javacord Wiki")
23-
.setDescription("https://javacord.org/wiki")
24-
.setThumbnail(getClass().getClassLoader().getResourceAsStream("javacord3_icon.png"), "png")
25-
.setColor(Constants.JAVACORD_ORANGE);
26-
channel.sendMessage(embed).join();
34+
public void onCommand(DiscordApi api, TextChannel channel, String[] args) throws IOException {
35+
try {
36+
if (args.length == 0) { // Just an overview
37+
EmbedBuilder embed = new EmbedBuilder()
38+
.setTitle("Javacord Wiki")
39+
.setDescription("The [Javacord Wiki](" + WikiParser.BASE_URL + "/wiki) is an excellent " +
40+
"resource to get you started with Javacord.\n")
41+
.addInlineField("Hint", "You can search the wiki using `!wiki [title|full] <search>")
42+
.setThumbnail(getClass().getClassLoader().getResourceAsStream("javacord3_icon.png"), "png")
43+
.setColor(Constants.JAVACORD_ORANGE);
44+
channel.sendMessage(embed).join();
45+
} else {
46+
EmbedBuilder embed = new EmbedBuilder()
47+
.setThumbnail(getClass().getClassLoader().getResourceAsStream("javacord3_icon.png"), "png")
48+
.setColor(Constants.JAVACORD_ORANGE);
49+
String searchString = String.join(" ", Arrays.copyOfRange(args, 1, args.length)).toLowerCase();
50+
switch (args[0]) {
51+
case "page":
52+
case "p":
53+
case "title":
54+
case "t":
55+
populatePages(api, embed, titleOnly(searchString));
56+
break;
57+
case "full":
58+
case "f":
59+
case "content":
60+
case "c":
61+
populatePages(api, embed, fullSearch(searchString));
62+
break;
63+
default:
64+
searchString = String.join(" ", Arrays.copyOfRange(args, 0, args.length)).toLowerCase();
65+
populatePages(api, embed, titleOnly(searchString).or(keywordsOnly(searchString)));
66+
}
67+
channel.sendMessage(embed).join();
68+
}
69+
} catch (Throwable t) {
70+
channel.sendMessage("Something went wrong: ```" + ExceptionLogger.unwrapThrowable(t).getMessage() + "```").join();
71+
// Throw the caught exception again. The sdcf4j will log it.
72+
throw t;
73+
}
74+
}
75+
76+
private Predicate<WikiPage> fullSearch(String searchString) {
77+
return titleOnly(searchString).or(keywordsOnly(searchString)).or(contentOnly(searchString));
78+
}
79+
80+
private Predicate<WikiPage> titleOnly(String searchString) {
81+
return p -> p.getTitle().toLowerCase().contains(searchString);
82+
}
83+
84+
private Predicate<WikiPage> keywordsOnly(String searchString) {
85+
return p -> Arrays.stream(p.getKeywords())
86+
.map(String::toLowerCase)
87+
.anyMatch(k -> k.contains(searchString));
88+
}
89+
90+
private Predicate<WikiPage> contentOnly(String searchString) {
91+
return p -> p.getContent().toLowerCase().contains(searchString);
92+
}
93+
94+
95+
private void populatePages(DiscordApi api, EmbedBuilder embed, Predicate<WikiPage> criteria) throws IOException {
96+
List<WikiPage> pages;
97+
98+
pages = new WikiParser(api)
99+
.getPagesBlocking().stream()
100+
.filter(criteria)
101+
.sorted()
102+
.collect(Collectors.toList());
103+
104+
if (pages.isEmpty()) {
105+
embed.setTitle("Javacord Wiki");
106+
embed.setUrl(WikiParser.BASE_URL + "/wiki/");
107+
embed.setDescription("No pages found. Maybe try another search.");
108+
embed.addField("Standard Search", "Use `!wiki <search>` to search page titles and keywords.");
109+
embed.addField("Title Search", "Use `!wiki [page|p|title|t] <search>` to exclusively search page titles.");
110+
embed.addField("Full Search", "Use `!wiki [full|f|content|c] <search>` to perform a full search.");
111+
} else if (pages.size() == 1) {
112+
WikiPage page = pages.get(0);
113+
displayPagePreview(embed, page);
114+
} else {
115+
displayPageList(embed, pages);
116+
}
117+
}
118+
119+
private void displayPagePreview(EmbedBuilder embed, WikiPage page) {
120+
embed.setTitle("Javacord Wiki");
121+
String cleanedDescription = HTML_TAG.matcher(page.getContent()).replaceAll("").trim();
122+
int length = 0;
123+
int sentences = 0;
124+
while (length < 600 && sentences < 3) {
125+
int tmpLength = cleanedDescription.indexOf(". ", length);
126+
length = (tmpLength > length) ? tmpLength : cleanedDescription.indexOf(".\n");
127+
128+
sentences++;
129+
}
130+
StringBuilder description = new StringBuilder()
131+
.append(String.format("**[%s](%s)**\n\n", page.getTitle(), WikiParser.BASE_URL + page.getUrl()))
132+
.append(cleanedDescription, 0, length + 1);
133+
if (length < cleanedDescription.length()) {
134+
description.append("\n\n[*view full page*](").append(WikiParser.BASE_URL).append(page.getUrl()).append(")");
135+
}
136+
embed.setDescription(description.toString());
137+
}
138+
139+
private void displayPageList(EmbedBuilder embed, List<WikiPage> pages) {
140+
embed.setTitle("Javacord Wiki");
141+
embed.setUrl(WikiParser.BASE_URL + "/wiki/");
142+
143+
StringBuilder builder = new StringBuilder();
144+
int counter = 0;
145+
for (WikiPage page : pages) {
146+
String pageLink = "• " + page.asMarkdownLink();
147+
if (builder.length() + pageLink.length() > 1900) { // Prevent hitting the description size limit
148+
break;
149+
}
150+
builder.append(pageLink).append("\n");
151+
counter++;
152+
}
153+
if (pages.size() > counter) {
154+
builder.append("and ").append(pages.size() - counter).append(" more ...");
155+
}
156+
embed.setDescription(builder.toString());
27157
}
28158

29159
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.javacord.bot.util.wiki.parser;
2+
3+
public class WikiPage implements Comparable<WikiPage> {
4+
5+
private final String title;
6+
private final String[] keywords;
7+
private final String url;
8+
private final String content;
9+
10+
public WikiPage(String title, String[] keywords, String url, String content) {
11+
this.title = title;
12+
this.keywords = keywords;
13+
this.url = url;
14+
this.content = content;
15+
}
16+
17+
public String getTitle() {
18+
return title;
19+
}
20+
21+
public String[] getKeywords() {
22+
return keywords;
23+
}
24+
25+
public String getUrl() {
26+
return url;
27+
}
28+
29+
public String getContent() {
30+
return content;
31+
}
32+
33+
public String asMarkdownLink() {
34+
return String.format("[%s](%s)", title, WikiParser.BASE_URL + url);
35+
}
36+
37+
@Override
38+
public int compareTo(WikiPage that) {
39+
return this.title.compareTo(that.title);
40+
}
41+
42+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package org.javacord.bot.util.wiki.parser;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import okhttp3.OkHttpClient;
6+
import okhttp3.Request;
7+
import okhttp3.Response;
8+
import okhttp3.ResponseBody;
9+
import org.javacord.api.DiscordApi;
10+
11+
import java.io.IOException;
12+
import java.util.HashSet;
13+
import java.util.Set;
14+
import java.util.concurrent.CompletableFuture;
15+
import java.util.concurrent.CompletionException;
16+
17+
public class WikiParser {
18+
19+
public static final String API_URL = "https://javacord.org/api/wiki.json";
20+
public static final String BASE_URL = "https://javacord.org"; // the /wiki/ part of the url will be returned by the API
21+
22+
private static final OkHttpClient client = new OkHttpClient();
23+
private static final ObjectMapper mapper = new ObjectMapper();
24+
25+
private final DiscordApi discordApi;
26+
private final String apiUrl;
27+
28+
public WikiParser(DiscordApi api) {
29+
this(api, API_URL);
30+
}
31+
32+
public WikiParser(DiscordApi api, String apiUrl) {
33+
this.discordApi = api;
34+
this.apiUrl = apiUrl;
35+
}
36+
37+
public CompletableFuture<Set<WikiPage>> getPages() {
38+
return CompletableFuture.supplyAsync(() -> {
39+
try {
40+
return getPagesBlocking();
41+
} catch (Throwable t) {
42+
throw new CompletionException(t);
43+
}
44+
}, discordApi.getThreadPool().getExecutorService());
45+
}
46+
47+
public Set<WikiPage> getPagesBlocking() throws IOException {
48+
Request request = new Request.Builder()
49+
.url(apiUrl)
50+
.build();
51+
52+
Response response = client.newCall(request).execute();
53+
ResponseBody body = response.body();
54+
Set<WikiPage> pages = new HashSet<>();
55+
if (body == null) {
56+
return pages;
57+
}
58+
JsonNode array = mapper.readTree(body.charStream());
59+
if (!array.isArray()) {
60+
throw new AssertionError("Format of Wiki page list not as expected");
61+
}
62+
for (JsonNode node : array) {
63+
if (node.has("title") && node.has("keywords") && node.has("url") && node.has("content")) {
64+
pages.add(new WikiPage(
65+
node.get("title").asText(),
66+
asStringArray(node.get("keywords")),
67+
node.get("url").asText(),
68+
node.get("content").asText()
69+
));
70+
} else {
71+
throw new AssertionError("Format of Wiki page list not as expected");
72+
}
73+
}
74+
return pages;
75+
}
76+
77+
private String[] asStringArray(JsonNode arrayNode) {
78+
if (!arrayNode.isArray()) {
79+
return new String[] {};
80+
}
81+
String[] result = new String[arrayNode.size()];
82+
int i = 0;
83+
for (JsonNode node : arrayNode) {
84+
result[i++] = node.asText();
85+
}
86+
return result;
87+
}
88+
89+
}

0 commit comments

Comments
 (0)