Skip to content

Commit 424480e

Browse files
Allow2 Devruvnet
andcommitted
Add core SDK modules
Port of PHP Service SDK core classes: Allow2Client facade with Builder pattern, OAuth2Manager, PermissionChecker with caching, RequestManager, VoiceCode (HMAC-SHA256), FeedbackManager, and HTTP client interface/impl using java.net.http.HttpClient. Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent b9486c9 commit 424480e

11 files changed

Lines changed: 1868 additions & 0 deletions

src/main/java/com/allow2/service/Allow2Client.java

Lines changed: 489 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.allow2.service;
2+
3+
import com.allow2.service.exceptions.ApiException;
4+
import org.json.JSONObject;
5+
6+
import java.io.IOException;
7+
import java.net.URI;
8+
import java.net.http.HttpClient;
9+
import java.net.http.HttpRequest;
10+
import java.time.Duration;
11+
import java.util.Map;
12+
13+
/**
14+
* Default HTTP client implementation using {@link java.net.http.HttpClient}.
15+
*
16+
* <p>No external HTTP dependencies -- uses Java 11+ built-in HTTP client.</p>
17+
*/
18+
public final class Allow2HttpClient implements HttpClientInterface {
19+
20+
private static final int DEFAULT_TIMEOUT = 15;
21+
private static final String DEFAULT_USER_AGENT = "Allow2-Java-SDK/2.0";
22+
23+
private final HttpClient client;
24+
private final int timeout;
25+
private final String userAgent;
26+
27+
/**
28+
* Create an HTTP client with custom settings.
29+
*
30+
* @param timeout Request timeout in seconds (default 15).
31+
* @param userAgent User-Agent header value.
32+
* @param client Optional pre-built HttpClient instance.
33+
*/
34+
public Allow2HttpClient(int timeout, String userAgent, HttpClient client) {
35+
this.timeout = timeout;
36+
this.userAgent = userAgent != null ? userAgent : DEFAULT_USER_AGENT;
37+
this.client = client != null ? client : HttpClient.newBuilder()
38+
.connectTimeout(Duration.ofSeconds(5))
39+
.followRedirects(HttpClient.Redirect.NORMAL)
40+
.build();
41+
}
42+
43+
/**
44+
* Create an HTTP client with default settings.
45+
*/
46+
public Allow2HttpClient() {
47+
this(DEFAULT_TIMEOUT, null, null);
48+
}
49+
50+
@Override
51+
public HttpResponse post(String url, Map<String, Object> data, Map<String, String> headers) {
52+
String jsonBody = new JSONObject(data).toString();
53+
54+
HttpRequest.Builder builder = HttpRequest.newBuilder()
55+
.uri(URI.create(url))
56+
.timeout(Duration.ofSeconds(timeout))
57+
.header("Content-Type", "application/json")
58+
.header("Accept", "application/json")
59+
.header("User-Agent", userAgent)
60+
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
61+
62+
for (Map.Entry<String, String> entry : headers.entrySet()) {
63+
builder.header(entry.getKey(), entry.getValue());
64+
}
65+
66+
return execute(builder.build());
67+
}
68+
69+
@Override
70+
public HttpResponse get(String url, Map<String, String> headers) {
71+
HttpRequest.Builder builder = HttpRequest.newBuilder()
72+
.uri(URI.create(url))
73+
.timeout(Duration.ofSeconds(timeout))
74+
.header("Accept", "application/json")
75+
.header("User-Agent", userAgent)
76+
.GET();
77+
78+
for (Map.Entry<String, String> entry : headers.entrySet()) {
79+
builder.header(entry.getKey(), entry.getValue());
80+
}
81+
82+
return execute(builder.build());
83+
}
84+
85+
private HttpResponse execute(HttpRequest request) {
86+
try {
87+
java.net.http.HttpResponse<String> response = client.send(
88+
request, java.net.http.HttpResponse.BodyHandlers.ofString());
89+
return new HttpResponse(response.statusCode(), response.body());
90+
} catch (IOException e) {
91+
throw new ApiException("HTTP request failed: " + e.getMessage(), 0, null, e);
92+
} catch (InterruptedException e) {
93+
Thread.currentThread().interrupt();
94+
throw new ApiException("HTTP request interrupted: " + e.getMessage(), 0, null, e);
95+
}
96+
}
97+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.allow2.service;
2+
3+
/**
4+
* Interface for caching permission check results.
5+
*
6+
* <p>Simple key-value cache with TTL support. Values are serialized strings.</p>
7+
*/
8+
public interface CacheInterface {
9+
10+
/**
11+
* Retrieve a cached value by key.
12+
*
13+
* @param key Cache key.
14+
* @return The cached value, or null if not found or expired.
15+
*/
16+
String get(String key);
17+
18+
/**
19+
* Store a value in cache.
20+
*
21+
* @param key Cache key.
22+
* @param value Value to cache.
23+
* @param ttl Time-to-live in seconds.
24+
*/
25+
void set(String key, String value, int ttl);
26+
27+
/**
28+
* Delete a cached value.
29+
*
30+
* @param key Cache key.
31+
*/
32+
void delete(String key);
33+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package com.allow2.service;
2+
3+
import com.allow2.service.exceptions.ApiException;
4+
import com.allow2.service.exceptions.UnpairedException;
5+
import com.allow2.service.models.FeedbackCategory;
6+
import org.json.JSONArray;
7+
import org.json.JSONException;
8+
import org.json.JSONObject;
9+
10+
import java.io.UnsupportedEncodingException;
11+
import java.net.URLEncoder;
12+
import java.nio.charset.StandardCharsets;
13+
import java.util.*;
14+
15+
/**
16+
* Manages feedback submission and discussion threads via the Allow2 API.
17+
*
18+
* <p>Feedback allows users to report issues, request features, or communicate
19+
* with the Allow2 Parental Freedom support team.</p>
20+
*/
21+
public final class FeedbackManager {
22+
23+
private final HttpClientInterface httpClient;
24+
private final String apiHost;
25+
26+
public FeedbackManager(HttpClientInterface httpClient, String apiHost) {
27+
this.httpClient = httpClient;
28+
this.apiHost = apiHost;
29+
}
30+
31+
/**
32+
* Submit new feedback.
33+
*
34+
* @param accessToken Valid OAuth2 access token.
35+
* @param userId The integrating application's user ID.
36+
* @param category The feedback category.
37+
* @param message The feedback message.
38+
* @return The discussion ID for the created feedback thread.
39+
* @throws UnpairedException If the API returns 401/403.
40+
* @throws ApiException On other API failures.
41+
*/
42+
public String submit(String accessToken, String userId, FeedbackCategory category, String message) {
43+
Map<String, Object> data = new LinkedHashMap<>();
44+
data.put("access_token", accessToken);
45+
data.put("category", category.getValue());
46+
data.put("message", message);
47+
48+
HttpResponse response = httpClient.post(apiHost + "/feedback/submit", data);
49+
50+
if (response.isUnauthorized()) {
51+
throw new UnpairedException(userId);
52+
}
53+
54+
if (!response.isSuccess()) {
55+
JSONObject body = safeJson(response);
56+
throw new ApiException(
57+
"Failed to submit feedback: HTTP " + response.getStatusCode(),
58+
response.getStatusCode(),
59+
body
60+
);
61+
}
62+
63+
JSONObject responseData = response.json();
64+
if (responseData.has("discussionId")) {
65+
return responseData.getString("discussionId");
66+
}
67+
if (responseData.has("feedbackId")) {
68+
return responseData.getString("feedbackId");
69+
}
70+
if (responseData.has("id")) {
71+
return String.valueOf(responseData.get("id"));
72+
}
73+
return "";
74+
}
75+
76+
/**
77+
* Load all feedback discussions for the authenticated user.
78+
*
79+
* @param accessToken Valid OAuth2 access token.
80+
* @param userId The integrating application's user ID.
81+
* @return List of feedback discussion threads as maps.
82+
* @throws UnpairedException If the API returns 401/403.
83+
* @throws ApiException On other API failures.
84+
*/
85+
public List<Map<String, Object>> load(String accessToken, String userId) {
86+
Map<String, String> headers = new LinkedHashMap<>();
87+
headers.put("Authorization", "Bearer " + accessToken);
88+
89+
HttpResponse response = httpClient.get(apiHost + "/feedback/load", headers);
90+
91+
if (response.isUnauthorized()) {
92+
throw new UnpairedException(userId);
93+
}
94+
95+
if (!response.isSuccess()) {
96+
JSONObject body = safeJson(response);
97+
throw new ApiException(
98+
"Failed to load feedback: HTTP " + response.getStatusCode(),
99+
response.getStatusCode(),
100+
body
101+
);
102+
}
103+
104+
JSONObject data = response.json();
105+
JSONArray discussions;
106+
if (data.has("discussions")) {
107+
discussions = data.getJSONArray("discussions");
108+
} else if (data.has("feedback")) {
109+
discussions = data.getJSONArray("feedback");
110+
} else {
111+
return Collections.emptyList();
112+
}
113+
114+
List<Map<String, Object>> result = new ArrayList<>();
115+
for (int i = 0; i < discussions.length(); i++) {
116+
JSONObject item = discussions.getJSONObject(i);
117+
result.add(item.toMap());
118+
}
119+
return result;
120+
}
121+
122+
/**
123+
* Reply to an existing feedback discussion thread.
124+
*
125+
* @param accessToken Valid OAuth2 access token.
126+
* @param userId The integrating application's user ID.
127+
* @param discussionId The discussion thread ID to reply to.
128+
* @param message The reply message.
129+
* @throws UnpairedException If the API returns 401/403.
130+
* @throws ApiException On other API failures.
131+
*/
132+
public void reply(String accessToken, String userId, String discussionId, String message) {
133+
Map<String, Object> data = new LinkedHashMap<>();
134+
data.put("access_token", accessToken);
135+
data.put("message", message);
136+
137+
HttpResponse response = httpClient.post(
138+
apiHost + "/feedback/" + urlEncode(discussionId) + "/reply",
139+
data
140+
);
141+
142+
if (response.isUnauthorized()) {
143+
throw new UnpairedException(userId);
144+
}
145+
146+
if (!response.isSuccess()) {
147+
JSONObject body = safeJson(response);
148+
throw new ApiException(
149+
"Failed to reply to feedback: HTTP " + response.getStatusCode(),
150+
response.getStatusCode(),
151+
body
152+
);
153+
}
154+
}
155+
156+
private static String urlEncode(String value) {
157+
try {
158+
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
159+
} catch (UnsupportedEncodingException e) {
160+
throw new RuntimeException(e);
161+
}
162+
}
163+
164+
private static JSONObject safeJson(HttpResponse response) {
165+
try {
166+
return response.json();
167+
} catch (JSONException e) {
168+
return null;
169+
}
170+
}
171+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.allow2.service;
2+
3+
import java.util.Map;
4+
5+
/**
6+
* HTTP client interface for Allow2 API calls.
7+
*
8+
* <p>Implementations must handle JSON encoding/decoding and return
9+
* an {@link HttpResponse} value object.</p>
10+
*/
11+
public interface HttpClientInterface {
12+
13+
/**
14+
* Send a POST request.
15+
*
16+
* @param url Full URL to send to.
17+
* @param data Body data (will be JSON-encoded by the implementation).
18+
* @param headers Additional headers.
19+
* @return The HTTP response.
20+
*/
21+
HttpResponse post(String url, Map<String, Object> data, Map<String, String> headers);
22+
23+
/**
24+
* Send a POST request with no extra headers.
25+
*
26+
* @param url Full URL to send to.
27+
* @param data Body data (will be JSON-encoded by the implementation).
28+
* @return The HTTP response.
29+
*/
30+
default HttpResponse post(String url, Map<String, Object> data) {
31+
return post(url, data, Map.of());
32+
}
33+
34+
/**
35+
* Send a GET request.
36+
*
37+
* @param url Full URL to send to.
38+
* @param headers Additional headers.
39+
* @return The HTTP response.
40+
*/
41+
HttpResponse get(String url, Map<String, String> headers);
42+
43+
/**
44+
* Send a GET request with no extra headers.
45+
*
46+
* @param url Full URL to send to.
47+
* @return The HTTP response.
48+
*/
49+
default HttpResponse get(String url) {
50+
return get(url, Map.of());
51+
}
52+
}

0 commit comments

Comments
 (0)