This commit is contained in:
nuclearfog 2022-01-17 17:28:05 +01:00
parent 31456a9331
commit 0282ed2376
No known key found for this signature in database
GPG Key ID: AA0271FBE406DB98
9 changed files with 213 additions and 117 deletions

View File

@ -2,6 +2,7 @@ package org.nuclearfog.twidda.backend.api;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.nuclearfog.twidda.model.Relation; import org.nuclearfog.twidda.model.Relation;
@ -20,13 +21,19 @@ class RelationV1 implements Relation {
private boolean canDm; private boolean canDm;
RelationV1(JSONObject json, long currentId) { RelationV1(JSONObject json) throws JSONException {
isHome = json.optLong("target_id") == currentId; JSONObject relationship = json.getJSONObject("relationship");
isFollowing = json.optBoolean("following"); JSONObject source = relationship.getJSONObject("source");
isFollower = json.optBoolean("followed_by"); JSONObject target = relationship.getJSONObject("target");
isBlocked = json.optBoolean("blocking");
isMuted = json.optBoolean("muting"); long sourceId = Long.parseLong(source.getString("id_str"));
canDm = json.optBoolean("can_dm"); long targetId = Long.parseLong(target.getString("id_str"));
isHome = sourceId == targetId;
isFollowing = source.optBoolean("following");
isFollower = source.optBoolean("followed_by");
isBlocked = source.optBoolean("blocking");
isMuted = source.optBoolean("muting");
canDm = source.optBoolean("can_dm");
} }
@Override @Override

View File

@ -64,8 +64,8 @@ class TweetV1 implements Tweet {
TweetV1(JSONObject json, long twitterId) throws JSONException { TweetV1(JSONObject json, long twitterId) throws JSONException {
id = json.optLong("id"); author = new UserV1(json.getJSONObject("user"), twitterId);
text = json.optString("full_text"); id = Long.parseLong(json.optString("id_str", "-1"));
replyId = json.optLong("in_reply_to_status_id", -1); replyId = json.optLong("in_reply_to_status_id", -1);
replyUserId = json.optLong("in_reply_to_status_id", -1); replyUserId = json.optLong("in_reply_to_status_id", -1);
retweetCount = json.optInt("retweet_count"); retweetCount = json.optInt("retweet_count");
@ -73,23 +73,19 @@ class TweetV1 implements Tweet {
isFavorited = json.optBoolean("favorited"); isFavorited = json.optBoolean("favorited");
isRetweeted = json.optBoolean("retweeted"); isRetweeted = json.optBoolean("retweeted");
isSensitive = json.optBoolean("possibly_sensitive"); isSensitive = json.optBoolean("possibly_sensitive");
timestamp = StringTools.getTime(json.optString("created_at")); timestamp = StringTools.getTime1(json.optString("created_at"));
source = StringTools.getSource(json.optString("source")); source = StringTools.getSource(json.optString("source"));
text = createText(json);
String replyName = json.optString("in_reply_to_screen_name"); String replyName = json.optString("in_reply_to_screen_name");
String userMentions = StringTools.getUserMentions(text); String userMentions = StringTools.getUserMentions(text);
JSONObject locationJson = json.optJSONObject("place"); JSONObject locationJson = json.optJSONObject("place");
JSONObject coordinateJson = json.optJSONObject("coordinates"); JSONObject coordinateJson = json.optJSONObject("coordinates");
JSONObject user = json.getJSONObject("user");
JSONObject quoted_tweet = json.optJSONObject("retweeted_status"); JSONObject quoted_tweet = json.optJSONObject("retweeted_status");
JSONObject user_retweet = json.optJSONObject("current_user_retweet"); JSONObject user_retweet = json.optJSONObject("current_user_retweet");
JSONObject entities = json.optJSONObject("entities");
JSONObject extEntities = json.optJSONObject("extended_entities"); JSONObject extEntities = json.optJSONObject("extended_entities");
author = new UserV1(user, twitterId);
if (locationJson != null) {
location = locationJson.optString("full_name");
}
if (coordinateJson != null) { if (coordinateJson != null) {
if (coordinateJson.optString("type").equals("Point")) { if (coordinateJson.optString("type").equals("Point")) {
JSONArray coordinateArray = coordinateJson.optJSONArray("coordinates"); JSONArray coordinateArray = coordinateJson.optJSONArray("coordinates");
@ -100,6 +96,9 @@ class TweetV1 implements Tweet {
} }
} }
} }
if (locationJson != null) {
location = locationJson.optString("full_name");
}
if (!replyName.equals("null")) { if (!replyName.equals("null")) {
this.replyName = '@' + replyName; this.replyName = '@' + replyName;
} }
@ -115,8 +114,6 @@ class TweetV1 implements Tweet {
isRetweeted = embeddedTweet.isRetweeted(); isRetweeted = embeddedTweet.isRetweeted();
isFavorited = embeddedTweet.isFavorited(); isFavorited = embeddedTweet.isFavorited();
} }
if (entities != null)
addURLs(entities);
if (extEntities != null) { if (extEntities != null) {
addMedia(extEntities); addMedia(extEntities);
} }
@ -338,15 +335,16 @@ class TweetV1 implements Tweet {
} }
/** /**
* expand URLs int the tweet text * read tweet and expand urls
*
* @param entities json object with tweet entities
*/ */
private void addURLs(@NonNull JSONObject entities) { private String createText(@NonNull JSONObject json) {
String text = json.optString("full_text");
StringBuilder builder = new StringBuilder(text);
// check for shortened urls and replace them with full urls
try { try {
JSONObject entities = json.getJSONObject("entities");
JSONArray urls = entities.getJSONArray("urls"); JSONArray urls = entities.getJSONArray("urls");
// replace new line symbol with new line character
StringBuilder builder = new StringBuilder(text);
for (int i = urls.length() - 1; i >= 0; i--) { for (int i = urls.length() - 1; i >= 0; i--) {
JSONObject entry = urls.getJSONObject(i); JSONObject entry = urls.getJSONObject(i);
String link = entry.getString("expanded_url"); String link = entry.getString("expanded_url");
@ -356,9 +354,17 @@ class TweetV1 implements Tweet {
int offset = StringTools.calculateIndexOffset(text, start); int offset = StringTools.calculateIndexOffset(text, start);
builder.replace(start + offset, end + offset, link); builder.replace(start + offset, end + offset, link);
} }
this.text = builder.toString();
} catch (JSONException e) { } catch (JSONException e) {
// use default description // use default tweet text
builder = new StringBuilder(text);
} }
// replace "&" string
int index = builder.lastIndexOf("&");
while (index >= 0) {
builder.replace(index, index + 5, "&");
index = builder.lastIndexOf("&");
}
return builder.toString();
} }
} }

View File

@ -338,9 +338,7 @@ public class Twitter {
if (response.body() != null) { if (response.body() != null) {
JSONObject json = new JSONObject(response.body().string()); JSONObject json = new JSONObject(response.body().string());
if (response.code() == 200) { if (response.code() == 200) {
JSONObject source = json.getJSONObject("relationship").getJSONObject("source"); return new RelationV1(json);
long currentId = settings.getCurrentUserId();
return new RelationV1(source, currentId);
} }
throw new TwitterException(json); throw new TwitterException(json);
} }

View File

@ -31,14 +31,14 @@ class UserListV1 implements UserList {
UserListV1(JSONObject json, long currentId) throws JSONException { UserListV1(JSONObject json, long currentId) throws JSONException {
id = json.optLong("id"); id = Long.parseLong(json.optString("id_str", "-1"));
title = json.optString("name"); title = json.optString("name");
description = json.optString("description"); description = json.optString("description");
memberCount = json.optInt("member_count"); memberCount = json.optInt("member_count");
subscriberCount = json.optInt("subscriber_count"); subscriberCount = json.optInt("subscriber_count");
isPrivate = json.optString("mode").equals("private"); isPrivate = json.optString("mode").equals("private");
following = json.optBoolean("following"); following = json.optBoolean("following");
time = StringTools.getTime(json.optString("created_at")); time = StringTools.getTime1(json.optString("created_at"));
owner = new UserV1(json.getJSONObject("user")); owner = new UserV1(json.getJSONObject("user"));
isOwner = currentId == owner.getId(); isOwner = currentId == owner.getId();
} }

View File

@ -24,7 +24,7 @@ class UserV1 implements User {
private String screenName; private String screenName;
private String description; private String description;
private String location; private String location;
private String profileUrl; private String url;
private String profileImageUrl; private String profileImageUrl;
private String profileBannerUrl; private String profileBannerUrl;
private int following; private int following;
@ -45,50 +45,23 @@ class UserV1 implements User {
UserV1(JSONObject json) { UserV1(JSONObject json) {
String profileImage = json.optString("profile_image_url_https"); id = Long.parseLong(json.optString("id_str", "-1"));
profileBannerUrl = json.optString("profile_banner_url");
description = json.optString("description");
username = json.optString("name"); username = json.optString("name");
screenName = '@' + json.optString("screen_name"); screenName = '@' + json.optString("screen_name");
location = json.optString("location");
id = json.optLong("id");
isVerified = json.optBoolean("verified"); isVerified = json.optBoolean("verified");
isLocked = json.optBoolean("protected"); isLocked = json.optBoolean("protected");
profileImageUrl = getProfileImage(json);
profileBannerUrl = json.optString("profile_banner_url");
description = getDescription(json);
location = json.optString("location");
following = json.optInt("friends_count"); following = json.optInt("friends_count");
follower = json.optInt("followers_count"); follower = json.optInt("followers_count");
tweetCount = json.optInt("statuses_count"); tweetCount = json.optInt("statuses_count");
favorCount = json.optInt("favourites_count"); favorCount = json.optInt("favourites_count");
followReqSent = json.optBoolean("follow_request_sent"); followReqSent = json.optBoolean("follow_request_sent");
defaultImage = json.optBoolean("default_profile_image"); defaultImage = json.optBoolean("default_profile_image");
profileUrl = json.optString("profile_image_url_https"); created = StringTools.getTime1(json.optString("created_at"));
created = StringTools.getTime(json.optString("created_at")); url = getUrl(json);
// expand URLs
JSONObject entities = json.optJSONObject("entities");
if (entities != null) {
JSONObject url = entities.optJSONObject("url");
if (url != null) {
JSONArray urls = url.optJSONArray("urls");
if (urls != null && urls.length() > 0) {
profileUrl = urls.optJSONObject(0).optString("display_url");
}
}
JSONObject descrEntities = entities.optJSONObject("description");
if (descrEntities != null) {
JSONArray urls = descrEntities.optJSONArray("urls");
if (urls != null) {
expandDescriptionUrls(urls);
}
}
}
// set profile image url
int start = profileImage.lastIndexOf('_');
int end = profileImage.lastIndexOf('.');
if (!defaultImage && start > 0 && end > 0) {
profileImageUrl = profileImage.substring(0, start) + profileImage.substring(end);
} else {
profileImageUrl = profileImage;
}
} }
@Override @Override
@ -133,7 +106,7 @@ class UserV1 implements User {
@Override @Override
public String getProfileUrl() { public String getProfileUrl() {
return profileUrl; return url;
} }
@Override @Override
@ -195,13 +168,19 @@ class UserV1 implements User {
} }
/** /**
* expand URLs in the user description * expand URLs of the user description
* *
* @param urls json object with url information * @param json root json object of user v1
* @return user description
*/ */
private void expandDescriptionUrls(@NonNull JSONArray urls) { private String getDescription(JSONObject json) {
try { try {
// replace new line symbol with new line character JSONObject entities = json.getJSONObject("entities");
String description = json.getString("description");
JSONObject descrEntities = entities.getJSONObject("description");
JSONArray urls = descrEntities.getJSONArray("urls");
// expand shortened urls
StringBuilder builder = new StringBuilder(description); StringBuilder builder = new StringBuilder(description);
for (int i = urls.length() - 1; i >= 0; i--) { for (int i = urls.length() - 1; i >= 0; i--) {
JSONObject entry = urls.getJSONObject(i); JSONObject entry = urls.getJSONObject(i);
@ -212,9 +191,45 @@ class UserV1 implements User {
int offset = StringTools.calculateIndexOffset(description, start); int offset = StringTools.calculateIndexOffset(description, start);
builder.replace(start + offset, end + offset, link); builder.replace(start + offset, end + offset, link);
} }
this.description = builder.toString(); return builder.toString();
} catch (JSONException e) { } catch (JSONException e) {
// use default description return "";
} }
} }
/**
* get expanded profile url
*
* @param json root json object of user v1
* @return expanded url
*/
private String getUrl(JSONObject json) {
try {
JSONObject entities = json.getJSONObject("entities");
JSONObject urlJson = entities.getJSONObject("url");
JSONArray urls = urlJson.getJSONArray("urls");
if ( urls.length() > 0) {
return urls.getJSONObject(0).getString("display_url");
}
} catch (JSONException e) {
// ignore
}
return "";
}
/**
* get original sized profile image url
*
* @param json root json object of user v1
* @return profile image url
*/
private String getProfileImage(JSONObject json) {
String profileImage = json.optString("profile_image_url_https");
// set profile image url
int start = profileImage.lastIndexOf('_');
int end = profileImage.lastIndexOf('.');
if (!defaultImage && start > 0 && end > 0)
return profileImage.substring(0, start) + profileImage.substring(end);
return profileImage;
}
} }

View File

@ -3,13 +3,12 @@ package org.nuclearfog.twidda.backend.api;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.nuclearfog.twidda.backend.utils.StringTools;
import org.nuclearfog.twidda.model.User; import org.nuclearfog.twidda.model.User;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/** /**
* implementation of User accessed by API 2.0 * implementation of User accessed by API 2.0
* *
@ -22,13 +21,8 @@ class UserV2 implements User {
/** /**
* extra parameters required to fetch additional data * extra parameters required to fetch additional data
*/ */
public static final String PARAMS = "user.fields=profile_image_url%2Cpublic_metrics%2Cverified%2Cprotected"; public static final String PARAMS = "user.fields=profile_image_url%2Cpublic_metrics%2Cverified" +
"%2Cprotected%2Cdescription%2Ccreated_at%2Curl%2Centities";
/**
* date time formatter for ISO 8601
*/
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
private long id; private long id;
private long created; private long created;
@ -36,7 +30,7 @@ class UserV2 implements User {
private String screenName; private String screenName;
private String description; private String description;
private String location; private String location;
private String profileUrl; private String url;
private String profileImageUrl; private String profileImageUrl;
private String profileBannerUrl; private String profileBannerUrl;
private int following; private int following;
@ -51,16 +45,18 @@ class UserV2 implements User {
UserV2(JSONObject json, long twitterId) { UserV2(JSONObject json, long twitterId) {
id = json.optLong("id"); id = Long.parseLong(json.optString("id", "-1"));
username = json.optString("name"); username = json.optString("name");
screenName = '@' + json.optString("username"); // username -> screenname screenName = '@' + json.optString("username");
isProtected = json.optBoolean("protected"); isProtected = json.optBoolean("protected");
location = json.optString("location"); location = json.optString("location");
profileUrl = json.optString("url");
description = json.optString("description");
isVerified = json.optBoolean("verified"); isVerified = json.optBoolean("verified");
profileImageUrl = json.optString("profile_image_url"); profileImageUrl = json.optString("profile_image_url");
profileBannerUrl = json.optString("profile_banner_url"); profileBannerUrl = json.optString("profile_banner_url");
created = StringTools.getTime2(json.optString("created_at"));
description = getDescription(json);
url = getUrl(json);
isCurrentUser = id == twitterId;
JSONObject metrics = json.optJSONObject("public_metrics"); JSONObject metrics = json.optJSONObject("public_metrics");
if (metrics != null) { if (metrics != null) {
@ -68,9 +64,8 @@ class UserV2 implements User {
follower = metrics.optInt("followers_count"); follower = metrics.optInt("followers_count");
tweetCount = metrics.optInt("tweet_count"); tweetCount = metrics.optInt("tweet_count");
} }
isCurrentUser = id == twitterId;
setDate(json.optString("created_at"));
// not yet implemented in API 2.0
favorCount = 0; favorCount = 0;
followReqSent = false; followReqSent = false;
defaultImage = false; defaultImage = false;
@ -118,7 +113,7 @@ class UserV2 implements User {
@Override @Override
public String getProfileUrl() { public String getProfileUrl() {
return profileUrl; return url;
} }
@Override @Override
@ -180,17 +175,57 @@ class UserV2 implements User {
} }
/** /**
* set time of account creation * expand URLs of the user description
* *
* @param dateStr date string from twitter * @param json root json object of user v1
* @return user description
*/ */
private void setDate(String dateStr) { private String getDescription(JSONObject json) {
try { String description = json.optString("description");
Date date = sdf.parse(dateStr); JSONObject entities = json.optJSONObject("entities");
if (date != null) if (entities != null) {
created = date.getTime(); try {
} catch (Exception e) { JSONObject descrEntities = entities.getJSONObject("description");
e.printStackTrace(); JSONArray urls = descrEntities.getJSONArray("urls");
// expand shortened urls
StringBuilder builder = new StringBuilder(description);
for (int i = urls.length() - 1; i >= 0; i--) {
JSONObject entry = urls.getJSONObject(i);
String link = entry.getString("expanded_url");
int start = entry.getInt("start");
int end = entry.getInt("end");
int offset = StringTools.calculateIndexOffset(description, start);
builder.replace(start + offset, end + offset, link);
}
return builder.toString();
} catch (JSONException e) {
// ignore, use default description
}
} }
return description;
}
/**
* get expanded profile url
*
* @param json root json object of user v1
* @return expanded url
*/
private String getUrl(JSONObject json) {
JSONObject entities = json.optJSONObject("entities");
if (entities != null) {
JSONObject urlJson = entities.optJSONObject("url");
if (urlJson != null) {
try {
JSONArray urls = urlJson.getJSONArray("urls");
if (urls.length() > 0) {
return urls.getJSONObject(0).getString("display_url");
}
} catch (JSONException e) {
// ignore
}
}
}
return "";
} }
} }

View File

@ -48,7 +48,7 @@ public class UserLists extends LinkedList<UserList> {
* @return true if list is linked * @return true if list is linked
*/ */
public boolean hasPrevious() { public boolean hasPrevious() {
return prevCursor > 0; return prevCursor != 0;
} }
/** /**
@ -57,7 +57,7 @@ public class UserLists extends LinkedList<UserList> {
* @return true if list has a successor * @return true if list has a successor
*/ */
public boolean hasNext() { public boolean hasNext() {
return nextCursor > 0; return nextCursor != 0;
} }
/** /**

View File

@ -64,7 +64,7 @@ public class Users extends LinkedList<User> {
* @return true if list is linked * @return true if list is linked
*/ */
public boolean hasPrevious() { public boolean hasPrevious() {
return prevCursor > 0; return prevCursor != 0;
} }
/** /**
@ -73,7 +73,7 @@ public class Users extends LinkedList<User> {
* @return true if list has a successor * @return true if list has a successor
*/ */
public boolean hasNext() { public boolean hasNext() {
return nextCursor > 0; return nextCursor != 0;
} }
/** /**

View File

@ -25,10 +25,31 @@ import javax.crypto.spec.SecretKeySpec;
*/ */
public final class StringTools { public final class StringTools {
/**
* regex pattern used to get user mentions
*/
private static final Pattern MENTION = Pattern.compile("[@][\\w_]+"); private static final Pattern MENTION = Pattern.compile("[@][\\w_]+");
private static final SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.US);
/**
* date format used by API 1.1
* e.g. "Mon Jan 17 13:00:12 +0000 2022"
*/
private static final SimpleDateFormat dateFormat1 = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.US);
/**
* date format used by API 2.0
* e.g. "2008-08-15T13:51:34.000Z"
*/
private static final SimpleDateFormat dateFormat2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
/**
* fallback date if parsing failed
*/
private static final long DEFAULT_TIME = 0x61D99F64; private static final long DEFAULT_TIME = 0x61D99F64;
/**
* random generator used to generate random strings
*/
private static Random rand = new Random(); private static Random rand = new Random();
private StringTools() { private StringTools() {
@ -116,26 +137,40 @@ public final class StringTools {
*/ */
public static int countMentions(String text) { public static int countMentions(String text) {
int result = 0; int result = 0;
for (int i = 0; i < text.length() - 1; i++) { Matcher m = MENTION.matcher(text);
if (text.charAt(i) == '@') { while (m.find()) {
char next = text.charAt(i + 1); result++;
if ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z') || (next >= '0' && next <= '9') || next == '_') {
result++;
}
}
} }
return result; return result;
} }
/** /**
* convert Twitter ISO 8601 date time to long format * convert Twitter API 1.1 date time to long format
* *
* @param timeStr Twitter time string * @param timeStr Twitter time string
* @return date time * @return date time
*/ */
public static long getTime(String timeStr) { public static long getTime1(String timeStr) {
try { try {
Date date = sdf.parse(timeStr); Date date = dateFormat1.parse(timeStr);
if (date != null)
return date.getTime();
} catch (Exception e) {
// make date invalid so it will be not shown
e.printStackTrace();
}
return DEFAULT_TIME;
}
/**
* convert Twitter API 2 date time to long format
*
* @param timeStr Twitter time string
* @return date time
*/
public static long getTime2(String timeStr) {
try {
Date date = dateFormat2.parse(timeStr);
if (date != null) if (date != null)
return date.getTime(); return date.getTime();
} catch (Exception e) { } catch (Exception e) {
@ -168,7 +203,7 @@ public final class StringTools {
*/ */
public static int calculateIndexOffset(String text, int limit) { public static int calculateIndexOffset(String text, int limit) {
int offset = 0; int offset = 0;
for (int c = 0; c < limit - 1 && c < text.length(); c++) { for (int c = 0; c < limit - 1 && c < text.length() - 1; c++) {
// determine if a pair of chars represent an emoji // determine if a pair of chars represent an emoji
if (Character.isSurrogatePair(text.charAt(c), text.charAt(c + 1))) { if (Character.isSurrogatePair(text.charAt(c), text.charAt(c + 1))) {
offset++; offset++;