Skip to content

Commit c2ed958

Browse files
authored
Add events service (#1)
1 parent a35f475 commit c2ed958

19 files changed

+938
-0
lines changed

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Test and Build
2+
3+
on:
4+
push:
5+
branches:
6+
- 'main'
7+
tags:
8+
- 'v*'
9+
pull_request:
10+
branches:
11+
- 'main'
12+
13+
jobs:
14+
build-gradle:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v3
19+
with:
20+
fetch-depth: 0
21+
22+
- name: validate gradle wrapper
23+
uses: gradle/wrapper-validation-action@v1
24+
25+
- uses: actions/setup-java@v3
26+
with:
27+
distribution: 'temurin'
28+
java-version: '17'
29+
30+
- name: Setup Gradle
31+
uses: gradle/gradle-build-action@v2
32+
33+
- name: Run build and tests with Gradle wrapper
34+
run: ./gradlew test build
35+
36+
- name: Publish test report
37+
uses: mikepenz/action-junit-report@v3
38+
if: success() || failure()
39+
with:
40+
report_paths: 'build/test-results/test/TEST-*.xml'
41+
annotate_notice: true
42+
detailed_summary: true

build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ plugins {
33
id 'maven-publish'
44
}
55

6+
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
7+
import org.gradle.api.tasks.testing.logging.TestLogEvent
8+
69
group = 'net.luckperms'
710
version = '0.1-SNAPSHOT'
811

@@ -21,6 +24,13 @@ repositories {
2124

2225
test {
2326
useJUnitPlatform()
27+
testLogging {
28+
events = [TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.SKIPPED]
29+
exceptionFormat = TestExceptionFormat.FULL
30+
showExceptions = true
31+
showCauses = true
32+
showStackTraces = true
33+
}
2434
}
2535

2636
dependencies {
@@ -29,6 +39,9 @@ dependencies {
2939
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
3040
implementation 'com.squareup.okio:okio:1.17.5'
3141
implementation 'com.google.code.gson:gson:2.9.1'
42+
implementation('com.launchdarkly:okhttp-eventsource:4.1.1') {
43+
exclude(module: 'okhttp')
44+
}
3245

3346
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
3447
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* This file is part of LuckPerms, licensed under the MIT License.
3+
*
4+
* Copyright (c) lucko (Luck) <[email protected]>
5+
* Copyright (c) contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in all
15+
* copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
* SOFTWARE.
24+
*/
25+
26+
package net.luckperms.rest;
27+
28+
import com.google.gson.Gson;
29+
import com.launchdarkly.eventsource.ConnectStrategy;
30+
import com.launchdarkly.eventsource.EventSource;
31+
import com.launchdarkly.eventsource.FaultEvent;
32+
import com.launchdarkly.eventsource.MessageEvent;
33+
import com.launchdarkly.eventsource.StreamEvent;
34+
import net.luckperms.rest.event.EventCall;
35+
import net.luckperms.rest.event.EventProducer;
36+
import okhttp3.HttpUrl;
37+
import okhttp3.OkHttpClient;
38+
import retrofit2.Call;
39+
import retrofit2.CallAdapter;
40+
41+
import java.lang.reflect.Type;
42+
import java.util.List;
43+
import java.util.concurrent.CopyOnWriteArrayList;
44+
import java.util.concurrent.Executor;
45+
import java.util.concurrent.ExecutorService;
46+
import java.util.function.Consumer;
47+
48+
class EventCallAdapter implements CallAdapter<Object, EventCall<?>> {
49+
private final Type eventType;
50+
private final OkHttpClient client;
51+
private final ExecutorService executorService;
52+
53+
EventCallAdapter(Type eventType, OkHttpClient client, ExecutorService executorService) {
54+
this.eventType = eventType;
55+
this.client = client;
56+
this.executorService = executorService;
57+
}
58+
59+
@Override
60+
public Type responseType() {
61+
return Object.class;
62+
}
63+
64+
@Override
65+
public EventCall<Object> adapt(Call<Object> call) {
66+
return new EventCallImpl<>(call.request().url(), this.eventType, this.client, this.executorService);
67+
}
68+
69+
private static final class EventCallImpl<E> implements EventCall<E> {
70+
private final HttpUrl url;
71+
private final Type eventType;
72+
private final OkHttpClient client;
73+
private final Executor executor;
74+
75+
EventCallImpl(HttpUrl url, Type eventType, OkHttpClient client, Executor executor) {
76+
this.url = url;
77+
this.eventType = eventType;
78+
this.client = client;
79+
this.executor = executor;
80+
}
81+
82+
@Override
83+
public EventProducer<E> subscribe() throws Exception {
84+
EventSource eventSource = new EventSource.Builder(ConnectStrategy.http(this.url).httpClient(this.client)).build();
85+
eventSource.start();
86+
87+
return new EventProducerImpl<>(eventSource, this.eventType, this.executor);
88+
}
89+
}
90+
91+
private static final class EventProducerImpl<E> implements EventProducer<E> {
92+
private static final Gson GSON = new Gson();
93+
94+
private final EventSource eventSource;
95+
private final Type eventType;
96+
97+
private final List<Consumer<E>> handlers;
98+
private final List<Consumer<Exception>> errorHandlers;
99+
100+
private EventProducerImpl(EventSource eventSource, Type eventType, Executor executor) {
101+
this.eventSource = eventSource;
102+
this.eventType = eventType;
103+
this.handlers = new CopyOnWriteArrayList<>();
104+
this.errorHandlers = new CopyOnWriteArrayList<>();
105+
106+
executor.execute(this::pollForEvents);
107+
}
108+
109+
private void pollForEvents() {
110+
try {
111+
for (StreamEvent event : this.eventSource.anyEvents()) {
112+
if (event instanceof MessageEvent) {
113+
handleMessage((MessageEvent) event);
114+
} else if (event instanceof FaultEvent) {
115+
handleError(((FaultEvent) event).getCause());
116+
}
117+
}
118+
} catch (Exception e) {
119+
handleError(e);
120+
}
121+
}
122+
123+
private void handleMessage(MessageEvent e) {
124+
String eventName = e.getEventName();
125+
if (!eventName.equals("message")) {
126+
return;
127+
}
128+
129+
E parsedEvent;
130+
try {
131+
parsedEvent = GSON.fromJson(e.getData(), this.eventType);
132+
} catch (Exception ex) {
133+
handleError(ex);
134+
return;
135+
}
136+
137+
for (Consumer<E> handler : this.handlers) {
138+
try {
139+
handler.accept(parsedEvent);
140+
} catch (Exception ex) {
141+
handleError(ex);
142+
}
143+
}
144+
}
145+
146+
private void handleError(Exception e) {
147+
for (Consumer<Exception> errorHandler : this.errorHandlers) {
148+
try {
149+
errorHandler.accept(e);
150+
} catch (Exception ex) {
151+
// ignore
152+
}
153+
}
154+
}
155+
156+
@Override
157+
public void subscribe(Consumer<E> consumer) {
158+
this.handlers.add(consumer);
159+
}
160+
161+
@Override
162+
public void errorHandler(Consumer<Exception> errorHandler) {
163+
this.errorHandlers.add(errorHandler);
164+
}
165+
166+
@Override
167+
public void close() {
168+
this.eventSource.close();
169+
}
170+
}
171+
172+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* This file is part of LuckPerms, licensed under the MIT License.
3+
*
4+
* Copyright (c) lucko (Luck) <[email protected]>
5+
* Copyright (c) contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in all
15+
* copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
* SOFTWARE.
24+
*/
25+
26+
package net.luckperms.rest;
27+
28+
import net.luckperms.rest.event.EventCall;
29+
import okhttp3.OkHttpClient;
30+
import retrofit2.CallAdapter;
31+
import retrofit2.Retrofit;
32+
33+
import java.lang.annotation.Annotation;
34+
import java.lang.reflect.ParameterizedType;
35+
import java.lang.reflect.Type;
36+
import java.util.concurrent.ExecutorService;
37+
import java.util.concurrent.Executors;
38+
39+
class EventCallAdapterFactory extends CallAdapter.Factory implements AutoCloseable {
40+
private final OkHttpClient client;
41+
private final ExecutorService executorService;
42+
43+
EventCallAdapterFactory(OkHttpClient client) {
44+
this.client = client;
45+
this.executorService = Executors.newCachedThreadPool();
46+
}
47+
48+
@Override
49+
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
50+
if (getRawType(returnType) != EventCall.class) {
51+
return null;
52+
}
53+
54+
if (!(returnType instanceof ParameterizedType)) {
55+
throw new IllegalArgumentException("Return type must be parameterized as EventCall<Foo> or EventCall<? extends Foo>");
56+
}
57+
58+
Type responseType = getParameterUpperBound(0, (ParameterizedType) returnType);
59+
return new EventCallAdapter(responseType, this.client, this.executorService);
60+
}
61+
62+
@Override
63+
public void close() {
64+
this.executorService.shutdown();
65+
}
66+
}

src/main/java/net/luckperms/rest/LuckPermsRestClient.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
package net.luckperms.rest;
2727

2828
import net.luckperms.rest.service.ActionService;
29+
import net.luckperms.rest.service.EventService;
2930
import net.luckperms.rest.service.GroupService;
3031
import net.luckperms.rest.service.MiscService;
3132
import net.luckperms.rest.service.TrackService;
@@ -75,6 +76,13 @@ static Builder builder() {
7576
*/
7677
ActionService actions();
7778

79+
/**
80+
* Gets the event service.
81+
*
82+
* @return the event service
83+
*/
84+
EventService events();
85+
7886
/**
7987
* Gets the misc service.
8088
*

0 commit comments

Comments
 (0)