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 org.json.JSONException;
import org.json.JSONObject;
import org.nuclearfog.twidda.model.Relation;
@ -20,13 +21,19 @@ class RelationV1 implements Relation {
private boolean canDm;
RelationV1(JSONObject json, long currentId) {
isHome = json.optLong("target_id") == currentId;
isFollowing = json.optBoolean("following");
isFollower = json.optBoolean("followed_by");
isBlocked = json.optBoolean("blocking");
isMuted = json.optBoolean("muting");
canDm = json.optBoolean("can_dm");
RelationV1(JSONObject json) throws JSONException {
JSONObject relationship = json.getJSONObject("relationship");
JSONObject source = relationship.getJSONObject("source");
JSONObject target = relationship.getJSONObject("target");
long sourceId = Long.parseLong(source.getString("id_str"));
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

View File

@ -64,8 +64,8 @@ class TweetV1 implements Tweet {
TweetV1(JSONObject json, long twitterId) throws JSONException {
id = json.optLong("id");
text = json.optString("full_text");
author = new UserV1(json.getJSONObject("user"), twitterId);
id = Long.parseLong(json.optString("id_str", "-1"));
replyId = json.optLong("in_reply_to_status_id", -1);
replyUserId = json.optLong("in_reply_to_status_id", -1);
retweetCount = json.optInt("retweet_count");
@ -73,23 +73,19 @@ class TweetV1 implements Tweet {
isFavorited = json.optBoolean("favorited");
isRetweeted = json.optBoolean("retweeted");
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"));
text = createText(json);
String replyName = json.optString("in_reply_to_screen_name");
String userMentions = StringTools.getUserMentions(text);
JSONObject locationJson = json.optJSONObject("place");
JSONObject coordinateJson = json.optJSONObject("coordinates");
JSONObject user = json.getJSONObject("user");
JSONObject quoted_tweet = json.optJSONObject("retweeted_status");
JSONObject user_retweet = json.optJSONObject("current_user_retweet");
JSONObject entities = json.optJSONObject("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.optString("type").equals("Point")) {
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")) {
this.replyName = '@' + replyName;
}
@ -115,8 +114,6 @@ class TweetV1 implements Tweet {
isRetweeted = embeddedTweet.isRetweeted();
isFavorited = embeddedTweet.isFavorited();
}
if (entities != null)
addURLs(entities);
if (extEntities != null) {
addMedia(extEntities);
}
@ -338,15 +335,16 @@ class TweetV1 implements Tweet {
}
/**
* expand URLs int the tweet text
*
* @param entities json object with tweet entities
* read tweet and expand urls
*/
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 {
JSONObject entities = json.getJSONObject("entities");
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--) {
JSONObject entry = urls.getJSONObject(i);
String link = entry.getString("expanded_url");
@ -356,9 +354,17 @@ class TweetV1 implements Tweet {
int offset = StringTools.calculateIndexOffset(text, start);
builder.replace(start + offset, end + offset, link);
}
this.text = builder.toString();
} 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) {
JSONObject json = new JSONObject(response.body().string());
if (response.code() == 200) {
JSONObject source = json.getJSONObject("relationship").getJSONObject("source");
long currentId = settings.getCurrentUserId();
return new RelationV1(source, currentId);
return new RelationV1(json);
}
throw new TwitterException(json);
}

View File

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

View File

@ -24,7 +24,7 @@ class UserV1 implements User {
private String screenName;
private String description;
private String location;
private String profileUrl;
private String url;
private String profileImageUrl;
private String profileBannerUrl;
private int following;
@ -45,50 +45,23 @@ class UserV1 implements User {
UserV1(JSONObject json) {
String profileImage = json.optString("profile_image_url_https");
profileBannerUrl = json.optString("profile_banner_url");
description = json.optString("description");
id = Long.parseLong(json.optString("id_str", "-1"));
username = json.optString("name");
screenName = '@' + json.optString("screen_name");
location = json.optString("location");
id = json.optLong("id");
isVerified = json.optBoolean("verified");
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");
follower = json.optInt("followers_count");
tweetCount = json.optInt("statuses_count");
favorCount = json.optInt("favourites_count");
followReqSent = json.optBoolean("follow_request_sent");
defaultImage = json.optBoolean("default_profile_image");
profileUrl = json.optString("profile_image_url_https");
created = StringTools.getTime(json.optString("created_at"));
// 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;
}
created = StringTools.getTime1(json.optString("created_at"));
url = getUrl(json);
}
@Override
@ -133,7 +106,7 @@ class UserV1 implements User {
@Override
public String getProfileUrl() {
return profileUrl;
return url;
}
@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 {
// 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);
for (int i = urls.length() - 1; i >= 0; i--) {
JSONObject entry = urls.getJSONObject(i);
@ -212,9 +191,45 @@ class UserV1 implements User {
int offset = StringTools.calculateIndexOffset(description, start);
builder.replace(start + offset, end + offset, link);
}
this.description = builder.toString();
return builder.toString();
} 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.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.nuclearfog.twidda.backend.utils.StringTools;
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
*
@ -22,13 +21,8 @@ class UserV2 implements User {
/**
* extra parameters required to fetch additional data
*/
public static final String PARAMS = "user.fields=profile_image_url%2Cpublic_metrics%2Cverified%2Cprotected";
/**
* date time formatter for ISO 8601
*/
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
public static final String PARAMS = "user.fields=profile_image_url%2Cpublic_metrics%2Cverified" +
"%2Cprotected%2Cdescription%2Ccreated_at%2Curl%2Centities";
private long id;
private long created;
@ -36,7 +30,7 @@ class UserV2 implements User {
private String screenName;
private String description;
private String location;
private String profileUrl;
private String url;
private String profileImageUrl;
private String profileBannerUrl;
private int following;
@ -51,16 +45,18 @@ class UserV2 implements User {
UserV2(JSONObject json, long twitterId) {
id = json.optLong("id");
id = Long.parseLong(json.optString("id", "-1"));
username = json.optString("name");
screenName = '@' + json.optString("username"); // username -> screenname
screenName = '@' + json.optString("username");
isProtected = json.optBoolean("protected");
location = json.optString("location");
profileUrl = json.optString("url");
description = json.optString("description");
isVerified = json.optBoolean("verified");
profileImageUrl = json.optString("profile_image_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");
if (metrics != null) {
@ -68,9 +64,8 @@ class UserV2 implements User {
follower = metrics.optInt("followers_count");
tweetCount = metrics.optInt("tweet_count");
}
isCurrentUser = id == twitterId;
setDate(json.optString("created_at"));
// not yet implemented in API 2.0
favorCount = 0;
followReqSent = false;
defaultImage = false;
@ -118,7 +113,7 @@ class UserV2 implements User {
@Override
public String getProfileUrl() {
return profileUrl;
return url;
}
@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) {
try {
Date date = sdf.parse(dateStr);
if (date != null)
created = date.getTime();
} catch (Exception e) {
e.printStackTrace();
private String getDescription(JSONObject json) {
String description = json.optString("description");
JSONObject entities = json.optJSONObject("entities");
if (entities != null) {
try {
JSONObject descrEntities = entities.getJSONObject("description");
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
*/
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
*/
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
*/
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
*/
public boolean hasNext() {
return nextCursor > 0;
return nextCursor != 0;
}
/**

View File

@ -25,10 +25,31 @@ import javax.crypto.spec.SecretKeySpec;
*/
public final class StringTools {
/**
* regex pattern used to get user mentions
*/
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;
/**
* random generator used to generate random strings
*/
private static Random rand = new Random();
private StringTools() {
@ -116,26 +137,40 @@ public final class StringTools {
*/
public static int countMentions(String text) {
int result = 0;
for (int i = 0; i < text.length() - 1; i++) {
if (text.charAt(i) == '@') {
char next = text.charAt(i + 1);
if ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z') || (next >= '0' && next <= '9') || next == '_') {
result++;
}
}
Matcher m = MENTION.matcher(text);
while (m.find()) {
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
* @return date time
*/
public static long getTime(String timeStr) {
public static long getTime1(String timeStr) {
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)
return date.getTime();
} catch (Exception e) {
@ -168,7 +203,7 @@ public final class StringTools {
*/
public static int calculateIndexOffset(String text, int limit) {
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
if (Character.isSurrogatePair(text.charAt(c), text.charAt(c + 1))) {
offset++;