Skip to content

Commit 0f605be

Browse files
committed
feat: add webhooks
1 parent bf09f9b commit 0f605be

File tree

9 files changed

+318
-18
lines changed

9 files changed

+318
-18
lines changed

README.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,118 @@ final String widgetUrl = Widget.owner(Widget.Type.DISCORD_BOT, "5746527517457776
188188

189189
```java
190190
final String widgetUrl = Widget.social(Widget.Type.DISCORD_BOT, "574652751745777665");
191+
```
192+
193+
### Webhooks
194+
195+
#### Being notified whenever someone voted for your bot
196+
197+
##### Spring Boot
198+
199+
In your `TopggWebhookFilterConfig.java`:
200+
201+
```java
202+
import org.discordbots.api.client.entity.Vote;
203+
import org.discordbots.api.client.webhooks.SpringBoot;
204+
205+
import org.springframework.boot.web.servlet.FilterRegistrationBean;
206+
import org.springframework.context.annotation.Bean;
207+
import org.springframework.context.annotation.Configuration;
208+
209+
@Configuration
210+
public class TopggWebhookFilterConfig {
211+
@Bean
212+
public FilterRegistrationBean<SpringBoot<Vote>> registerVoteWebhook() {
213+
final FilterRegistrationBean<SpringBoot<Vote>> registrationBean = new FilterRegistrationBean<>();
214+
215+
registrationBean.setFilter(new SpringBoot<>(Vote.class, System.getenv("MY_TOPGG_WEBHOOK_SECRET")) {
216+
@Override
217+
public void callback(final Vote vote) {
218+
System.out.println("A user with the ID of " + vote.getVoterId() + " has voted us on Top.gg!");
219+
}
220+
});
221+
222+
registrationBean.addUrlPatterns("/votes");
223+
registrationBean.setOrder(1);
224+
225+
return registrationBean;
226+
}
227+
}
228+
```
229+
230+
##### Dropwizard
231+
232+
In your `MyVoteListener.java`:
233+
234+
```java
235+
import org.discordbots.api.client.entity.Vote;
236+
import org.discordbots.api.client.webhooks.Dropwizard;
237+
238+
import jakarta.ws.rs.Path;
239+
240+
@Path("/votes")
241+
public class MyVoteListener extends Dropwizard<Vote> {
242+
public MyVoteListener() {
243+
super(Vote.class, System.getenv("MY_TOPGG_WEBHOOK_SECRET"));
244+
}
245+
246+
@Override
247+
public void callback(final Vote vote) {
248+
System.out.println("A user with the ID of " + vote.getVoterId() + " has voted us on Top.gg!");
249+
}
250+
}
251+
```
252+
253+
In your `MyServer.java`:
254+
255+
```java
256+
import io.dropwizard.core.Application;
257+
import io.dropwizard.core.Configuration;
258+
import io.dropwizard.core.setup.Environment;
259+
import io.dropwizard.jersey.setup.JerseyEnvironment;
260+
261+
public class MyServer extends Application<Configuration> {
262+
public static void main(String[] args) throws Exception {
263+
new MyServer().run(args);
264+
}
265+
266+
@Override
267+
public void run(Configuration config, Environment env) {
268+
final JerseyEnvironment jersey = env.jersey();
269+
270+
jersey.register(new MyVoteListener());
271+
}
272+
}
273+
```
274+
275+
##### Eclipse Jetty
276+
277+
In your `MyServer.java`:
278+
279+
```java
280+
import org.discordbots.api.client.entity.Vote;
281+
import org.discordbots.api.client.webhooks.EclipseJetty;
282+
283+
import org.eclipse.jetty.server.Server;
284+
import org.eclipse.jetty.servlet.ServletContextHandler;
285+
import org.eclipse.jetty.servlet.ServletHolder;
286+
287+
public class MyServer {
288+
public static void main(String[] args) throws Exception {
289+
final Server server = new Server(8080);
290+
final ServletContextHandler handler = new ServletContextHandler();
291+
292+
handler.setContextPath("/");
293+
handler.addServlet(new ServletHolder(new EclipseJetty<>(Vote.class, System.getenv("MY_TOPGG_WEBHOOK_SECRET")) {
294+
@Override
295+
public void callback(final Vote vote) {
296+
System.out.println("A user with the ID of " + vote.getVoterId() + " has voted us on Top.gg!");
297+
}
298+
}), "/votes");
299+
300+
server.setHandler(handler);
301+
server.start();
302+
server.join();
303+
}
304+
}
191305
```

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ dependencies {
1616
implementation "com.squareup.okhttp3:okhttp:4.12.0"
1717
implementation "com.google.code.gson:gson:2.13.1"
1818
implementation "com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2"
19+
implementation "org.springframework.boot:spring-boot-starter-web:3.5.3"
20+
implementation "jakarta.servlet:jakarta.servlet-api:6.1.0"
21+
implementation "jakarta.ws.rs:jakarta.ws.rs-api:4.0.0"
1922
}

src/main/java/org/discordbots/api/client/entity/Vote.java

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
package org.discordbots.api.client.entity;
22

3+
import java.net.URLDecoder;
4+
import java.nio.charset.StandardCharsets;
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
38
import com.google.gson.annotations.SerializedName;
49

510
public class Vote {
611

712
@SerializedName("bot")
813
private String botId;
14+
15+
@SerializedName("guild")
16+
private String serverId;
17+
918
@SerializedName("user")
10-
private String userId;
19+
private String voterId;
1120

1221
private String type;
1322

@@ -16,22 +25,36 @@ public class Vote {
1625
@SerializedName("isWeekend")
1726
private boolean weekend;
1827

19-
20-
21-
public String getBotId() {
22-
return botId;
28+
public String getReceiverId() {
29+
return botId == null ? serverId : botId;
2330
}
2431

25-
public String getUserId() {
26-
return userId;
32+
public String getVoterId() {
33+
return voterId;
2734
}
2835

29-
public String getType() {
30-
return type;
36+
public boolean isTest() {
37+
return type.equals("test");
3138
}
3239

33-
public String getQuery() {
34-
return query;
40+
public Map<String, String> getQuery() {
41+
Map<String, String> map = new HashMap<>();
42+
43+
if (query != null) {
44+
if (query.startsWith("?")) {
45+
query = query.substring(1);
46+
}
47+
48+
for (final String param : query.split("&")) {
49+
final String[] pair = param.split("=", 2);
50+
final String key = URLDecoder.decode(pair[0], StandardCharsets.UTF_8);
51+
final String value = pair.length > 1 ? URLDecoder.decode(pair[1], StandardCharsets.UTF_8) : "";
52+
53+
map.put(key, value);
54+
}
55+
}
56+
57+
return map;
3558
}
3659

3760
public boolean isWeekend() {

src/main/java/org/discordbots/api/client/entity/VotingMultiplier.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ public class VotingMultiplier {
77
@SerializedName("is_weekend")
88
private boolean weekend;
99

10-
11-
1210
public boolean isWeekend() {
1311
return weekend;
1412
}

src/main/java/org/discordbots/api/client/impl/DiscordBotListAPIImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.discordbots.api.client.io.EmptyResponseTransformer;
2626
import org.discordbots.api.client.io.ResponseTransformer;
2727
import org.discordbots.api.client.io.UnsuccessfulHttpException;
28+
import org.json.JSONException;
2829
import org.json.JSONObject;
2930

3031
import com.fatboyindustrial.gsonjavatime.OffsetDateTimeConverter;
@@ -344,7 +345,7 @@ public void onResponse(Call call, Response response) {
344345
if (body != null) {
345346
message = (new JSONObject(body)).getString("error");
346347
}
347-
} catch (final Exception ignored) {}
348+
} catch (final JSONException ignored) {}
348349
}
349350

350351
Exception e = new UnsuccessfulHttpException(response.code(), message);

src/main/java/org/discordbots/api/client/io/DefaultResponseTransformer.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package org.discordbots.api.client.io;
22

3+
import java.io.IOException;
4+
35
import com.google.gson.Gson;
4-
import okhttp3.Response;
56

6-
import java.io.IOException;
7+
import okhttp3.Response;
8+
import okhttp3.ResponseBody;
79

810
public class DefaultResponseTransformer<E> implements ResponseTransformer<E> {
911

@@ -17,8 +19,13 @@ public DefaultResponseTransformer(Class<E> aClass, Gson gson) {
1719

1820
@Override
1921
public E transform(Response response) throws IOException {
20-
String body = response.body().string();
21-
return gson.fromJson(body, aClass);
22+
try (ResponseBody responseBody = response.body()) {
23+
if (responseBody != null) {
24+
return gson.fromJson(responseBody.charStream(), aClass);
25+
}
26+
}
27+
28+
throw new IOException("Unable to parse JSON because of malformed response body.");
2229
}
2330

2431
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.discordbots.api.client.webhooks;
2+
3+
import java.io.IOException;
4+
import java.io.InputStreamReader;
5+
6+
import com.google.gson.Gson;
7+
import com.google.gson.GsonBuilder;
8+
import com.google.gson.JsonIOException;
9+
import com.google.gson.JsonSyntaxException;
10+
11+
import jakarta.servlet.http.HttpServletRequest;
12+
import jakarta.ws.rs.POST;
13+
import jakarta.ws.rs.core.Context;
14+
import jakarta.ws.rs.core.Response;
15+
16+
public abstract class Dropwizard<T> {
17+
private final Class<T> aClass;
18+
private final String authorization;
19+
private final Gson gson;
20+
21+
public Dropwizard(final Class<T> aClass, final String authorization) {
22+
this.aClass = aClass;
23+
this.authorization = authorization;
24+
this.gson = new GsonBuilder().create();
25+
}
26+
27+
@POST
28+
public Response handle(@Context HttpServletRequest request) {
29+
final String authorizationHeader = request.getHeader("Authorization");
30+
31+
if (authorizationHeader == null || !authorizationHeader.equals(this.authorization)) {
32+
return Response.status(Response.Status.UNAUTHORIZED).entity("Unauthorized").build();
33+
}
34+
35+
try {
36+
callback(gson.fromJson(new InputStreamReader(request.getInputStream()), aClass));
37+
38+
return Response.noContent().build();
39+
} catch (final JsonSyntaxException | JsonIOException | IOException ignored) {}
40+
41+
return Response.status(Response.Status.BAD_REQUEST).entity("Bad request").build();
42+
}
43+
44+
public abstract void callback(T data);
45+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.discordbots.api.client.webhooks;
2+
3+
import java.io.IOException;
4+
import java.io.InputStreamReader;
5+
6+
import com.google.gson.Gson;
7+
import com.google.gson.GsonBuilder;
8+
import com.google.gson.JsonIOException;
9+
import com.google.gson.JsonSyntaxException;
10+
11+
import jakarta.servlet.http.HttpServlet;
12+
import jakarta.servlet.http.HttpServletRequest;
13+
import jakarta.servlet.http.HttpServletResponse;
14+
15+
public abstract class EclipseJetty<T> extends HttpServlet {
16+
private final Class<T> aClass;
17+
private final String authorization;
18+
private final Gson gson;
19+
20+
public EclipseJetty(final Class<T> aClass, final String authorization) {
21+
this.aClass = aClass;
22+
this.authorization = authorization;
23+
this.gson = new GsonBuilder().create();
24+
}
25+
26+
@Override
27+
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
28+
final String authorizationHeader = request.getHeader("Authorization");
29+
30+
if (authorizationHeader == null || !authorizationHeader.equals(this.authorization)) {
31+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
32+
response.getWriter().write("Unauthorized");
33+
34+
return;
35+
}
36+
37+
try {
38+
callback(gson.fromJson(new InputStreamReader(request.getInputStream()), aClass));
39+
40+
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
41+
response.getWriter().write("");
42+
43+
return;
44+
} catch (final JsonSyntaxException | JsonIOException | IOException ignored) {}
45+
46+
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
47+
response.getWriter().write("Bad request");
48+
}
49+
50+
public abstract void callback(T data);
51+
}

0 commit comments

Comments
 (0)