This repository has no description
5.0 kB
141 lines
1package us.lassul.assist;
2
3import android.content.Context;
4import android.content.SharedPreferences;
5import java.io.ByteArrayOutputStream;
6import java.io.IOException;
7import java.io.InputStream;
8import java.io.OutputStream;
9import java.net.HttpURLConnection;
10import java.net.URL;
11import java.nio.charset.StandardCharsets;
12import org.json.JSONException;
13import org.json.JSONObject;
14
15/**
16 * hermes-voice's API (tools/hermes-voice/hermes_voice.py) behind assist.lassul.us/app/, where the
17 * proxy passes the app's own bearer token through instead of asking for the web login.
18 */
19final class Api {
20 private static final String BASE = "https://assist.lassul.us/app";
21 private static final String PREFS = "api";
22
23 /** An answer other than 2xx, with the server's error text. */
24 static final class Failure extends IOException {
25 Failure(String message) {
26 super(message);
27 }
28 }
29
30 private final String token;
31
32 private Api(String token) {
33 this.token = token;
34 }
35
36 /** Null until the token is set (SetupActivity). */
37 static Api load(Context context) {
38 String token = prefs(context).getString("token", null);
39 return token == null || token.isEmpty() ? null : new Api(token);
40 }
41
42 static void saveToken(Context context, String token) {
43 prefs(context).edit().putString("token", token.trim()).apply();
44 }
45
46 static boolean hasToken(Context context) {
47 return load(context) != null;
48 }
49
50 private static SharedPreferences prefs(Context context) {
51 return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
52 }
53
54 JSONObject get(String path) throws IOException, JSONException {
55 HttpURLConnection connection = open(path, "GET");
56 try {
57 return answer(connection);
58 } finally {
59 connection.disconnect();
60 }
61 }
62
63 JSONObject post(String path, JSONObject body) throws IOException, JSONException {
64 HttpURLConnection connection = open(path, "POST");
65 try {
66 send(connection, body);
67 return answer(connection);
68 } finally {
69 connection.disconnect();
70 }
71 }
72
73 /**
74 * A POST whose answer is read as a stream (/delegate's NDJSON); the caller reads and
75 * disconnects. Throws Failure on an error answer.
76 */
77 HttpURLConnection stream(String path, JSONObject body) throws IOException, JSONException {
78 HttpURLConnection connection = open(path, "POST");
79 // The server writes a keepalive line every 10 s, so a long silence means it is gone.
80 connection.setReadTimeout(60_000);
81 send(connection, body);
82 int status = connection.getResponseCode();
83 if (status / 100 != 2) {
84 String error = errorOf(connection, status);
85 connection.disconnect();
86 throw new Failure(error);
87 }
88 return connection;
89 }
90
91 private HttpURLConnection open(String path, String method) throws IOException {
92 HttpURLConnection connection = (HttpURLConnection) new URL(BASE + path).openConnection();
93 connection.setRequestMethod(method);
94 connection.setRequestProperty("Authorization", "Bearer " + token);
95 connection.setConnectTimeout(10_000);
96 connection.setReadTimeout(30_000);
97 return connection;
98 }
99
100 private static void send(HttpURLConnection connection, JSONObject body) throws IOException {
101 byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
102 connection.setDoOutput(true);
103 connection.setRequestProperty("Content-Type", "application/json");
104 connection.setFixedLengthStreamingMode(bytes.length);
105 try (OutputStream out = connection.getOutputStream()) {
106 out.write(bytes);
107 }
108 }
109
110 private static JSONObject answer(HttpURLConnection connection)
111 throws IOException, JSONException {
112 int status = connection.getResponseCode();
113 if (status / 100 != 2) {
114 throw new Failure(errorOf(connection, status));
115 }
116 return new JSONObject(read(connection.getInputStream()));
117 }
118
119 private static String errorOf(HttpURLConnection connection, int status) {
120 try {
121 InputStream stream = connection.getErrorStream();
122 if (stream != null) {
123 return new JSONObject(read(stream)).optString("error", "HTTP " + status);
124 }
125 } catch (IOException | JSONException ignored) {
126 // The status says enough.
127 }
128 return "HTTP " + status;
129 }
130
131 private static String read(InputStream stream) throws IOException {
132 ByteArrayOutputStream out = new ByteArrayOutputStream();
133 byte[] buffer = new byte[8192];
134 try (InputStream in = stream) {
135 for (int n; (n = in.read(buffer)) > 0; ) {
136 out.write(buffer, 0, n);
137 }
138 }
139 return new String(out.toByteArray(), StandardCharsets.UTF_8);
140 }
141}