diff --git a/app/build.gradle b/app/build.gradle index 9f8ce80f..6d287e7a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,7 +67,6 @@ dependencies { implementation 'androidx.cardview:cardview:1.0.0' implementation 'org.twitter4j:twitter4j-core:4.0.7' - implementation 'com.github.takke:twitter4j-v2:1.0.3' //noinspection GradleDependency implementation 'com.squareup.picasso:picasso:2.8' implementation 'com.larswerkman:LicenseView:1.1' diff --git a/app/src/main/java/org/nuclearfog/twidda/activities/LoginActivity.java b/app/src/main/java/org/nuclearfog/twidda/activities/LoginActivity.java index 4317bbf0..5c691ad3 100644 --- a/app/src/main/java/org/nuclearfog/twidda/activities/LoginActivity.java +++ b/app/src/main/java/org/nuclearfog/twidda/activities/LoginActivity.java @@ -22,6 +22,7 @@ import androidx.appcompat.widget.Toolbar; import org.nuclearfog.twidda.R; import org.nuclearfog.twidda.backend.Registration; +import org.nuclearfog.twidda.backend.api.Twitter; import org.nuclearfog.twidda.backend.apiold.EngineException; import org.nuclearfog.twidda.backend.utils.AppStyles; import org.nuclearfog.twidda.backend.utils.ErrorHandler; @@ -54,6 +55,8 @@ public class LoginActivity extends AppCompatActivity implements OnClickListener private EditText pinInput; private ViewGroup root; + private String requestToken; + @Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(AppStyles.setFontScale(newBase)); @@ -156,7 +159,7 @@ public class LoginActivity extends AppCompatActivity implements OnClickListener Toast.makeText(this, R.string.info_login_to_twitter, LENGTH_LONG).show(); String twitterPin = pinInput.getText().toString(); registerAsync = new Registration(this); - registerAsync.execute(twitterPin); + registerAsync.execute(requestToken, twitterPin); } else { Toast.makeText(this, R.string.error_enter_pin, LENGTH_LONG).show(); } @@ -166,9 +169,11 @@ public class LoginActivity extends AppCompatActivity implements OnClickListener /** * Called when a twitter login link was created * - * @param link Link to twitter login page + * @param requestToken temporary request token */ - public void connect(String link) { + public void connect(String requestToken) { + this.requestToken = requestToken; + String link = Twitter.REQUEST_URL + requestToken; Intent loginIntent = new Intent(ACTION_VIEW, Uri.parse(link)); try { startActivity(loginIntent); diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/Registration.java b/app/src/main/java/org/nuclearfog/twidda/backend/Registration.java index 9a75c28f..45500a38 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/Registration.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/Registration.java @@ -3,9 +3,11 @@ package org.nuclearfog.twidda.backend; import android.os.AsyncTask; import org.nuclearfog.twidda.activities.LoginActivity; -import org.nuclearfog.twidda.backend.api.TwitterImpl; +import org.nuclearfog.twidda.backend.api.Twitter; import org.nuclearfog.twidda.database.AccountDatabase; +import org.nuclearfog.twidda.database.AppDatabase; import org.nuclearfog.twidda.database.GlobalSettings; +import org.nuclearfog.twidda.model.User; import java.lang.ref.WeakReference; @@ -19,7 +21,8 @@ public class Registration extends AsyncTask { private WeakReference callback; private AccountDatabase accountDB; - private TwitterImpl twitter; + private AppDatabase database; + private Twitter twitter; private GlobalSettings settings; /** @@ -32,8 +35,9 @@ public class Registration extends AsyncTask { this.callback = new WeakReference<>(activity); // init database and storage accountDB = new AccountDatabase(activity); + database = new AppDatabase(activity); settings = GlobalSettings.getInstance(activity); - twitter = TwitterImpl.get(activity); + twitter = Twitter.get(activity); } @@ -42,15 +46,15 @@ public class Registration extends AsyncTask { try { // check if we need to backup current session if (settings.isLoggedIn() && !accountDB.exists(settings.getCurrentUserId())) { - String[] tokens = settings.getCurrentUserAccessToken(); - accountDB.setLogin(settings.getCurrentUserId(), tokens[0], tokens[1]); + accountDB.setLogin(settings.getCurrentUserId(), settings.getAccessToken(), settings.getTokenSecret()); } // no PIN means we need to request a token to login if (param.length == 0) { - return twitter.getRequestURL(); + return twitter.getRequestToken(); } // login with pin - twitter.login(param[0]); + User user = twitter.login(param[0], param[1]); + database.storeUser(user); return ""; } catch (Exception err) { err.printStackTrace(); diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/UserLoader.java b/app/src/main/java/org/nuclearfog/twidda/backend/UserLoader.java index 824ab3cb..00380848 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/UserLoader.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/UserLoader.java @@ -4,6 +4,8 @@ import android.os.AsyncTask; import androidx.annotation.Nullable; +import org.nuclearfog.twidda.backend.api.Twitter; +import org.nuclearfog.twidda.backend.api.TwitterException; import org.nuclearfog.twidda.backend.apiold.EngineException; import org.nuclearfog.twidda.backend.apiold.TwitterEngine; import org.nuclearfog.twidda.backend.lists.Users; @@ -68,6 +70,7 @@ public class UserLoader extends AsyncTask { private EngineException twException; private final WeakReference callback; private final TwitterEngine mTwitter; + private Twitter mTwitter2; private final Type type; private final String search; @@ -78,6 +81,7 @@ public class UserLoader extends AsyncTask { super(); this.callback = new WeakReference<>(callback); mTwitter = TwitterEngine.getInstance(callback.getContext()); + mTwitter2 = Twitter.get(callback.getContext()); this.type = type; this.search = search; this.id = id; @@ -96,10 +100,10 @@ public class UserLoader extends AsyncTask { return mTwitter.getFollowing(id, cursor); case RETWEET: - return mTwitter.getRetweeter(id, cursor); + return mTwitter2.getRetweetingUsers(id); case FAVORIT: - return mTwitter.getFavoriter(id, cursor); + return mTwitter2.getLikingUsers(id); case SEARCH: return mTwitter.searchUsers(search, cursor); @@ -119,6 +123,8 @@ public class UserLoader extends AsyncTask { } } catch (EngineException twException) { this.twException = twException; + } catch (TwitterException err) { + } return null; } diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/Twitter.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/Twitter.java new file mode 100644 index 00000000..8dc152ca --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/Twitter.java @@ -0,0 +1,300 @@ +package org.nuclearfog.twidda.backend.api; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.nuclearfog.twidda.backend.lists.Users; +import org.nuclearfog.twidda.backend.utils.StringTools; +import org.nuclearfog.twidda.backend.utils.TLSSocketFactory; +import org.nuclearfog.twidda.backend.utils.Tokens; +import org.nuclearfog.twidda.model.User; +import org.nuclearfog.twidda.database.GlobalSettings; + +import java.io.IOException; +import java.security.KeyStore; +import java.util.Arrays; +import java.util.TreeSet; + +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * new API implementation to replace twitter4j and add version 2.0 support + * + * @author nuclearfog + */ +public class Twitter { + + private static final String OAUTH = "1.0"; + private static final String API = "https://api.twitter.com/"; + private static final String AUTHENTICATE = API + "oauth/authenticate"; + private static final String REQUEST_TOKEN = API + "oauth/request_token"; + private static final String OAUTH_VERIFIER = API + "oauth/access_token"; + private static final String CREDENTIALS = API + "1.1/account/verify_credentials.json"; + public static final String REQUEST_URL = AUTHENTICATE + "?oauth_token="; + public static final String SIGNATURE_ALG = "HMAC-SHA256"; + + private static Twitter instance; + + private OkHttpClient client; + private GlobalSettings settings; + private Tokens tokens; + + + private Twitter(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + try { + TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init((KeyStore) null); + X509TrustManager manager = (X509TrustManager) factory.getTrustManagers()[0]; + client = new OkHttpClient().newBuilder().sslSocketFactory(new TLSSocketFactory(), manager).build(); + } catch (Exception e) { + client = new OkHttpClient().newBuilder().build(); + } + } else { + client = new OkHttpClient().newBuilder().build(); + } + settings = GlobalSettings.getInstance(context); + tokens = Tokens.getInstance(context); + } + + /** + * get singleton instance + * + * @return instance of this class + */ + public static Twitter get(Context context) { + if (instance == null) { + instance = new Twitter(context); + } + return instance; + } + + /** + * request temporary access token to pass it to the Twitter login page + * + * @return a temporary access token created by Twitter + */ + public String getRequestToken() throws TwitterException { + try { + Response response = post(REQUEST_TOKEN); + if (response.code() == 200 && response.body() != null) { + String res = response.body().string(); + Uri uri = Uri.parse(AUTHENTICATE + "?" + res); + return uri.getQueryParameter("oauth_token"); + } else { + throw new TwitterException(response.code()); + } + } catch (IOException e) { + throw new TwitterException(e); + } + } + + /** + * login to twitter using pin and add store access tokens + * + * @param pin pin from the login website + */ + public User login(String oauth_token, String pin) throws TwitterException { + try { + if (oauth_token == null) + throw new TwitterException(TwitterException.TOKEN_NOT_SET); + String paramPin = "oauth_verifier=" + pin; + String paramToken = "oauth_token=" + oauth_token; + Response response = post(OAUTH_VERIFIER, paramPin, paramToken); + if (response.code() == 200 && response.body() != null) { + String res = response.body().string(); + // extrect tokens from link + Uri uri = Uri.parse(OAUTH_VERIFIER + "?" + res); + settings.setAccessToken(uri.getQueryParameter("oauth_token")); + settings.setTokenSecret(uri.getQueryParameter("oauth_token_secret")); + settings.setUserId(Long.parseLong(uri.getQueryParameter("user_id"))); + settings.setogin(true); + return getCredentials(); + } else { + throw new TwitterException(response.code()); + } + } catch (IOException e) { + throw new TwitterException(e); + } + } + + /** + * get credentials of the current user + * + * @return current user + */ + public User getCredentials() throws TwitterException { + try { + Response response = get(CREDENTIALS); + if (response.code() == 200 && response.body() != null) { + JSONObject json = new JSONObject(response.body().string()); + return new UserV1(json); + } else { + throw new TwitterException(response.code()); + } + } catch (IOException err) { + throw new TwitterException(err); + } catch (JSONException err) { + throw new TwitterException(err); + } + } + + /** + * get users retweeting a tweet + * + * @param tweetId ID of the tweet + * @return user list + */ + public Users getRetweetingUsers(long tweetId) throws TwitterException { + String endpoint = API + "2/tweets/" + tweetId + "/retweeted_by"; + return getUsers(endpoint); + } + + /** + * get users liking a tweet + * + * @param tweetId ID of the tweet + * @return user list + */ + public Users getLikingUsers(long tweetId) throws TwitterException { + String endpoint = API + "2/tweets/" + tweetId + "/liking_users"; + return getUsers(endpoint); + } + + /** + * get a list of twitter users + * + * @param endpoint endpoint url to get the user data from + * @return user list + */ + private Users getUsers(String endpoint) throws TwitterException { + try { + Response response = get(endpoint, UserV2.PARAMS); + if (response.code() == 200 && response.body() != null) { + JSONObject json = new JSONObject(response.body().string()); + JSONArray array = json.getJSONArray("data"); + Users users = new Users(); + long homeId = settings.getCurrentUserId(); + for (int i = 0 ; i < array.length() ; i++) { + users.add(new UserV2(array.getJSONObject(i), homeId)); + } + return users; + } else { + throw new TwitterException(response.code()); + } + } catch (IOException err) { + throw new TwitterException(err); + } catch (JSONException err) { + throw new TwitterException(err); + } + } + + /** + * create and call POST endpoint + * + * @param endpoint endpoint url + * @return http resonse + */ + private Response post(String endpoint, String... params) throws IOException { + String authHeader = buildHeader("POST", endpoint, params); + String url = appendParams(endpoint, params); + RequestBody body = RequestBody.create(MediaType.parse("text/plain"), ""); + Request request = new Request.Builder().url(url).addHeader("Authorization", authHeader).post(body).build(); + return client.newCall(request).execute(); + } + + /** + * create and call GET endpoint + * + * @param endpoint endpoint url + * @return http response + */ + private Response get(String endpoint, String... params) throws IOException { + String authHeader = buildHeader("GET", endpoint, params); + String url = appendParams(endpoint, params); + Request request = new Request.Builder().url(url).addHeader("Authorization", authHeader).get().build(); + return client.newCall(request).execute(); + } + + /** + * create http header with credentials and signature + * + * @param method endpoint method to call + * @param endpoint endpoint url + * @param params parameter to add to signature + * @return header string + */ + private String buildHeader(String method, String endpoint, String... params) { + String timeStamp = StringTools.getTimestamp(); + String random = StringTools.getRandomString(); + String signkey = tokens.getConsumerSec() + "&"; + String oauth_token_param = ""; + + // init default parameters + TreeSet sortedParams = new TreeSet<>(); + sortedParams.add("oauth_callback=oob"); + sortedParams.add("oauth_consumer_key=" + tokens.getConsumerKey()); + sortedParams.add("oauth_nonce=" + random); + sortedParams.add("oauth_signature_method=" + SIGNATURE_ALG); + sortedParams.add("oauth_timestamp=" + timeStamp); + sortedParams.add("oauth_version=" + OAUTH); + // add custom parameters + sortedParams.addAll(Arrays.asList(params)); + + // only add tokens if there is no login process + if (!REQUEST_TOKEN.equals(endpoint) && !OAUTH_VERIFIER.equals(endpoint)) { + sortedParams.add("oauth_token=" + settings.getAccessToken()); + oauth_token_param = ", oauth_token=\"" + settings.getAccessToken() + "\""; + signkey += settings.getTokenSecret(); + } + + // build string with sorted parameters + StringBuilder paramStr = new StringBuilder(); + for (String param : sortedParams) + paramStr.append(param).append('&'); + paramStr.deleteCharAt(paramStr.length() - 1); + + // calculate oauth signature + String signature = StringTools.sign(method, endpoint, paramStr.toString(), signkey); + + // create header string + return "OAuth oauth_callback=\"oob\"" + + ", oauth_consumer_key=\"" + tokens.getConsumerKey() + "\"" + + ", oauth_nonce=\""+ random + "\"" + + ", oauth_signature=\"" + signature + "\"" + + ", oauth_signature_method=\""+ SIGNATURE_ALG + "\"" + + ", oauth_timestamp=\"" + timeStamp + "\"" + + oauth_token_param + + ", oauth_version=\"" + OAUTH + "\""; + } + + /** + * build url with param + * + * @param url url without parameters + * @param params parameters + * @return url with parameters + */ + private String appendParams(String url, String[] params) { + if (params.length > 0) { + StringBuilder result = new StringBuilder(url); + result.append('?'); + for (String param : params) + result.append(param).append('&'); + result.deleteCharAt(result.length() - 1); + return result.toString(); + } + return url; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/TwitterException.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/TwitterException.java new file mode 100644 index 00000000..2f27f407 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/TwitterException.java @@ -0,0 +1,24 @@ +package org.nuclearfog.twidda.backend.api; + +import java.io.IOException; + + +public class TwitterException extends Exception { + + public static final int TOKEN_NOT_SET = 600; + + private int httpCode; + + TwitterException(Exception e) { + super(e); + httpCode = -1; + } + + TwitterException(int httpCode) { + this.httpCode = httpCode; + } + + public int getCode() { + return httpCode; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/TwitterImpl.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/TwitterImpl.java deleted file mode 100644 index 657b0b46..00000000 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/TwitterImpl.java +++ /dev/null @@ -1,196 +0,0 @@ -package org.nuclearfog.twidda.backend.api; - -import android.content.Context; -import android.net.Uri; -import android.os.Build; - -import org.json.JSONException; -import org.json.JSONObject; -import org.nuclearfog.twidda.backend.utils.StringTools; -import org.nuclearfog.twidda.backend.utils.TLSSocketFactory; -import org.nuclearfog.twidda.backend.utils.Tokens; -import org.nuclearfog.twidda.model.User; -import org.nuclearfog.twidda.database.GlobalSettings; - -import java.io.IOException; -import java.security.KeyStore; -import java.util.Arrays; -import java.util.TreeSet; - -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -/** - * new API implementation to replace twitter4j and add version 2.0 support - * - * @author nuclearfog - */ -public class TwitterImpl { - - public static final String HASH = "HMAC-SHA256"; - private static final String OAUTH = "1.0"; - - private static final String API = "https://api.twitter.com/"; - private static final String REQUEST_TOKEN = API + "oauth/request_token"; - private static final String AUTHENTICATE = API + "oauth/authenticate"; - private static final String OAUTH_VERIFIER = API + "oauth/access_token"; - private static final String CREDENTIALS = API + "1.1/account/verify_credentials.json"; - - private static TwitterImpl instance; - - private GlobalSettings settings; - private Tokens tokens; - - private OkHttpClient client; - - - private TwitterImpl(Context context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - try { - TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - factory.init((KeyStore) null); - X509TrustManager manager = (X509TrustManager) factory.getTrustManagers()[0]; - client = new OkHttpClient().newBuilder().sslSocketFactory(new TLSSocketFactory(), manager).build(); - } catch (Exception e) { - client = new OkHttpClient().newBuilder().build(); - } - } else { - client = new OkHttpClient().newBuilder().build(); - } - settings = GlobalSettings.getInstance(context); - tokens = Tokens.getInstance(context); - } - - /** - * get singleton instance - * - * @return instance of this class - */ - public static TwitterImpl get(Context context) { - if (instance == null) { - instance = new TwitterImpl(context); - } - return instance; - } - - /** - * request login page url and generate forst token - * - * @return url to the login page - */ - public String getRequestURL() throws IOException { - Response response = post(REQUEST_TOKEN, "oauth_callback=oob"); - if (response.code() == 200 && response.body() != null) { - String res = response.body().string(); - Uri uri = Uri.parse(AUTHENTICATE + "?" + res); - String token = uri.getQueryParameter("oauth_token"); - tokens.setTokens(token); - return AUTHENTICATE + "?oauth_token=" + token; - } else { - // todo add exception - } - return ""; - } - - /** - * login to twitter using pin and add store access tokens - * - * @param pin pin from the login website - */ - public void login(String pin) throws IOException, JSONException { - Response response = post(OAUTH_VERIFIER, "oauth_verifier=" + pin, "oauth_token=" + tokens.getToken()); - if (response.code() == 200 && response.body() != null) { - String res = response.body().string(); - Uri uri = Uri.parse(OAUTH_VERIFIER + "?" + res); - String token = uri.getQueryParameter("oauth_token"); - String tokenSec = uri.getQueryParameter("oauth_token_secret"); - tokens.setTokens(token, tokenSec); - settings.setUserId(getCredentials().getId()); - } else { - // todo add exception - } - } - - /** - * get credentials of the current user - * - * @return current user - */ - public User getCredentials() throws IOException, JSONException { - Response response = get(CREDENTIALS); - if (response.code() == 200 && response.body() != null) { - JSONObject json = new JSONObject(response.body().string()); - return new UserV1(json, -1); - } else { - throw new IOException(""); - } - } - - /** - * create and call POST endpoint - * - * @param endpoint endpoint url - * @param add additional parameters - * @return http resonse - */ - private Response post(String endpoint, String... add) throws IOException { - String oauth_sec = ""; - if (settings.isLoggedIn()) { - oauth_sec = tokens.getTokenSec(); - } - String param = buildParamString(add); - param += "&oauth_signature=" + StringTools.signPost(endpoint, param, tokens.getConsumerSec() + "&" +oauth_sec); - RequestBody body = RequestBody.create(MediaType.parse("text/plain"), ""); - Request request = new Request.Builder().url(endpoint + "?" + param).post(body).build(); - return client.newCall(request).execute(); - } - - /** - * create and call GET endpoint - * - * @param endpoint endpoint url - * @param add additional parameters - * @return http response - */ - private Response get(String endpoint, String... add) throws IOException { - String oauth_sec = ""; - if (settings.isLoggedIn()) { - oauth_sec = tokens.getTokenSec(); - } - String param = buildParamString(add); - param += "&oauth_signature=" + StringTools.signGet(endpoint, param, tokens.getConsumerSec() + "&" + oauth_sec); - Request request = new Request.Builder().url(endpoint + "?" + param).get().build(); - return client.newCall(request).execute(); - } - - /** - * build twitter API parameters - * - * @param add additional parameters - * @return parameter string - */ - private String buildParamString(String... add) { - // sort parameters - TreeSet params = new TreeSet<>(); - params.add("oauth_consumer_key=" + tokens.getConsumerKey()); - params.add("oauth_nonce=" + StringTools.getRandomString()); - params.add("oauth_signature_method=" + HASH); - params.add("oauth_timestamp=" + StringTools.getTimestamp()); - params.add("oauth_version=" + OAUTH); - params.addAll(Arrays.asList(add)); - if (settings.isLoggedIn()) { - params.add("oauth_token=" + tokens.getToken()); - } - // append sorted parameters to string - StringBuilder param = new StringBuilder(); - for (String e : params) - param.append(e).append('&'); - return param.deleteCharAt(param.length() - 1).toString(); - } -} diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/UserV1.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/UserV1.java index e1574715..7f6a0fc0 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/UserV1.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/UserV1.java @@ -6,6 +6,11 @@ import org.nuclearfog.twidda.model.User; import java.text.SimpleDateFormat; import java.util.Locale; +/** + * API 1.1 implementation of User + * + * @author nuclearfog + */ class UserV1 implements User { private static final SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.US); @@ -31,6 +36,12 @@ class UserV1 implements User { UserV1(JSONObject json, long twitterId) { + this(json); + isCurrentUser = twitterId == userID; + } + + + UserV1(JSONObject json) { String bannerLink = json.optString("profile_banner_url"); description = json.optString("description"); username = json.optString("name"); @@ -46,12 +57,12 @@ class UserV1 implements User { favorCount = json.optInt("favourites_count"); followReqSent = json.optBoolean("follow_request_sent"); defaultImage = json.optBoolean("default_profile_image"); - isCurrentUser = twitterId == userID; - - if (bannerLink.length() > 4) - profileBannerUrl = bannerLink.substring(0, bannerLink.length() - 4); - + profileUrl = json.optString("profile_image_url_https"); setDate(json.optString("created_at")); + isCurrentUser = true; + if (bannerLink.length() > 4) { + profileBannerUrl = bannerLink.substring(0, bannerLink.length() - 4); + } } diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/UserV2.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/UserV2.java new file mode 100644 index 00000000..5a4c9090 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/UserV2.java @@ -0,0 +1,168 @@ +package org.nuclearfog.twidda.backend.api; + +import org.json.JSONObject; +import org.nuclearfog.twidda.model.User; + +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** + * implementation of User accessed by API 2.0 + * + * @author nuclearfog + */ +class UserV2 implements User { + + /** + * extra parameters required to fetch additional data + */ + public static final String PARAMS = "user.fields=profile_image_url"; + // ISO8601 time format used by twitter + private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + + private long userID; + private long created; + private String username; + private String screenName; + private String description; + private String location; + private String profileUrl; + private String profileImageUrl; + private String profileBannerUrl; + private int following; + private int follower; + private int tweetCount; + private int favorCount; + private boolean isCurrentUser; + private boolean isVerified; + private boolean isProtected; + private boolean followReqSent; + private boolean defaultImage; + + + UserV2(JSONObject json, long twitterId) { + userID = json.optLong("id"); + username = json.optString("name"); + screenName = json.optString("username"); // username -> screenname + isProtected = json.optBoolean("protected"); + location = json.optString("location"); + profileUrl = json.optString("profile_image_url"); + description = json.optString("description"); + isVerified = json.optBoolean("verified"); + profileImageUrl = json.optString("profile_image_url"); + following = json.optInt("public_metrics.following_count"); + follower = json.optInt("public_metrics.followers_count"); + tweetCount = json.optInt("public_metrics.tweet_count"); + isCurrentUser = userID == twitterId; + setDate(json.optString("created_at")); + // not defined jet in V2 + profileBannerUrl = ""; + favorCount = 0; + followReqSent = false; + defaultImage = true; + } + + @Override + public long getId() { + return userID; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getScreenname() { + return screenName; + } + + @Override + public long getCreatedAt() { + return created; + } + + @Override + public String getImageUrl() { + return profileImageUrl; + } + + @Override + public String getBannerUrl() { + return profileBannerUrl; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String getLocation() { + return location; + } + + @Override + public String getProfileUrl() { + return profileUrl; + } + + @Override + public boolean isVerified() { + return isVerified; + } + + @Override + public boolean isProtected() { + return isProtected; + } + + @Override + public boolean followRequested() { + return followReqSent; + } + + @Override + public int getFollowing() { + return following; + } + + @Override + public int getFollower() { + return follower; + } + + @Override + public int getTweetCount() { + return tweetCount; + } + + @Override + public int getFavoriteCount() { + return favorCount; + } + + @Override + public boolean hasDefaultProfileImage() { + return defaultImage; + } + + @Override + public boolean isCurrentUser() { + return isCurrentUser; + } + + /** + * set time of account creation + * + * @param dateStr date string from twitter + */ + private void setDate(String dateStr) { + try { + created = sdf.parse(dateStr).getTime(); + } catch (Exception e) { + // make date invalid so it will be not shown + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/apiold/TwitterEngine.java b/app/src/main/java/org/nuclearfog/twidda/backend/apiold/TwitterEngine.java index 3baa95fb..b0baebe4 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/apiold/TwitterEngine.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/apiold/TwitterEngine.java @@ -15,7 +15,6 @@ import org.nuclearfog.twidda.backend.lists.UserLists; import org.nuclearfog.twidda.backend.utils.ProxySetup; import org.nuclearfog.twidda.backend.utils.TLSSocketFactory; import org.nuclearfog.twidda.backend.utils.Tokens; -import org.nuclearfog.twidda.database.AccountDatabase; import org.nuclearfog.twidda.database.ExcludeDatabase; import org.nuclearfog.twidda.database.GlobalSettings; import org.nuclearfog.twidda.model.Location; @@ -42,7 +41,6 @@ import twitter4j.DirectMessage; import twitter4j.DirectMessageList; import twitter4j.GeoLocation; import twitter4j.IDs; -import twitter4j.LikesExKt; import twitter4j.PagableResponseList; import twitter4j.Paging; import twitter4j.Query; @@ -53,10 +51,7 @@ import twitter4j.Twitter; import twitter4j.TwitterException; import twitter4j.TwitterFactory; import twitter4j.UploadedMedia; -import twitter4j.User2; -import twitter4j.UsersResponse; import twitter4j.auth.AccessToken; -import twitter4j.auth.RequestToken; import twitter4j.conf.ConfigurationBuilder; /** @@ -68,10 +63,7 @@ public class TwitterEngine { private static final TwitterEngine mTwitter = new TwitterEngine(); - @Nullable - private RequestToken reqToken; private GlobalSettings settings; - private AccountDatabase accountDB; private ExcludeDatabase excludeDB; private Tokens tokens; private Twitter twitter; @@ -123,13 +115,13 @@ public class TwitterEngine { // initialize database and settings mTwitter.settings = GlobalSettings.getInstance(context); mTwitter.tokens = Tokens.getInstance(context); - mTwitter.accountDB = new AccountDatabase(context); mTwitter.excludeDB = new ExcludeDatabase(context); // check if already logged in if (mTwitter.settings.isLoggedIn()) { // init login access - String[] keys = mTwitter.settings.getCurrentUserAccessToken(); - AccessToken token = new AccessToken(keys[0], keys[1]); + String accessToken = mTwitter.settings.getAccessToken(); + String tokenSecret = mTwitter.settings.getTokenSecret(); + AccessToken token = new AccessToken(accessToken, tokenSecret); mTwitter.initTwitter(token); } else { // init empty session @@ -139,21 +131,6 @@ public class TwitterEngine { return mTwitter; } - /** - * get singleton instance with empty session - * - * @return TwitterEngine Instance - */ - public static TwitterEngine getEmptyInstance(Context context) { - // initialize storage - mTwitter.settings = GlobalSettings.getInstance(context); - mTwitter.accountDB = new AccountDatabase(context); - // init empty session - mTwitter.isInitialized = false; - mTwitter.initTwitter(null); - return mTwitter; - } - /** * reset Twitter state */ @@ -161,54 +138,6 @@ public class TwitterEngine { mTwitter.isInitialized = false; } - /** - * Request Registration Website - * - * @return Link to App Registration - * @throws EngineException if internet connection is unavailable - */ - public String request() throws EngineException { - try { - if (reqToken == null) { - // request token without redirecting to the callback url - reqToken = twitter.getOAuthRequestToken("oob"); - } - } catch (Exception err) { - throw new EngineException(err); - } - return reqToken.getAuthenticationURL(); - } - - /** - * Get account access keys, store them and initialize Twitter login - * - * @param twitterPin PIN from the twitter login page, after successful login - * @throws EngineException if pin is false or request token is null - */ - public void initialize(String twitterPin) throws EngineException { - try { - // check if corresponding request key is valid - if (reqToken != null) { - // get login keys - AccessToken accessToken = twitter.getOAuthAccessToken(reqToken, twitterPin); - String key1 = accessToken.getToken(); - String key2 = accessToken.getTokenSecret(); - // init twitter login - initTwitter(new AccessToken(key1, key2)); - // save login to storage and database - settings.setConnection(key1, key2, twitter.getId()); - accountDB.setLogin(twitter.getId(), key1, key2); - // request token is not needed anymore - reqToken = null; - } else { - // request token does not exist, open login page first - throw new EngineException(EngineException.InternalErrorType.TOKENNOTSET); - } - } catch (Exception err) { - throw new EngineException(err); - } - } - /** * Get Home Timeline * @@ -841,54 +770,6 @@ public class TwitterEngine { } } - - /** - * Get User who retweeted a Tweet - * - * @param tweetID Tweet ID - * @return List of users - * @throws EngineException if Access is unavailable - */ - public Users getRetweeter(long tweetID, long cursor) throws EngineException { - try { - int load = settings.getListSize(); - IDs userIDs = twitter.getRetweeterIds(tweetID, load, cursor); - long[] ids = userIDs.getIDs(); - long prevCursor = cursor > 0 ? cursor : 0; - long nextCursor = userIDs.getNextCursor(); // fixme next cursor always zero - Users result = new Users(prevCursor, nextCursor); - if (ids.length > 0) { - result.addAll(convertUserList(twitter.lookupUsers(ids))); - } - return result; - } catch (Exception err) { - throw new EngineException(err); - } - } - - /** - * get user who liked a tweet - * - * @param tweetId Tweet ID - * @return list of users liking a tweet - * @throws EngineException if Access is unavailable - */ - public Users getFavoriter(long tweetId, long cursor) throws EngineException { - try { - UsersResponse response = LikesExKt.getLikingUsers(twitter, tweetId, null, null, null); - List users = response.getUsers(); - long[] ids = new long[users.size()]; - for (int i = 0 ; i < ids.length ; i++) - ids[i] = users.get(i).getId(); - // lookup users with Twitter4J for maximum compability - Users result = new Users(cursor, 0); - result.addAll(convertUserList(twitter.lookupUsers(ids))); - return result; - } catch (TwitterException err) { - throw new EngineException(err); - } - } - /** * get list of Direct Messages * diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/utils/StringTools.java b/app/src/main/java/org/nuclearfog/twidda/backend/utils/StringTools.java index 2bcb2089..d7e00365 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/utils/StringTools.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/utils/StringTools.java @@ -1,6 +1,6 @@ package org.nuclearfog.twidda.backend.utils; -import static org.nuclearfog.twidda.backend.api.TwitterImpl.HASH; +import static org.nuclearfog.twidda.backend.api.Twitter.SIGNATURE_ALG; import android.util.Base64; @@ -161,28 +161,16 @@ public final class StringTools { } /** - * sign GET API request + * generate signature for oauth * - * @param endpoint API endpoint - * @param param API parameter to sign + * @param method method e.g. POST,GET or PUT + * @param endpoint endpoint URL + * @param param parameter * @param keyString key used to sign - * @return sign string + * @return key signature */ - public static String signGet(String endpoint, String param, String keyString) { - String input = "GET&" + encode(endpoint) + "&" + encode(param); - return encode(computeSignature(input, keyString)); - } - - /** - * sign POST API request - * - * @param endpoint API endpoint - * @param param API parameter to sign - * @param keyString key used to sign - * @return sign string - */ - public static String signPost(String endpoint, String param, String keyString) { - String input = "POST&" + encode(endpoint) + "&" + encode(param); + public static String sign(String method, String endpoint, String param, String keyString) { + String input = method + "&" + encode(endpoint) + "&" + encode(param); return encode(computeSignature(input, keyString)); } @@ -195,8 +183,8 @@ public final class StringTools { */ private static String computeSignature(String baseString, String keyString) { try { - SecretKey secretKey = new SecretKeySpec(keyString.getBytes(), HASH); - Mac mac = Mac.getInstance(HASH); + SecretKey secretKey = new SecretKeySpec(keyString.getBytes(), SIGNATURE_ALG); + Mac mac = Mac.getInstance(SIGNATURE_ALG); mac.init(secretKey); return new String(Base64.encode(mac.doFinal(baseString.getBytes()), Base64.DEFAULT)).trim(); } catch (NoSuchAlgorithmException | InvalidKeyException e) { diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/utils/Tokens.java b/app/src/main/java/org/nuclearfog/twidda/backend/utils/Tokens.java index b4724e40..96a5f125 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/utils/Tokens.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/utils/Tokens.java @@ -26,7 +26,6 @@ public class Tokens { private static Tokens instance; private GlobalSettings settings; - private String token, tokenSec; private Tokens(Context context) { @@ -66,33 +65,4 @@ public class Tokens { return settings.getConsumerSecret(); return API_SECRET; } - - /** - * set oauth tokens - * - * @param tokens oauth tokens (single or pair) - */ - public void setTokens(String... tokens) { - if (tokens.length == 2) { - token = tokens[0]; - tokenSec = tokens[1]; - settings.setConnection(token, tokenSec); - } else if (tokens.length == 1) { - token = tokens[0]; - } - } - - /** - * @return first oauth token - */ - public String getToken() { - return token; - } - - /** - * @return second secret oauth token - */ - public String getTokenSec() { - return tokenSec; - } } \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/database/AccountDB.java b/app/src/main/java/org/nuclearfog/twidda/database/AccountDB.java index 37737460..505aabcb 100644 --- a/app/src/main/java/org/nuclearfog/twidda/database/AccountDB.java +++ b/app/src/main/java/org/nuclearfog/twidda/database/AccountDB.java @@ -55,10 +55,16 @@ class AccountDB implements Account { } @Override - public String[] getKeys() { - return new String[]{key1, key2}; + public String getAccessToken() { + return key1; } + @Override + public String getTokenSecret() { + return key2; + } + + @NonNull @Override public String toString() { diff --git a/app/src/main/java/org/nuclearfog/twidda/database/GlobalSettings.java b/app/src/main/java/org/nuclearfog/twidda/database/GlobalSettings.java index 6384312e..df6508ec 100644 --- a/app/src/main/java/org/nuclearfog/twidda/database/GlobalSettings.java +++ b/app/src/main/java/org/nuclearfog/twidda/database/GlobalSettings.java @@ -847,16 +847,58 @@ public class GlobalSettings { return loggedIn; } + /** - * get Access tokens + * set app login status * - * @return access tokens + * @param login true if current user is logged in successfully */ - public String[] getCurrentUserAccessToken() { - String[] out = new String[2]; - out[0] = auth_key1; - out[1] = auth_key2; - return out; + public void setogin(boolean login) { + loggedIn = login; + Editor e = settings.edit(); + e.putBoolean(LOGGED_IN, login); + e.apply(); + } + + /** + * return access token of the current user + * + * @return first access token + */ + public String getAccessToken() { + return auth_key1; + } + + /** + * set access token of the current user + * + * @param token first access token + */ + public void setAccessToken(String token) { + this.auth_key1 = token; + Editor e = settings.edit(); + e.putString(CURRENT_AUTH_KEY1, token); + e.apply(); + } + + /** + * return second access token of the current user + * @return first access token + */ + public String getTokenSecret() { + return auth_key2; + } + + /** + * set second access token of the current user + * + * @param token first access token + */ + public void setTokenSecret(String token) { + this.auth_key2 = token; + Editor e = settings.edit(); + e.putString(CURRENT_AUTH_KEY2, token); + e.apply(); } /** @@ -887,28 +929,10 @@ public class GlobalSettings { } /** - * Set Access tokens and user ID + * set current user ID * - * @param key1 1st access token - * @param key2 2nd access token - * @param userId User ID + * @param userId current user ID */ - public void setConnection(String key1, String key2, long userId) { - setConnection(key1, key2); - setUserId(userId); - } - - public void setConnection(String key1, String key2) { - this.auth_key1 = key1; - this.auth_key2 = key2; - loggedIn = true; - Editor e = settings.edit(); - e.putString(CURRENT_AUTH_KEY1, key1); - e.putString(CURRENT_AUTH_KEY2, key2); - e.putBoolean(LOGGED_IN, true); - e.apply(); - } - public void setUserId(long userId) { this.userId = userId; Editor e = settings.edit(); diff --git a/app/src/main/java/org/nuclearfog/twidda/fragments/AccountFragment.java b/app/src/main/java/org/nuclearfog/twidda/fragments/AccountFragment.java index d70d3157..f7c7d34b 100644 --- a/app/src/main/java/org/nuclearfog/twidda/fragments/AccountFragment.java +++ b/app/src/main/java/org/nuclearfog/twidda/fragments/AccountFragment.java @@ -85,8 +85,9 @@ public class AccountFragment extends ListFragment implements OnAccountClickListe @Override public void onAccountClick(Account account) { // set new account - String[] token = account.getKeys(); - settings.setConnection(token[0], token[1], account.getId()); + settings.setAccessToken(account.getAccessToken()); + settings.setTokenSecret(account.getTokenSecret()); + settings.setUserId(account.getId()); // finish activity and return to parent activity requireActivity().setResult(RET_ACCOUNT_CHANGE); requireActivity().finish(); diff --git a/app/src/main/java/org/nuclearfog/twidda/model/Account.java b/app/src/main/java/org/nuclearfog/twidda/model/Account.java index 82ca773e..8352d90b 100644 --- a/app/src/main/java/org/nuclearfog/twidda/model/Account.java +++ b/app/src/main/java/org/nuclearfog/twidda/model/Account.java @@ -26,7 +26,12 @@ public interface Account { User getUser(); /** - * @return oauth keys + * @return first access token of the user */ - String[] getKeys(); + String getAccessToken(); + + /** + * @return second access token of the user + */ + String getTokenSecret(); } \ No newline at end of file