biskuteri-cafe-JKomasto2/MastodonApi.java
2022-04-27 21:56:25 -04:00

601 lines
18 KiB
Java

import cafe.biskuteri.hinoki.Tree;
import cafe.biskuteri.hinoki.JsonConverter;
import cafe.biskuteri.hinoki.DSVTokeniser;
import java.net.URL;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
class
MastodonApi {
private String
instanceUrl;
private Tree<String>
appCredentials,
accessToken,
accountDetails;
// - -%- -
private static final String
SCOPES = "read+write";
// ---%-@-%---
public String
getInstanceUrl() { return instanceUrl; }
public Tree<String>
getAppCredentials() { return appCredentials; }
public Tree<String>
getAccessToken() { return accessToken; }
public Tree<String>
getAccountDetails() { return accountDetails; }
public void
setInstanceUrl(String a) { instanceUrl = a; }
public void
setAppCredentials(Tree<String> a) { appCredentials = a; }
public void
setAccessToken(Tree<String> a) { accessToken = a; }
public void
setAccountDetails(Tree<String> a) { accountDetails = a; }
public void
testUrlConnection(String url, RequestListener handler)
{
try
{
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
conn.connect();
returnResponseInTree(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
getAppCredentials(RequestListener handler)
{
assert instanceUrl != null;
try
{
URL endpoint = new URL(instanceUrl + "/api/v1/apps");
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.connect();
OutputStreamWriter output;
output = new OutputStreamWriter(conn.getOutputStream());
output.write("client_name=JKomasto alpha");
output.write("&redirect_uris=urn:ietf:wg:oauth:2.0:oob");
output.write("&scopes=" + SCOPES);
output.close();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public URI
getAuthorisationURL()
{
assert instanceUrl != null;
assert appCredentials != null;
String clientId = appCredentials.get("client_id").value;
try
{
StringBuilder b = new StringBuilder();
b.append(instanceUrl);
b.append("/oauth/authorize");
// Be careful of the spelling!!
b.append("?response_type=code");
b.append("&redirect_uri=urn:ietf:wg:oauth:2.0:oob");
b.append("&scope=" + SCOPES);
b.append("&client_id=" + clientId);
return new URI(b.toString());
}
catch (URISyntaxException eUs) { assert false; return null; }
}
public void
getAccessToken(String authorisationCode, RequestListener handler)
{
assert instanceUrl != null;
assert appCredentials != null;
String id = appCredentials.get("client_id").value;
String secret = appCredentials.get("client_secret").value;
try
{
URL endpoint = new URL(instanceUrl + "/oauth/token");
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.connect();
OutputStreamWriter output;
output = new OutputStreamWriter(conn.getOutputStream());
output.write("client_id=" + id);
output.write("&client_secret=" + secret);
output.write("&redirect_uri=urn:ietf:wg:oauth:2.0:oob");
output.write("&grant_type=authorization_code");
output.write("&scope=" + SCOPES);
output.write("&code=" + authorisationCode);
output.close();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
getAccountDetails(RequestListener handler)
{
assert accessToken != null;
String token = accessToken.get("access_token").value;
try
{
String s = "/api/v1/accounts/verify_credentials";
URL endpoint = new URL(instanceUrl + s);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s2 = "Bearer " + token;
conn.setRequestProperty("Authorization", s2);
conn.connect();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
getTimelinePage(
TimelineType type,
int count, String maxId, String minId,
String accountId, String listId,
RequestListener handler)
{
String token = accessToken.get("access_token").value;
assert !(accountId != null && listId != null);
String url = instanceUrl + "/api/v1";
if (accountId != null)
{
url += "/accounts/" + accountId + "/statuses";
}
else if (listId != null)
{
url += "/lists/" + listId;
}
else switch (type)
{
case FEDERATED:
case LOCAL: url += "/timelines/public"; break;
case HOME: url += "/timelines/home"; break;
default: assert false;
}
url += "?limit=" + count;
if (maxId != null) url += "&max_id=" + maxId;
if (minId != null) url += "&min_id=" + minId;
// This is a GET endpoint, it rejects receiving
// query params through the body.
try
{
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s2 = "Bearer " + token;
conn.setRequestProperty("Authorization", s2);
conn.connect();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
setPostFavourited(
String postId, boolean favourited,
RequestListener handler)
{
String token = accessToken.get("access_token").value;
String s1 = "/api/v1/statuses/" + postId;
String s2 = favourited ? "/favourite" : "/unfavourite";
String url = instanceUrl + s1 + s2;
try
{
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s3 = "Bearer " + token;
conn.setRequestProperty("Authorization", s3);
conn.setRequestMethod("POST");
conn.connect();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
setPostBoosted(
String postId, boolean boosted,
RequestListener handler)
{
String token = accessToken.get("access_token").value;
String s1 = "/api/v1/statuses/" + postId;
String s2 = boosted ? "/reblog" : "/unreblog";
String url = instanceUrl + s1 + s2;
try
{
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s3 = "Bearer " + token;
conn.setRequestProperty("Authorization", s3);
conn.setRequestMethod("POST");
conn.connect();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
submit(
String text, PostVisibility visibility,
String replyTo, String contentWarning,
RequestListener handler)
{
String token = accessToken.get("access_token").value;
String visibilityParam = "direct";
switch (visibility)
{
case PUBLIC: visibilityParam = "public"; break;
case UNLISTED: visibilityParam = "unlisted"; break;
case FOLLOWERS: visibilityParam = "private"; break;
case MENTIONED: visibilityParam = "direct"; break;
default: assert false;
}
String url = instanceUrl + "/api/v1/statuses";
try
{
text = encode(text);
contentWarning = encode(contentWarning);
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s1 = "Bearer " + token;
conn.setRequestProperty("Authorization", s1);
String s2 = Integer.toString(text.hashCode());
conn.setRequestProperty("Idempotency-Key", s2);
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.connect();
OutputStreamWriter output;
output = new OutputStreamWriter(conn.getOutputStream());
output.write("status=" + text);
output.write("&visibility=" + visibilityParam);
if (replyTo != null) {
output.write("&in_reply_to_id=" + replyTo);
}
if (contentWarning != null) {
output.write("&spoiler_text=" + contentWarning);
}
output.close();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
getNotifications(
int count, String maxId, String minId,
RequestListener handler)
{
String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/notifications";
url += "?limit=" + count;
if (maxId != null) url += "&max_id=" + maxId;
if (minId != null) url += "&min_id=" + minId;
try
{
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s1 = "Bearer " + token;
conn.setRequestProperty("Authorization", s1);
conn.connect();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
deletePost(String postId, RequestListener handler)
{
String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/statuses/" + postId;
try
{
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s1 = "Bearer " + token;
conn.setRequestProperty("Authorization", s1);
conn.setRequestMethod("DELETE");
conn.connect();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
getAccounts(String query, RequestListener handler)
{
assert query != null;
String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/accounts/search";
url += "?q=" + encode(query);
try
{
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s1 = "Bearer " + token;
conn.setRequestProperty("Authorization", s1);
conn.connect();
doStandardJsonReturn(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
monitorTimeline(
TimelineType type, ServerSideEventsListener handler)
{
String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/streaming";
switch (type)
{
case FEDERATED: url += "/public"; break;
case LOCAL: url += "/public/local"; break;
case HOME: url += "/user"; break;
default: assert false;
}
try
{
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s = "Bearer " + token;
conn.setRequestProperty("Authorization", s);
conn.connect();
InputStreamReader input;
int code = conn.getResponseCode();
if (code >= 300)
{
input = new InputStreamReader(conn.getErrorStream());
Tree<String> response = JsonConverter.convert(input);
input.close();
handler.requestFailed(code, response);
return;
}
input = new InputStreamReader(conn.getInputStream());
BufferedReader br = new BufferedReader(input);
while (true) {
String line = br.readLine();
if (line != null) handler.lineReceived(line);
}
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
// - -%- -
private void
doStandardJsonReturn(
HttpURLConnection conn, RequestListener handler)
throws IOException
{
InputStreamReader input;
int code = conn.getResponseCode();
if (code >= 300)
{
input = new InputStreamReader(conn.getErrorStream());
Tree<String> response = JsonConverter.convert(input);
input.close();
handler.requestFailed(code, response);
return;
}
input = new InputStreamReader(conn.getInputStream());
Tree<String> response = JsonConverter.convert(input);
input.close();
handler.requestSucceeded(response);
}
private void
returnResponseInTree(
HttpURLConnection conn, RequestListener handler)
throws IOException
{
InputStreamReader input;
int code = conn.getResponseCode();
if (code >= 300)
{
input = new InputStreamReader(conn.getErrorStream());
Tree<String> response = fromPlain(input);
input.close();
handler.requestFailed(code, response);
return;
}
input = new InputStreamReader(conn.getInputStream());
Tree<String> response = fromPlain(input);
input.close();
handler.requestSucceeded(response);
}
// - -%- -
private static Tree<String>
fromPlain(InputStreamReader r)
throws IOException
{
StringBuilder b = new StringBuilder();
int c; while ((c = r.read()) != -1) b.append((char)c);
Tree<String> leaf = new Tree<String>();
leaf.key = "body";
leaf.value = b.toString();
Tree<String> doc = new Tree<String>();
doc.add(leaf);
return doc;
}
private static String
encode(String s)
{
try {
if (s == null) return null;
return URLEncoder.encode(s, "UTF-8");
}
catch (UnsupportedEncodingException eUe) {
assert false;
return null;
}
}
// ---%-@-%---
public void
loadCache()
throws IOException
{
FileReader r = new FileReader(getCachePath());
DSVTokeniser.Options o = new DSVTokeniser.Options();
Tree<String> row1 = DSVTokeniser.tokenise(r, o);
Tree<String> row2 = DSVTokeniser.tokenise(r, o);
Tree<String> row3 = DSVTokeniser.tokenise(r, o);
assert !row1.get(0).value.equals(o.endOfStreamValue);
assert !row2.get(0).value.equals(o.endOfStreamValue);
assert !row3.get(0).value.equals(o.endOfStreamValue);
r.close();
// Prepare to bark like mad.
boolean yes10 = !row1.get(0).value.equals(o.endOfStreamValue);
boolean yes20 = !row2.get(0).value.equals(o.endOfStreamValue);
boolean yes30 = !row3.get(0).value.equals(o.endOfStreamValue);
boolean yes11 = row1.size() == 1;
boolean yes21 = row2.size() == 2;
boolean yes31 = row3.size() == 1;
boolean all = yes10 & yes20 & yes30 & yes11 & yes21 & yes31;
if (!all) {
throw new IOException("Cache has invalid format!");
}
setInstanceUrl(row1.get(0).value);
appCredentials = new Tree<String>();
appCredentials.add(new Tree<String>());
appCredentials.add(new Tree<String>());
appCredentials.get(0).key = "client_id";
appCredentials.get(0).value = row2.get(0).value;
appCredentials.get(1).key = "client_secret";
appCredentials.get(1).value = row2.get(1).value;
accessToken = new Tree<String>();
accessToken.add(new Tree<String>());
accessToken.get(0).key = "access_token";
accessToken.get(0).value = row3.get(0).value;
}
public void
saveToCache()
throws IOException
{
String f10 = instanceUrl;
String f20 = appCredentials.get("client_id").value;
String f21 = appCredentials.get("client_secret").value;
String f30 = accessToken.get("access_token").value;
f10 = f10.replaceAll(":", "\\\\:") + "\n";
f20 = f20.replaceAll(":", "\\\\:") + ":";
f21 = f21.replaceAll(":", "\\\\:") + "\n";
f30 = f30.replaceAll(":", "\\\\:") + "\n";
FileWriter w = new FileWriter(getCachePath());
w.write(f10);
w.write(f20);
w.write(f21);
w.write(f30);
w.close();
}
// - -%- -
private static String
getCachePath()
{
String userHome = System.getProperty("user.home");
String osName = System.getProperty("os.name");
boolean isWindows = osName.contains("Windows");
boolean isUnix = !isWindows;
// We assume. If you're running JKomasto in classic Mac OS
// for some reason, you should probably edit the code..
String configDir = isWindows ? "AppData/Local" : ".config";
String filename = "jkomasto.cache.dsv";
String path = userHome + "/" + configDir + "/" + filename;
return path;
}
}