-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathTwitchEmotesAPI.java
More file actions
247 lines (213 loc) · 7.94 KB
/
Copy pathTwitchEmotesAPI.java
File metadata and controls
247 lines (213 loc) · 7.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
package xeed.mc.streamotes.addon;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import xeed.mc.streamotes.InternalMethods;
import xeed.mc.streamotes.StreamotesCommon;
import xeed.mc.streamotes.api.EmoteLoaderException;
import xeed.mc.streamotes.emoticon.Emoticon;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.GZIPInputStream;
public class TwitchEmotesAPI {
private static final int CACHE_LIFETIME_IMAGE = 604800000; // 7 days
private static final boolean CACHE_EMOTES = true;
private static final HashMap<String, CacheEntry<JsonElement>> jsonCache = new HashMap<>();
private static final HashMap<String, CacheEntry<String>> channelIdCache = new HashMap<>();
private static final Gson gson = new Gson();
private static File cacheDir;
private static File cachedEmotes;
private record CacheEntry<T>(T item, long expTime) {
}
public static void initialize(File mcDataDir) {
cacheDir = new File(mcDataDir, "emoticons/cache");
cachedEmotes = new File(cacheDir, "images/");
}
public static void concentrateLines(BufferedReader reader, Consumer<String> action) throws IOException {
var buffer = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.isBlank() && !buffer.isEmpty()) {
line = buffer.toString();
buffer.setLength(0);
action.accept(line);
}
else if (!line.isBlank()) {
buffer.append(line);
}
}
}
public static URL getURL(String url) {
try {
return new URI(url).toURL();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public static InputStream openStream(URL url) throws IOException {
var conn = url.openConnection();
conn.setConnectTimeout(10000);
conn.setReadTimeout(15000);
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0");
conn.setRequestProperty("Accept-Encoding", "gzip");
var stream = conn.getInputStream();
return "gzip".equalsIgnoreCase(conn.getContentEncoding()) ? new GZIPInputStream(stream) : stream;
}
private static <T> T getJson(InputStream stream, Class<T> cls) throws IOException {
try (var reader = new InputStreamReader(stream)) {
return gson.fromJson(reader, cls);
}
}
public static <T extends JsonElement> JsonElement getJson(URL url, Class<T> cls) throws IOException {
var key = url.toString();
synchronized (jsonCache) {
var entry = jsonCache.get(key);
if (entry != null && entry.expTime() > System.currentTimeMillis()) return entry.item();
}
var json = getJson(openStream(url), cls);
synchronized (jsonCache) {
jsonCache.put(key, new CacheEntry<>(json, System.currentTimeMillis() + (1000 * 60)));
}
return json;
}
public static JsonObject getJsonObj(URL url) throws IOException {
return getJson(url, JsonObject.class).getAsJsonObject();
}
public static JsonArray getJsonArr(URL url) throws IOException {
return getJson(url, JsonArray.class).getAsJsonArray();
}
private static HttpURLConnection makeGQL(String name) throws IOException {
var apiURL = getURL("https://7tv.io/v3/gql");
var conn = (HttpURLConnection)apiURL.openConnection();
conn.setConnectTimeout(10000);
conn.setReadTimeout(15000);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("User-Agent", "insomnia/9.3.3");
conn.setDoOutput(true);
var query = "query FindUser($name: String!) { users(query: $name) { id username connections { id platform display_name } } }";
var vars = "{ \"name\": \"" + name + "\" }";
var body = "{\"query\": \"" + query + "\",\"operationName\": \"FindUser\",\"variables\": " + vars + "}";
try (var writer = new DataOutputStream(conn.getOutputStream())) {
writer.writeBytes(body);
writer.flush();
}
return conn;
}
public static String getChannelId(String name) throws IOException {
synchronized (channelIdCache) {
var entry = channelIdCache.get(name);
if (entry != null && entry.expTime() > System.currentTimeMillis()) return entry.item();
}
var conn = makeGQL(name);
int code = conn.getResponseCode();
if (code / 100 != 2) {
String info = IOUtils.toString(conn.getErrorStream(), StandardCharsets.UTF_8);
throw new IOException("Channel ID request for name " + name + " returned " + code + ": " + info);
}
var data = getJson(conn.getInputStream(), JsonObject.class);
try {
var users = data.getAsJsonObject("data").getAsJsonArray("users").asList();
boolean nameFound = false;
for (var uelem : users) {
var user = uelem.getAsJsonObject();
if (!user.get("username").getAsString().equalsIgnoreCase(name)) continue;
nameFound = true;
var conns = user.getAsJsonArray("connections").asList();
for (var celem : conns) {
var cdata = celem.getAsJsonObject();
if (!cdata.get("platform").getAsString().equals("TWITCH")) continue;
var channelId = cdata.get("id").getAsString();
synchronized (channelIdCache) {
channelIdCache.put(name, new CacheEntry<>(channelId, System.currentTimeMillis() + (1000 * 60 * 5)));
}
return channelId;
}
}
if (nameFound) throw new IOException("7tv profile " + name + " has no associated Twitch channel");
else throw new IOException("Channel " + name + " has no valid 7tv profile");
}
catch (NullPointerException | IndexOutOfBoundsException e) {
throw new IOException("Invalid json trying to get channel ID of " + name + ": " + data.toString(), e);
}
}
private static boolean shouldUseCacheFileImage(File file) {
return file.exists() && (System.currentTimeMillis() - file.lastModified()) <= CACHE_LIFETIME_IMAGE;
}
public static void loadEmoteImage(Emoticon emote, URI source, String cacheId, String imageId) {
var cachedImageFile = new File(cachedEmotes, cacheId + "-" + imageId + ".png");
if (CACHE_EMOTES && shouldUseCacheFileImage(cachedImageFile)) {
if (InternalMethods.loadImage(emote, cachedImageFile)) return;
else clearFileCache();
}
if (InternalMethods.loadImage(emote, source)) {
try {
var cacheDir = cachedImageFile.getParentFile();
if (CACHE_EMOTES && (cacheDir.exists() || cacheDir.mkdirs())) {
emote.writeImage(cachedImageFile);
if (emote.isAnimated()) {
Files.write(new File(cachedImageFile.getParentFile(), cachedImageFile.getName() + ".txt").toPath(),
IntStream.concat(IntStream.of(emote.getWidth(), emote.getHeight()), IntStream.of(emote.getFrameTimes()))
.mapToObj(Integer::toString).collect(Collectors.toList()),
StandardCharsets.UTF_8);
}
}
}
catch (IOException e) {
StreamotesCommon.loge("Cache writing failed for " + emote.getName(), e);
}
finally {
emote.discardBitmap();
}
}
}
public static void clearFileCache() {
try {
FileUtils.deleteDirectory(cacheDir);
}
catch (IOException e) {
StreamotesCommon.loge("Cache purge failed", e);
}
}
public static void clearJsonCache() {
synchronized (jsonCache) {
jsonCache.clear();
}
}
public static String getJsonString(JsonObject object, String name) {
JsonElement element = object.get(name);
if (element == null) {
throw new EmoteLoaderException("'" + name + "' is null");
}
try {
return element.getAsString();
}
catch (ClassCastException e) {
throw new EmoteLoaderException("name: " + name, e);
}
}
public static JsonArray getJsonArray(JsonObject object, String name) {
try {
JsonArray result = object.getAsJsonArray(name);
if (result == null) {
throw new EmoteLoaderException("'" + name + "' is null");
}
return result;
}
catch (ClassCastException e) {
throw new EmoteLoaderException("name: " + name, e);
}
}
}