Skip to content

Commit 70469fb

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

File tree

3 files changed

+336
-7
lines changed

3 files changed

+336
-7
lines changed

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

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

29166
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package org.javacord.bot.util.wiki.parser;
2+
3+
/**
4+
* A class representing a page on the wiki.
5+
*/
6+
public class WikiPage implements Comparable<WikiPage> {
7+
8+
private final String title;
9+
private final String[] keywords;
10+
private final String url;
11+
private final String content;
12+
13+
/**
14+
* Creates a new wiki page.
15+
*
16+
* @param title The title of the page.
17+
* @param keywords The keywords the page is tagged with.
18+
* @param url The URL of the page, relative to the wiki's base URL.
19+
* @param content The content of the page.
20+
*/
21+
public WikiPage(String title, String[] keywords, String url, String content) {
22+
this.title = title;
23+
this.keywords = keywords;
24+
this.url = url;
25+
this.content = content;
26+
}
27+
28+
/**
29+
* Gets the title.
30+
*
31+
* @return The title of the page.
32+
*/
33+
public String getTitle() {
34+
return title;
35+
}
36+
37+
/**
38+
* Gets the keywords.
39+
*
40+
* @return The keywords for the page.
41+
*/
42+
public String[] getKeywords() {
43+
return keywords;
44+
}
45+
46+
/**
47+
* Gets the relative URL.
48+
*
49+
* @return The page URL.
50+
*/
51+
public String getUrl() {
52+
return url;
53+
}
54+
55+
/**
56+
* Gets the content.
57+
*
58+
* @return The content of the page.
59+
*/
60+
public String getContent() {
61+
return content;
62+
}
63+
64+
/**
65+
* Gets a markdown-formatted link to the page.
66+
*
67+
* @return The markdown for a link to the page.
68+
*/
69+
public String asMarkdownLink() {
70+
return String.format("[%s](%s)", title, WikiParser.BASE_URL + url);
71+
}
72+
73+
@Override
74+
public int compareTo(WikiPage that) {
75+
return this.title.compareTo(that.title);
76+
}
77+
78+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
/**
18+
* A parser for the Javacord wiki.
19+
*/
20+
public class WikiParser {
21+
22+
public static final String API_URL = "https://javacord.org/api/wiki.json";
23+
public static final String BASE_URL = "https://javacord.org"; // the /wiki/ part of the url will be returned by the API
24+
25+
private static final OkHttpClient client = new OkHttpClient();
26+
private static final ObjectMapper mapper = new ObjectMapper();
27+
28+
private final DiscordApi discordApi;
29+
private final String apiUrl;
30+
31+
/**
32+
* Creates a new wiki parser.
33+
*
34+
* @param api The Discord Api of which to use the HTTP client.
35+
*/
36+
public WikiParser(DiscordApi api) {
37+
this(api, API_URL);
38+
}
39+
40+
/**
41+
* Creates a new Wiki parser.
42+
*
43+
* @param api The Discord Api of which to use the HTTP client.
44+
* @param apiUrl The URL for the json file with the page list.
45+
*/
46+
public WikiParser(DiscordApi api, String apiUrl) {
47+
this.discordApi = api;
48+
this.apiUrl = apiUrl;
49+
}
50+
51+
/**
52+
* Gets the pages asynchronously.
53+
*
54+
* @return The pages of the wiki.
55+
*/
56+
public CompletableFuture<Set<WikiPage>> getPages() {
57+
return CompletableFuture.supplyAsync(() -> {
58+
try {
59+
return getPagesBlocking();
60+
} catch (Throwable t) {
61+
throw new CompletionException(t);
62+
}
63+
}, discordApi.getThreadPool().getExecutorService());
64+
}
65+
66+
/**
67+
* Gets the pages synchronously.
68+
*
69+
* @return The pages of the wiki.
70+
* @throws IOException If the connection to the wiki failed.
71+
*/
72+
public Set<WikiPage> getPagesBlocking() throws IOException {
73+
Request request = new Request.Builder()
74+
.url(apiUrl)
75+
.build();
76+
77+
Response response = client.newCall(request).execute();
78+
ResponseBody body = response.body();
79+
Set<WikiPage> pages = new HashSet<>();
80+
if (body == null) {
81+
return pages;
82+
}
83+
JsonNode array = mapper.readTree(body.charStream());
84+
if (!array.isArray()) {
85+
throw new AssertionError("Format of wiki page list not as expected");
86+
}
87+
for (JsonNode node : array) {
88+
if (node.has("title") && node.has("keywords") && node.has("url") && node.has("content")) {
89+
pages.add(new WikiPage(
90+
node.get("title").asText(),
91+
asStringArray(node.get("keywords")),
92+
node.get("url").asText(),
93+
node.get("content").asText()
94+
));
95+
} else {
96+
throw new AssertionError("Format of wiki page list not as expected");
97+
}
98+
}
99+
return pages;
100+
}
101+
102+
private String[] asStringArray(JsonNode arrayNode) {
103+
if (!arrayNode.isArray()) {
104+
return new String[] {};
105+
}
106+
String[] result = new String[arrayNode.size()];
107+
int i = 0;
108+
for (JsonNode node : arrayNode) {
109+
result[i++] = node.asText();
110+
}
111+
return result;
112+
}
113+
114+
}

0 commit comments

Comments
 (0)