2022-04-12 08:37:39 +02:00
|
|
|
|
|
|
|
import cafe.biskuteri.hinoki.Tree;
|
|
|
|
import cafe.biskuteri.hinoki.JsonConverter;
|
|
|
|
import cafe.biskuteri.hinoki.DSVTokeniser;
|
|
|
|
import java.net.URL;
|
2022-05-03 14:40:20 +02:00
|
|
|
import java.net.URLConnection;
|
2022-04-12 08:37:39 +02:00
|
|
|
import java.net.HttpURLConnection;
|
|
|
|
import java.net.URI;
|
|
|
|
import java.net.URISyntaxException;
|
|
|
|
import java.net.URLEncoder;
|
2022-05-06 22:34:22 +02:00
|
|
|
import java.net.SocketTimeoutException;
|
2022-05-03 14:40:20 +02:00
|
|
|
import java.io.Reader;
|
|
|
|
import java.io.Writer;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.io.OutputStream;
|
2022-04-12 08:37:39 +02:00
|
|
|
import java.io.InputStreamReader;
|
|
|
|
import java.io.OutputStreamWriter;
|
2022-05-13 16:32:11 +02:00
|
|
|
import java.io.File;
|
2022-04-12 08:37:39 +02:00
|
|
|
import java.io.FileReader;
|
|
|
|
import java.io.FileWriter;
|
2022-05-13 16:32:11 +02:00
|
|
|
import java.io.FileInputStream;
|
2022-04-14 06:38:49 +02:00
|
|
|
import java.io.BufferedReader;
|
2022-04-12 08:37:39 +02:00
|
|
|
import java.io.IOException;
|
2022-04-14 06:38:49 +02:00
|
|
|
import java.io.UnsupportedEncodingException;
|
2022-04-12 08:37:39 +02:00
|
|
|
|
|
|
|
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);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-12 08:37:39 +02:00
|
|
|
conn.connect();
|
|
|
|
|
2022-05-03 14:40:20 +02:00
|
|
|
wrapResponseInTree(conn, handler);
|
2022-04-12 08:37:39 +02:00
|
|
|
}
|
|
|
|
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void
|
|
|
|
getAppCredentials(RequestListener handler)
|
|
|
|
{
|
|
|
|
assert instanceUrl != null;
|
2022-04-14 06:38:49 +02:00
|
|
|
try
|
|
|
|
{
|
2022-04-12 08:37:39 +02:00
|
|
|
URL endpoint = new URL(instanceUrl + "/api/v1/apps");
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-12 08:37:39 +02:00
|
|
|
conn.setRequestMethod("POST");
|
|
|
|
conn.setDoOutput(true);
|
|
|
|
conn.connect();
|
|
|
|
|
2022-05-03 14:40:20 +02:00
|
|
|
Writer output = owriter(conn.getOutputStream());
|
2022-04-12 08:37:39 +02:00
|
|
|
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");
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-12 08:37:39 +02:00
|
|
|
conn.setRequestMethod("POST");
|
|
|
|
conn.setDoOutput(true);
|
|
|
|
conn.connect();
|
|
|
|
|
2022-05-03 14:40:20 +02:00
|
|
|
Writer output = owriter(conn.getOutputStream());
|
2022-04-12 08:37:39 +02:00
|
|
|
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);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-14 06:38:49 +02:00
|
|
|
String s2 = "Bearer " + token;
|
|
|
|
conn.setRequestProperty("Authorization", s2);
|
2022-04-12 08:37:39 +02:00
|
|
|
conn.connect();
|
|
|
|
|
|
|
|
doStandardJsonReturn(conn, handler);
|
|
|
|
}
|
|
|
|
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
|
|
|
}
|
|
|
|
|
|
|
|
public void
|
|
|
|
getTimelinePage(
|
2022-04-16 16:54:17 +02:00
|
|
|
TimelineType type,
|
2022-04-14 06:38:49 +02:00
|
|
|
int count, String maxId, String minId,
|
2022-04-29 19:44:38 +02:00
|
|
|
String accountId, String listId,
|
2022-04-12 08:37:39 +02:00
|
|
|
RequestListener handler)
|
|
|
|
{
|
|
|
|
String token = accessToken.get("access_token").value;
|
|
|
|
|
2022-04-29 19:44:38 +02:00
|
|
|
assert !(accountId != null && listId != null);
|
2022-04-16 16:54:17 +02:00
|
|
|
|
2022-04-12 08:37:39 +02:00
|
|
|
String url = instanceUrl + "/api/v1";
|
2022-04-14 06:38:49 +02:00
|
|
|
if (accountId != null)
|
|
|
|
{
|
|
|
|
url += "/accounts/" + accountId + "/statuses";
|
|
|
|
}
|
2022-04-29 19:44:38 +02:00
|
|
|
else if (listId != null)
|
|
|
|
{
|
|
|
|
url += "/lists/" + listId;
|
|
|
|
}
|
2022-04-14 06:38:49 +02:00
|
|
|
else switch (type)
|
2022-04-12 08:37:39 +02:00
|
|
|
{
|
|
|
|
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);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-12 08:37:39 +02:00
|
|
|
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);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-12 08:37:39 +02:00
|
|
|
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);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-12 08:37:39 +02:00
|
|
|
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(
|
2022-04-14 06:38:49 +02:00
|
|
|
String text, PostVisibility visibility,
|
|
|
|
String replyTo, String contentWarning,
|
2022-04-12 08:37:39 +02:00
|
|
|
RequestListener handler)
|
|
|
|
{
|
|
|
|
String token = accessToken.get("access_token").value;
|
|
|
|
|
|
|
|
String visibilityParam = "direct";
|
2022-04-14 06:38:49 +02:00
|
|
|
switch (visibility)
|
|
|
|
{
|
2022-04-12 08:37:39 +02:00
|
|
|
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
|
|
|
|
{
|
2022-04-14 06:38:49 +02:00
|
|
|
text = encode(text);
|
|
|
|
contentWarning = encode(contentWarning);
|
2022-04-12 08:37:39 +02:00
|
|
|
|
|
|
|
URL endpoint = new URL(url);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-12 08:37:39 +02:00
|
|
|
String s1 = "Bearer " + token;
|
|
|
|
conn.setRequestProperty("Authorization", s1);
|
2022-05-31 09:39:56 +02:00
|
|
|
String s2 = Integer.toString(handler.hashCode());
|
2022-04-12 08:37:39 +02:00
|
|
|
conn.setRequestProperty("Idempotency-Key", s2);
|
|
|
|
conn.setDoOutput(true);
|
|
|
|
conn.setRequestMethod("POST");
|
|
|
|
conn.connect();
|
|
|
|
|
2022-05-03 14:40:20 +02:00
|
|
|
Writer output = owriter(conn.getOutputStream());
|
2022-04-12 08:37:39 +02:00
|
|
|
output.write("status=" + text);
|
|
|
|
output.write("&visibility=" + visibilityParam);
|
|
|
|
if (replyTo != null) {
|
|
|
|
output.write("&in_reply_to_id=" + replyTo);
|
|
|
|
}
|
2022-04-14 06:38:49 +02:00
|
|
|
if (contentWarning != null) {
|
|
|
|
output.write("&spoiler_text=" + contentWarning);
|
|
|
|
}
|
2022-04-12 08:37:39 +02:00
|
|
|
output.close();
|
|
|
|
|
|
|
|
doStandardJsonReturn(conn, handler);
|
|
|
|
}
|
|
|
|
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
|
|
|
}
|
|
|
|
|
2022-04-29 19:44:38 +02:00
|
|
|
public void
|
|
|
|
getNotifications(
|
|
|
|
int count, String maxId, String minId,
|
|
|
|
RequestListener handler)
|
|
|
|
{
|
2022-04-16 16:54:17 +02:00
|
|
|
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);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-16 16:54:17 +02:00
|
|
|
String s1 = "Bearer " + token;
|
|
|
|
conn.setRequestProperty("Authorization", s1);
|
|
|
|
conn.connect();
|
|
|
|
|
|
|
|
doStandardJsonReturn(conn, handler);
|
|
|
|
}
|
|
|
|
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
2022-04-29 19:44:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-29 19:44:38 +02:00
|
|
|
String s1 = "Bearer " + token;
|
|
|
|
conn.setRequestProperty("Authorization", s1);
|
|
|
|
conn.setRequestMethod("DELETE");
|
|
|
|
conn.connect();
|
|
|
|
|
|
|
|
doStandardJsonReturn(conn, handler);
|
|
|
|
}
|
|
|
|
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
|
|
|
}
|
|
|
|
|
|
|
|
public void
|
|
|
|
getSpecificPost(String postId, RequestListener handler)
|
|
|
|
{
|
|
|
|
String token = accessToken.get("access_token").value;
|
|
|
|
|
|
|
|
String url = instanceUrl + "/api/v1/statuses/" + postId;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
URL endpoint = new URL(url);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-29 19:44:38 +02:00
|
|
|
String s1 = "Bearer " + token;
|
|
|
|
conn.setRequestProperty("Authorization", s1);
|
|
|
|
conn.connect();
|
|
|
|
|
|
|
|
doStandardJsonReturn(conn, handler);
|
|
|
|
}
|
|
|
|
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
|
|
|
}
|
|
|
|
|
|
|
|
public void
|
|
|
|
getPostContext(String postId, RequestListener handler)
|
|
|
|
{
|
|
|
|
String token = accessToken.get("access_token").value;
|
|
|
|
|
|
|
|
String s1 = instanceUrl + "/api/v1/statuses/";
|
|
|
|
String s2 = postId + "/context";
|
|
|
|
String url = s1 + s2;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
URL endpoint = new URL(url);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-29 19:44:38 +02:00
|
|
|
String s3 = "Bearer " + token;
|
|
|
|
conn.setRequestProperty("Authorization", s3);
|
|
|
|
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);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-29 19:44:38 +02:00
|
|
|
String s1 = "Bearer " + token;
|
|
|
|
conn.setRequestProperty("Authorization", s1);
|
|
|
|
conn.connect();
|
|
|
|
|
|
|
|
doStandardJsonReturn(conn, handler);
|
|
|
|
}
|
|
|
|
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
|
|
|
}
|
2022-04-19 16:08:20 +02:00
|
|
|
|
2022-05-13 16:32:11 +02:00
|
|
|
public void
|
|
|
|
uploadFile(File file, RequestListener handler)
|
|
|
|
{
|
|
|
|
assert file != null;
|
|
|
|
assert file.canRead();
|
|
|
|
|
2022-05-20 15:14:32 +02:00
|
|
|
String bct =
|
|
|
|
"multipart/form-data; "
|
|
|
|
+ "boundary=\"JKomastoFileUpload\"";
|
|
|
|
String fsb = "--JKomastoFileUpload\r\n";
|
|
|
|
String feb = "\r\n--JKomastoFileUpload--\r\n";
|
|
|
|
String fcd =
|
|
|
|
"Content-Disposition: form-data; "
|
|
|
|
+ "name=\"file\"; "
|
|
|
|
+ "filename=\"" + file.getName() + "\"\r\n";
|
|
|
|
String fct = "Content-Type: image/png\r\n\r\n";
|
|
|
|
int contentLength = 0;
|
|
|
|
contentLength += fsb.length();
|
|
|
|
contentLength += feb.length();
|
|
|
|
contentLength += fcd.length();
|
|
|
|
contentLength += fct.length();
|
|
|
|
contentLength += file.length();
|
|
|
|
/*
|
|
|
|
* (知) This was an absurdity to debug. Contrary to
|
|
|
|
* cURL, Java sets default values for some headers,
|
|
|
|
* some of which are restricted, meaning you can't
|
|
|
|
* arbitrarily change them. Content-Length is one
|
|
|
|
* of them, set to 2^14-1 bytes. I'm pretty sure
|
|
|
|
* the file I was uploading was under this, but
|
|
|
|
* anyways one of the two parties was stopping me
|
|
|
|
* from finishing transferring my form data.
|
|
|
|
*
|
|
|
|
* They didn't mention this in the Javadocs.
|
|
|
|
* I noticed HttpURLConnection#setChunkedStreamingMode
|
|
|
|
* and #setFixedLengthStreamingMode by accident.
|
|
|
|
* Turns out, the latter is how I do what cURL and
|
|
|
|
* Firefox are doing - precalculate the exact size
|
|
|
|
* of the body and set the content length to it.
|
|
|
|
* Unfortunately, this is not flexible, we have to
|
|
|
|
* be exact. Thankfully, my answers pass..
|
|
|
|
*
|
|
|
|
* On the other side, Mastodon is obtuse as usual.
|
|
|
|
* They had code that basically throws a generic 500
|
|
|
|
* upon any sort of error from their library[1]. What
|
|
|
|
* problem the library had with my requests, I could
|
|
|
|
* never know. There is an undocumented requirement
|
|
|
|
* that you must put a filename in the content
|
|
|
|
* disposition. That one I found by guessing.
|
|
|
|
*
|
|
|
|
* I solved this with the help of -Djavax.net.debug,
|
|
|
|
* which revealed to me how my headers and body
|
|
|
|
* differed from cURL and Firefox. If this issue
|
|
|
|
* happens again, I advise giving up.
|
|
|
|
*
|
|
|
|
* [1] app/controllers/api/v1/media_controller.rb
|
|
|
|
* #create. 3 March 2022
|
|
|
|
*/
|
|
|
|
|
|
|
|
String token = accessToken.get("access_token").value;
|
2022-05-13 16:32:11 +02:00
|
|
|
String url = instanceUrl + "/api/v1/media/";
|
|
|
|
try
|
|
|
|
{
|
2022-05-20 15:14:32 +02:00
|
|
|
URL endpoint = new URL(url);
|
2022-05-13 16:32:11 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
|
|
|
String s1 = "Bearer " + token;
|
|
|
|
conn.setRequestProperty("Authorization", s1);
|
|
|
|
conn.setDoOutput(true);
|
|
|
|
conn.setRequestMethod("POST");
|
2022-05-20 15:14:32 +02:00
|
|
|
conn.setFixedLengthStreamingMode(contentLength);
|
|
|
|
conn.setRequestProperty("Content-Type", bct);
|
|
|
|
conn.setRequestProperty("Accept", "*/*");
|
|
|
|
conn.connect();
|
2022-05-13 16:32:11 +02:00
|
|
|
|
|
|
|
OutputStream ostream = conn.getOutputStream();
|
|
|
|
InputStream istream = new FileInputStream(file);
|
2022-05-20 15:14:32 +02:00
|
|
|
|
|
|
|
ostream.write(fsb.getBytes());
|
|
|
|
ostream.write(fcd.getBytes());
|
|
|
|
ostream.write(fct.getBytes());
|
|
|
|
|
|
|
|
int c; while ((c = istream.read()) != -1)
|
|
|
|
ostream.write(c);
|
|
|
|
|
|
|
|
ostream.write(feb.getBytes());
|
|
|
|
istream.close();
|
|
|
|
ostream.close();
|
2022-05-13 16:32:11 +02:00
|
|
|
|
|
|
|
wrapResponseInTree(conn, handler);
|
|
|
|
}
|
|
|
|
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
|
|
|
}
|
|
|
|
|
2022-04-14 06:38:49 +02:00
|
|
|
public void
|
|
|
|
monitorTimeline(
|
2022-05-03 14:40:20 +02:00
|
|
|
TimelineType type, ServerSideEventsListener handler)
|
2022-04-14 06:38:49 +02:00
|
|
|
{
|
|
|
|
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;
|
2022-04-16 16:54:17 +02:00
|
|
|
case HOME: url += "/user"; break;
|
2022-04-14 06:38:49 +02:00
|
|
|
default: assert false;
|
|
|
|
}
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
2022-05-06 22:34:22 +02:00
|
|
|
URL endpoint = new URL(url);
|
2022-05-03 14:40:20 +02:00
|
|
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
2022-04-14 06:38:49 +02:00
|
|
|
String s = "Bearer " + token;
|
|
|
|
conn.setRequestProperty("Authorization", s);
|
|
|
|
conn.connect();
|
|
|
|
|
|
|
|
int code = conn.getResponseCode();
|
|
|
|
if (code >= 300)
|
|
|
|
{
|
2022-05-03 14:40:20 +02:00
|
|
|
Reader input = ireader(conn.getErrorStream());
|
2022-04-14 06:38:49 +02:00
|
|
|
Tree<String> response = JsonConverter.convert(input);
|
|
|
|
input.close();
|
|
|
|
handler.requestFailed(code, response);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-05-13 16:32:11 +02:00
|
|
|
conn.setReadTimeout(500);
|
2022-05-03 14:40:20 +02:00
|
|
|
Reader input = ireader(conn.getInputStream());
|
2022-04-14 06:38:49 +02:00
|
|
|
BufferedReader br = new BufferedReader(input);
|
2022-05-03 14:40:20 +02:00
|
|
|
Thread thread = Thread.currentThread();
|
2022-05-06 22:34:22 +02:00
|
|
|
while (true) try
|
2022-05-02 05:05:36 +02:00
|
|
|
{
|
2022-04-14 06:38:49 +02:00
|
|
|
String line = br.readLine();
|
|
|
|
if (line != null) handler.lineReceived(line);
|
|
|
|
}
|
2022-05-06 22:34:22 +02:00
|
|
|
catch (SocketTimeoutException eSt)
|
|
|
|
{
|
|
|
|
if (thread.interrupted()) break;
|
|
|
|
}
|
2022-04-14 06:38:49 +02:00
|
|
|
}
|
|
|
|
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
|
|
|
}
|
|
|
|
|
2022-04-12 08:37:39 +02:00
|
|
|
// - -%- -
|
|
|
|
|
|
|
|
private void
|
2022-04-14 06:38:49 +02:00
|
|
|
doStandardJsonReturn(
|
|
|
|
HttpURLConnection conn, RequestListener handler)
|
2022-04-12 08:37:39 +02:00
|
|
|
throws IOException
|
|
|
|
{
|
|
|
|
int code = conn.getResponseCode();
|
|
|
|
if (code >= 300)
|
|
|
|
{
|
2022-05-03 14:40:20 +02:00
|
|
|
Reader input = ireader(conn.getErrorStream());
|
2022-04-12 08:37:39 +02:00
|
|
|
Tree<String> response = JsonConverter.convert(input);
|
|
|
|
input.close();
|
|
|
|
handler.requestFailed(code, response);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-05-03 14:40:20 +02:00
|
|
|
Reader input = ireader(conn.getInputStream());
|
2022-04-12 08:37:39 +02:00
|
|
|
Tree<String> response = JsonConverter.convert(input);
|
|
|
|
input.close();
|
|
|
|
handler.requestSucceeded(response);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void
|
2022-05-03 14:40:20 +02:00
|
|
|
wrapResponseInTree(
|
2022-04-14 06:38:49 +02:00
|
|
|
HttpURLConnection conn, RequestListener handler)
|
2022-04-12 08:37:39 +02:00
|
|
|
throws IOException
|
|
|
|
{
|
|
|
|
int code = conn.getResponseCode();
|
|
|
|
if (code >= 300)
|
|
|
|
{
|
2022-05-20 15:14:32 +02:00
|
|
|
Reader input = ireader(conn.getErrorStream());
|
2022-04-12 08:37:39 +02:00
|
|
|
Tree<String> response = fromPlain(input);
|
|
|
|
input.close();
|
|
|
|
handler.requestFailed(code, response);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-05-03 14:40:20 +02:00
|
|
|
Reader input = ireader(conn.getInputStream());
|
2022-04-12 08:37:39 +02:00
|
|
|
Tree<String> response = fromPlain(input);
|
|
|
|
input.close();
|
|
|
|
handler.requestSucceeded(response);
|
|
|
|
}
|
|
|
|
|
2022-04-29 19:44:38 +02:00
|
|
|
// - -%- -
|
|
|
|
|
|
|
|
public static void
|
|
|
|
debugPrint(Tree<String> tree)
|
|
|
|
{
|
|
|
|
debugPrint(tree, "");
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void
|
|
|
|
debugPrint(Tree<String> tree, String prefix)
|
|
|
|
{
|
|
|
|
System.err.print(prefix);
|
2022-05-15 00:04:46 +02:00
|
|
|
System.err.print(deescape(tree.key));
|
2022-04-29 19:44:38 +02:00
|
|
|
System.err.print(": ");
|
2022-05-15 00:04:46 +02:00
|
|
|
System.err.println(deescape(tree.value));
|
2022-04-29 19:44:38 +02:00
|
|
|
for (Tree<String> child: tree)
|
|
|
|
debugPrint(child, prefix + " ");
|
|
|
|
}
|
|
|
|
|
2022-04-12 08:37:39 +02:00
|
|
|
// - -%- -
|
|
|
|
|
2022-05-15 00:04:46 +02:00
|
|
|
private static String
|
|
|
|
deescape(String string)
|
|
|
|
{
|
|
|
|
if (string == null) return string;
|
|
|
|
string = string.replaceAll("\n", "\\\\n");
|
|
|
|
return string;
|
|
|
|
}
|
|
|
|
|
2022-04-12 08:37:39 +02:00
|
|
|
private static Tree<String>
|
2022-05-03 14:40:20 +02:00
|
|
|
fromPlain(Reader r)
|
2022-04-12 08:37:39 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-04-14 06:38:49 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-03 14:40:20 +02:00
|
|
|
private static HttpURLConnection
|
|
|
|
cast(URLConnection conn)
|
|
|
|
{
|
|
|
|
return (HttpURLConnection)conn;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static InputStreamReader
|
|
|
|
ireader(InputStream is)
|
|
|
|
throws IOException
|
|
|
|
{
|
2022-05-20 15:14:32 +02:00
|
|
|
assert is != null;
|
2022-05-03 14:40:20 +02:00
|
|
|
return new InputStreamReader(is);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static OutputStreamWriter
|
|
|
|
owriter(OutputStream os)
|
|
|
|
throws IOException
|
|
|
|
{
|
2022-05-20 15:14:32 +02:00
|
|
|
assert os != null;
|
2022-05-03 14:40:20 +02:00
|
|
|
return new OutputStreamWriter(os);
|
|
|
|
}
|
|
|
|
|
2022-04-29 19:44:38 +02:00
|
|
|
// ---%-@-%---
|
2022-04-12 08:37:39 +02:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2022-04-29 19:44:38 +02:00
|
|
|
// - -%- -
|
2022-04-12 08:37:39 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|