diff --git a/build.gradle b/build.gradle index b5860ce15..ad53fbe77 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,7 @@ subprojects { Kotlin : '1.1.1', SupportLib : '25.3.1', MariotakuCommons : '0.9.13', - RestFu : '0.9.49', + RestFu : '0.9.50', ObjectCursor : '0.9.16', PlayServices : '10.2.1', MapsUtils : '0.4.4', diff --git a/twidere.component.common/build.gradle b/twidere.component.common/build.gradle index 4b607415e..0f39bb64f 100644 --- a/twidere.component.common/build.gradle +++ b/twidere.component.common/build.gradle @@ -45,6 +45,7 @@ dependencies { compile "com.bluelinelabs:logansquare:${libVersions['LoganSquare']}" compile "com.github.mariotaku.RestFu:library:${libVersions['RestFu']}" compile "com.github.mariotaku.RestFu:oauth:${libVersions['RestFu']}" + compile "com.github.mariotaku.RestFu:oauth2:${libVersions['RestFu']}" compile 'com.hannesdorfmann.parcelableplease:annotation:1.0.2' compile "com.github.mariotaku.ObjectCursor:core:${libVersions['ObjectCursor']}" compile "com.github.mariotaku.CommonsLibrary:objectcursor:${libVersions['MariotakuCommons']}" diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/Mastodon.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/Mastodon.java index 84ef50dff..c13640ffa 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/Mastodon.java +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/Mastodon.java @@ -21,8 +21,27 @@ package org.mariotaku.microblog.library.mastodon; +import org.mariotaku.microblog.library.mastodon.api.AccountResources; +import org.mariotaku.microblog.library.mastodon.api.ApplicationResources; +import org.mariotaku.microblog.library.mastodon.api.BlockResources; +import org.mariotaku.microblog.library.mastodon.api.FavouriteResources; +import org.mariotaku.microblog.library.mastodon.api.FollowRequestResources; +import org.mariotaku.microblog.library.mastodon.api.FollowResources; +import org.mariotaku.microblog.library.mastodon.api.InstanceResources; +import org.mariotaku.microblog.library.mastodon.api.MediaResources; +import org.mariotaku.microblog.library.mastodon.api.MuteResources; +import org.mariotaku.microblog.library.mastodon.api.NotificationResources; +import org.mariotaku.microblog.library.mastodon.api.ReportResources; +import org.mariotaku.microblog.library.mastodon.api.SearchResources; +import org.mariotaku.microblog.library.mastodon.api.StatusResources; +import org.mariotaku.microblog.library.mastodon.api.TimelineResources; + /** * Created by mariotaku on 2017/4/17. */ -public interface Mastodon { +public interface Mastodon extends AccountResources, ApplicationResources, BlockResources, + FavouriteResources, FollowRequestResources, FollowResources, InstanceResources, + MediaResources, MuteResources, NotificationResources, ReportResources, SearchResources, + StatusResources, TimelineResources { + } diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/MastodonOAuth2.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/MastodonOAuth2.java new file mode 100644 index 000000000..aef6b145c --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/MastodonOAuth2.java @@ -0,0 +1,39 @@ +/* + * Twidere - Twitter client for Android + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.mariotaku.microblog.library.mastodon; + +import org.mariotaku.microblog.library.MicroBlogException; +import org.mariotaku.microblog.library.twitter.auth.OAuth2Token; +import org.mariotaku.restfu.annotation.method.GET; +import org.mariotaku.restfu.annotation.param.Query; + +/** + * Created by mariotaku on 2017/4/18. + */ + +public interface MastodonOAuth2 { + + @GET("/v1/oauth/token") + OAuth2Token getToken(@Query("client_id") String clientId, @Query("client_secret") String clientSecret, + @Query("grant_type") String grantType, @Query("code") String code, + @Query("redirect_uri") String redirectUri) throws MicroBlogException; +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/annotation/AuthScope.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/annotation/AuthScope.java new file mode 100644 index 000000000..08ceedc17 --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/annotation/AuthScope.java @@ -0,0 +1,38 @@ +/* + * Twidere - Twitter client for Android + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.mariotaku.microblog.library.mastodon.annotation; + +import android.support.annotation.StringDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by mariotaku on 2017/4/18. + */ +@StringDef({AuthScope.READ, AuthScope.WRITE, AuthScope.FOLLOW}) +@Retention(RetentionPolicy.SOURCE) +public @interface AuthScope { + String READ = "read"; + String WRITE = "write"; + String FOLLOW = "follow"; +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/api/AccountResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/api/AccountResources.java index 09868df54..c0ea3feb7 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/api/AccountResources.java +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/api/AccountResources.java @@ -43,48 +43,48 @@ import java.util.List; public interface AccountResources { - @GET("/api/v1/accounts/{id}") + @GET("/v1/accounts/{id}") Account getAccount(@Path("id") String id) throws MicroBlogException; - @GET("/api/v1/accounts/verify_credentials") + @GET("/v1/accounts/verify_credentials") Account verifyCredentials() throws MicroBlogException; - @PATCH("/api/v1/accounts/update_credentials") + @PATCH("/v1/accounts/update_credentials") Account updateCredentials(@Param AccountUpdate update) throws MicroBlogException; - @GET("/api/v1/accounts/{id}/followers") + @GET("/v1/accounts/{id}/followers") List getFollowers(@Path("id") String id, @Query Paging paging) throws MicroBlogException; - @GET("/api/v1/accounts/{id}/following") + @GET("/v1/accounts/{id}/following") List getFollowing(@Path("id") String id, @Query Paging paging) throws MicroBlogException; - @GET("/api/v1/accounts/{id}/statuses") + @GET("/v1/accounts/{id}/statuses") List getStatuses(@Path("id") String id, @Query Paging paging, @Query TimelineOption option) throws MicroBlogException; - @POST("/api/v1/accounts/{id}/follow") + @POST("/v1/accounts/{id}/follow") Relationship followUser(@Path("id") String id) throws MicroBlogException; - @POST("/api/v1/accounts/{id}/unfollow") + @POST("/v1/accounts/{id}/unfollow") Relationship unfollowUser(@Path("id") String id) throws MicroBlogException; - @POST("/api/v1/accounts/{id}/block") + @POST("/v1/accounts/{id}/block") Relationship blockUser(@Path("id") String id) throws MicroBlogException; - @POST("/api/v1/accounts/{id}/unblock") + @POST("/v1/accounts/{id}/unblock") Relationship unblockUser(@Path("id") String id) throws MicroBlogException; - @POST("/api/v1/accounts/{id}/mute") + @POST("/v1/accounts/{id}/mute") Relationship muteUser(@Path("id") String id) throws MicroBlogException; - @POST("/api/v1/accounts/{id}/unmute") + @POST("/v1/accounts/{id}/unmute") Relationship unmuteUser(@Path("id") String id) throws MicroBlogException; - @GET("/api/v1/accounts/relationships") + @GET("/v1/accounts/relationships") List getRelationships(@Path("id") String id) throws MicroBlogException; - @GET("/api/v1/accounts/search") + @GET("/v1/accounts/search") List searchAccounts(@Query("q") String query) throws MicroBlogException; } diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/api/ApplicationResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/api/ApplicationResources.java index ce5a2dcaa..beb2d3a95 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/api/ApplicationResources.java +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/mastodon/api/ApplicationResources.java @@ -33,9 +33,9 @@ import org.mariotaku.restfu.annotation.param.Param; */ public interface ApplicationResources { - @POST("/api/v1/apps") + @POST("/v1/apps") RegisteredApplication registerApplication(@Param("client_name") String clientName, @Param("redirect_uris") String redirectUris, - @Param(value = "scopes", arrayDelimiter = ' ') String scopes, + @Param(value = "scopes", arrayDelimiter = ' ') String[] scopes, @Nullable @Param("website") String website) throws MicroBlogException; } diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/auth/OAuth2Token.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/auth/OAuth2Token.java index e31fbae70..3e8c9d645 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/auth/OAuth2Token.java +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/auth/OAuth2Token.java @@ -33,6 +33,10 @@ public class OAuth2Token { String tokenType; @JsonField(name = "access_token") String accessToken; + @JsonField(name = "expires_in") + long expiresIn; + @JsonField(name = "refresh_token") + String refreshToken; public String getTokenType() { return tokenType; @@ -41,4 +45,22 @@ public class OAuth2Token { public String getAccessToken() { return accessToken; } + + public long getExpiresIn() { + return expiresIn; + } + + public String getRefreshToken() { + return refreshToken; + } + + @Override + public String toString() { + return "OAuth2Token{" + + "tokenType='" + tokenType + '\'' + + ", accessToken='" + accessToken + '\'' + + ", expiresIn=" + expiresIn + + ", refreshToken='" + refreshToken + '\'' + + '}'; + } } diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java index d89026c9c..9fd1c8bc7 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java @@ -35,7 +35,7 @@ import org.mariotaku.twidere.constant.SharedPreferenceConstants; public interface TwidereConstants extends SharedPreferenceConstants, IntentConstants, CompatibilityConstants { String TWIDERE_APP_NAME = "Twidere"; - String TWIDERE_PROJECT_URL = "https://github.com/mariotaku/twidere"; + String TWIDERE_PROJECT_URL = "https://github.com/TwidereProject/"; String TWIDERE_PROJECT_EMAIL = "twidere.project@gmail.com"; String TWIDERE_PACKAGE_NAME = "org.mariotaku.twidere"; @@ -172,13 +172,13 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst String OAUTH_CALLBACK_OOB = "oob"; String OAUTH_CALLBACK_URL = PROTOCOL_TWIDERE + "com.twitter.oauth/"; + String MASTODON_CALLBACK_URL = "https://org.mariotaku.twidere/auth/callback/mastodon"; int REQUEST_TAKE_PHOTO = 1; int REQUEST_PICK_MEDIA = 2; int REQUEST_SELECT_ACCOUNT = 3; int REQUEST_COMPOSE = 4; int REQUEST_EDIT_API = 5; - int REQUEST_BROWSER_SIGN_IN = 6; int REQUEST_SET_COLOR = 7; int REQUEST_SET_NICKNAME = 8; int REQUEST_EDIT_IMAGE = 9; diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/constant/IntentConstants.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/constant/IntentConstants.java index 0fdae43ac..42d2c9f34 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/constant/IntentConstants.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/constant/IntentConstants.java @@ -146,9 +146,12 @@ public interface IntentConstants { String EXTRA_TAB_POSITION = "tab_position"; String EXTRA_TAB_ID = "tab_id"; String EXTRA_OAUTH_VERIFIER = "oauth_verifier"; + String EXTRA_CODE = "code"; String EXTRA_ACCESS_TOKEN = "access_token"; String EXTRA_REQUEST_TOKEN = "request_token"; String EXTRA_REQUEST_TOKEN_SECRET = "request_token_secret"; + String EXTRA_CLIENT_ID = "client_id"; + String EXTRA_CLIENT_SECRET = "client_secret"; String EXTRA_OMIT_INTENT_EXTRA = "omit_intent_extra"; String EXTRA_COMMAND = "command"; String EXTRA_WIDTH = "width"; @@ -222,4 +225,5 @@ public interface IntentConstants { String EXTRA_PLACE_NAME = "place_name"; String EXTRA_SCHEDULE_INFO = "schedule_info"; String EXTRA_SAVE_DRAFT = "save_draft"; + String EXTRA_HOST = "host"; } diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/account/cred/OAuth2Credentials.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/account/cred/OAuth2Credentials.java index 796cb4398..1a81d37dd 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/account/cred/OAuth2Credentials.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/account/cred/OAuth2Credentials.java @@ -24,8 +24,10 @@ package org.mariotaku.twidere.model.account.cred; import android.os.Parcel; import android.os.Parcelable; +import com.bluelinelabs.logansquare.annotation.JsonField; import com.bluelinelabs.logansquare.annotation.JsonObject; import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; +import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease; /** * Created by mariotaku on 2016/12/2. @@ -34,6 +36,11 @@ import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; @ParcelablePlease @JsonObject public class OAuth2Credentials extends Credentials implements Parcelable { + + @JsonField(name = "access_token") + @ParcelableThisPlease + public String access_token; + @Override public int describeContents() { return 0; diff --git a/twidere/src/main/kotlin/org/mariotaku/ktextension/TextViewExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/ktextension/TextViewExtensions.kt index 18390157e..a3178d600 100644 --- a/twidere/src/main/kotlin/org/mariotaku/ktextension/TextViewExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/ktextension/TextViewExtensions.kt @@ -6,6 +6,9 @@ import android.widget.TextView val TextView.empty: Boolean get() = length() <= 0 +val TextView.string: String? + get() = text?.toString() + fun TextView.applyFontFamily(lightFont: Boolean) { if (lightFont) { typeface = Typeface.create("sans-serif-light", typeface?.style ?: Typeface.NORMAL) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/BaseActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/BaseActivity.kt index 224cd22e4..6d6f0c828 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/BaseActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/BaseActivity.kt @@ -110,6 +110,8 @@ open class BaseActivity : ChameleonActivity(), IBaseActivity, IThe lateinit var defaultFeatures: DefaultFeatures @Inject lateinit var restHttpClient: RestHttpClient + @Inject + lateinit var mastodonApplicationRegistry: MastodonApplicationRegistry protected val statusScheduleProvider: StatusScheduleProvider? get() = statusScheduleProviderFactory.newInstance(this) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/BrowserSignInActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/BrowserSignInActivity.kt index cd30b0cd8..f11d878bb 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/BrowserSignInActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/BrowserSignInActivity.kt @@ -24,7 +24,6 @@ import android.app.Activity import android.content.Intent import android.graphics.Bitmap import android.net.Uri -import android.os.AsyncTask import android.os.Bundle import android.util.Log import android.view.MenuItem @@ -37,28 +36,17 @@ import android.widget.Toast import kotlinx.android.synthetic.main.activity_browser_sign_in.* import org.attoparser.ParseException import org.mariotaku.ktextension.removeAllCookiesSupport -import org.mariotaku.microblog.library.MicroBlogException -import org.mariotaku.microblog.library.twitter.TwitterOAuth import org.mariotaku.restfu.oauth.OAuthToken import org.mariotaku.twidere.R import org.mariotaku.twidere.TwidereConstants.* -import org.mariotaku.twidere.extension.model.getOAuthAuthorization -import org.mariotaku.twidere.extension.model.newMicroBlogInstance -import org.mariotaku.twidere.model.CustomAPIConfig -import org.mariotaku.twidere.model.SingleResponse -import org.mariotaku.twidere.util.AsyncTaskUtils -import org.mariotaku.twidere.util.DebugLog -import org.mariotaku.twidere.util.MicroBlogAPIFactory import org.mariotaku.twidere.util.OAuthPasswordAuthenticator import org.mariotaku.twidere.util.webkit.DefaultWebViewClient import java.io.IOException import java.io.StringReader -import java.lang.ref.WeakReference class BrowserSignInActivity : BaseActivity() { private var requestToken: OAuthToken? = null - private var task: GetRequestTokenTask? = null override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { @@ -84,13 +72,11 @@ class BrowserSignInActivity : BaseActivity() { webSettings.javaScriptEnabled = true webSettings.blockNetworkImage = false webSettings.saveFormData = true - getRequestToken() + + webView.loadUrl(intent.dataString) } override fun onDestroy() { - if (task?.status == AsyncTask.Status.RUNNING) { - task?.cancel(true) - } webView?.destroy() super.onDestroy() } @@ -105,16 +91,6 @@ class BrowserSignInActivity : BaseActivity() { super.onPause() } - private fun getRequestToken() { - if (requestToken != null || task?.status == AsyncTask.Status.RUNNING) return - task = GetRequestTokenTask(this) - AsyncTaskUtils.executeTask(task) - } - - private fun loadUrl(url: String) { - webView.loadUrl(url) - } - private fun readOAuthPin(html: String): String? { try { val data = OAuthPasswordAuthenticator.OAuthPinData() @@ -137,10 +113,6 @@ class BrowserSignInActivity : BaseActivity() { loadProgress.progress = progress } - private fun setRequestToken(token: OAuthToken) { - requestToken = token - } - internal class AuthorizationWebChromeClient(val activity: BrowserSignInActivity) : WebChromeClient() { override fun onProgressChanged(view: WebView, newProgress: Int) { super.onProgressChanged(view, newProgress) @@ -166,14 +138,11 @@ class BrowserSignInActivity : BaseActivity() { val paramNames = uri.queryParameterNames if ("/oauth/authorize" == path && paramNames.contains("oauth_callback")) { // Sign in successful response. - val requestToken = activity.requestToken - if (requestToken != null) { - val intent = Intent() - intent.putExtra(EXTRA_REQUEST_TOKEN, requestToken.oauthToken) - intent.putExtra(EXTRA_REQUEST_TOKEN_SECRET, requestToken.oauthTokenSecret) - activity.setResult(Activity.RESULT_OK, intent) - activity.finish() - } + val intent = activity.intent + val data = Intent() + data.putExtra(EXTRA_EXTRAS, intent.getBundleExtra(EXTRA_EXTRAS)) + activity.setResult(Activity.RESULT_OK, data) + activity.finish() } } } @@ -190,67 +159,20 @@ class BrowserSignInActivity : BaseActivity() { @Suppress("Deprecation", "OverridingDeprecatedMember") override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { val uri = Uri.parse(url) + val data = Intent() + data.putExtra(EXTRA_EXTRAS, activity.intent.getBundleExtra(EXTRA_EXTRAS)) if (url.startsWith(OAUTH_CALLBACK_URL)) { - val oauthVerifier = uri.getQueryParameter(EXTRA_OAUTH_VERIFIER) - val activity = activity - val requestToken = activity.requestToken - if (oauthVerifier != null && requestToken != null) { - val intent = Intent() - intent.putExtra(EXTRA_OAUTH_VERIFIER, oauthVerifier) - intent.putExtra(EXTRA_REQUEST_TOKEN, requestToken.oauthToken) - intent.putExtra(EXTRA_REQUEST_TOKEN_SECRET, requestToken.oauthTokenSecret) - activity.setResult(Activity.RESULT_OK, intent) - activity.finish() - } - return true - } - return false - } - - } - - internal class GetRequestTokenTask(activity: BrowserSignInActivity) : AsyncTask>() { - private val activityRef: WeakReference = WeakReference(activity) - private val apiConfig: CustomAPIConfig = activity.intent.getParcelableExtra(EXTRA_API_CONFIG) - - override fun doInBackground(vararg params: Any): SingleResponse { - val activity = activityRef.get() ?: return SingleResponse(exception = InterruptedException()) - try { - val apiUrlFormat = apiConfig.apiUrlFormat ?: - throw MicroBlogException("Invalid API URL format") - val endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiUrlFormat, - apiConfig.isSameOAuthUrl) - val auth = apiConfig.getOAuthAuthorization() ?: - throw MicroBlogException("Invalid OAuth credentials") - val oauth = newMicroBlogInstance(activity, endpoint, auth, apiConfig.type, - TwitterOAuth::class.java) - return SingleResponse(oauth.getRequestToken(OAUTH_CALLBACK_OOB)) - } catch (e: MicroBlogException) { - return SingleResponse(exception = e) - } - - } - - override fun onPostExecute(result: SingleResponse) { - val activity = activityRef.get() ?: return - activity.setLoadProgressShown(false) - if (result.hasData()) { - val token = result.data!! - activity.setRequestToken(token) - val endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiConfig.apiUrlFormat!!, true) - activity.loadUrl(endpoint.construct("/oauth/authorize", arrayOf("oauth_token", token.oauthToken))) + val oauthVerifier = uri.getQueryParameter("oauth_verifier") ?: return false + data.putExtra(EXTRA_OAUTH_VERIFIER, oauthVerifier) + } else if (url.startsWith(MASTODON_CALLBACK_URL)) { + val code = uri.getQueryParameter("code") ?: return false + data.putExtra(EXTRA_CODE, code) } else { - DebugLog.w(LOGTAG, "Error while browser sign in", result.exception) - if (!activity.isFinishing) { - Toast.makeText(activity, R.string.message_toast_error_occurred, Toast.LENGTH_SHORT).show() - activity.finish() - } + return false } - } - - override fun onPreExecute() { - val activity = activityRef.get() ?: return - activity.setLoadProgressShown(true) + activity.setResult(Activity.RESULT_OK, data) + activity.finish() + return true } } @@ -260,13 +182,12 @@ class BrowserSignInActivity : BaseActivity() { @JavascriptInterface fun processHTML(html: String) { val oauthVerifier = activity.readOAuthPin(html) - val requestToken = activity.requestToken - if (oauthVerifier != null && requestToken != null) { - val intent = Intent() - intent.putExtra(EXTRA_OAUTH_VERIFIER, oauthVerifier) - intent.putExtra(EXTRA_REQUEST_TOKEN, requestToken.oauthToken) - intent.putExtra(EXTRA_REQUEST_TOKEN_SECRET, requestToken.oauthTokenSecret) - activity.setResult(Activity.RESULT_OK, intent) + if (oauthVerifier != null) { + val intent = activity.intent + val data = Intent() + data.putExtra(EXTRA_OAUTH_VERIFIER, oauthVerifier) + data.putExtra(EXTRA_EXTRAS, intent.getBundleExtra(EXTRA_EXTRAS)) + activity.setResult(Activity.RESULT_OK, data) activity.finish() } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt index bbe0e74eb..e8cdf37a0 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt @@ -49,11 +49,15 @@ import kotlinx.android.synthetic.main.activity_sign_in.* import nl.komponents.kovenant.combine.and import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.alwaysUi +import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi import org.mariotaku.kpreferences.get import org.mariotaku.ktextension.* import org.mariotaku.microblog.library.MicroBlog import org.mariotaku.microblog.library.MicroBlogException +import org.mariotaku.microblog.library.mastodon.Mastodon +import org.mariotaku.microblog.library.mastodon.MastodonOAuth2 +import org.mariotaku.microblog.library.mastodon.annotation.AuthScope import org.mariotaku.microblog.library.twitter.TwitterOAuth import org.mariotaku.microblog.library.twitter.auth.BasicAuthorization import org.mariotaku.microblog.library.twitter.auth.EmptyAuthorization @@ -167,11 +171,21 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, setSignInButton() invalidateOptionsMenu() } - REQUEST_BROWSER_SIGN_IN -> { + REQUEST_BROWSER_TWITTER_SIGN_IN -> { if (resultCode == Activity.RESULT_OK && data != null) { handleBrowserLoginResult(data) } } + REQUEST_BROWSER_MASTODON_SIGN_IN -> { + if (resultCode == Activity.RESULT_OK && data != null) { + val extras = data.getBundleExtra(EXTRA_EXTRAS) + val host = extras.getString(EXTRA_HOST) + val clientId = extras.getString(EXTRA_CLIENT_ID) + val clientSecret = extras.getString(EXTRA_CLIENT_SECRET) + val code = extras.getString(EXTRA_CODE) + finishMastodonBrowserLogin(host, clientId, clientSecret, code) + } + } } super.onActivityResult(requestCode, resultCode, data) } @@ -208,12 +222,16 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, editUsername.text = null editPassword.text = null } - when (apiConfig.credentialsType) { + if (apiConfig.type == AccountType.MASTODON) { + performMastodonLogin() + } else when (apiConfig.credentialsType) { Credentials.Type.OAUTH -> { - doBrowserLogin() + performBrowserLogin() } else -> { - doLogin() + val username = editUsername.text.toString() + val password = editPassword.text.toString() + performUserPassLogin(username, password) } } } @@ -283,26 +301,127 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, invalidateOptionsMenu() } - internal fun doBrowserLogin(): Boolean { - if (apiConfig.credentialsType != Credentials.Type.OAUTH || signInTask != null && signInTask!!.status == AsyncTask.Status.RUNNING) - return true - val intent = Intent(this, BrowserSignInActivity::class.java) - intent.putExtra(EXTRA_API_CONFIG, apiConfig) - startActivityForResult(intent, REQUEST_BROWSER_SIGN_IN) - return false + + private fun performBrowserLogin() { + val weakThis = WeakReference(this) + executeAfterFragmentResumed { activity -> + ProgressDialogFragment.show(activity.supportFragmentManager, "get_request_token") + } and task { + val activity = weakThis.get() ?: throw InterruptedException() + val apiConfig = activity.apiConfig + val apiUrlFormat = apiConfig.apiUrlFormat ?: + throw MicroBlogException("Invalid API URL format") + val endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiUrlFormat, + apiConfig.isSameOAuthUrl) + val auth = apiConfig.getOAuthAuthorization() ?: + throw MicroBlogException("Invalid OAuth credentials") + val oauth = newMicroBlogInstance(activity, endpoint, auth, apiConfig.type, + TwitterOAuth::class.java) + return@task oauth.getRequestToken(OAUTH_CALLBACK_OOB) + }.successUi { requestToken -> + val activity = weakThis.get() ?: return@successUi + val intent = Intent(activity, BrowserSignInActivity::class.java) + val apiConfig = activity.apiConfig + val endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiConfig.apiUrlFormat!!, true) + intent.data = Uri.parse(endpoint.construct("/oauth/authorize", arrayOf("oauth_token", + requestToken.oauthToken))) + intent.putExtra(EXTRA_EXTRAS, Bundle { + this[EXTRA_REQUEST_TOKEN] = requestToken.oauthToken + this[EXTRA_REQUEST_TOKEN_SECRET] = requestToken.oauthTokenSecret + }) + activity.startActivityForResult(intent, REQUEST_BROWSER_TWITTER_SIGN_IN) + }.failUi { + val activity = weakThis.get() ?: return@failUi + // TODO show error message + }.alwaysUi { + executeAfterFragmentResumed { + it.supportFragmentManager.dismissDialogFragment("get_request_token") + } + } } - internal fun doLogin() { + private fun performMastodonLogin() { + val weakThis = WeakReference(this) + val userKey = editUsername.string?.takeIf(String::isNotEmpty) + ?.let(UserKey::valueOf) ?: run { + Toast.makeText(this, R.string.message_toast_invalid_mastodon_username, + Toast.LENGTH_SHORT).show() + return + } + val scopes = arrayOf(AuthScope.READ, AuthScope.WRITE, AuthScope.FOLLOW) + executeAfterFragmentResumed { activity -> + ProgressDialogFragment.show(activity.supportFragmentManager, "open_browser_auth") + } and task { + val host = userKey.host ?: throw IOException() + val activity = weakThis.get() ?: throw InterruptedException() + val registry = activity.mastodonApplicationRegistry + return@task Pair(host, registry[host] ?: run { + val endpoint = Endpoint("https://$host/api/") + val mastodon = newMicroBlogInstance(activity, endpoint, EmptyAuthorization(), + AccountType.MASTODON, Mastodon::class.java) + val registered = mastodon.registerApplication("Twidere for Android", + MASTODON_CALLBACK_URL, scopes, TWIDERE_PROJECT_URL) + registry[host] = registered + return@run registered + }) + }.successUi { (host, app) -> + val activity = weakThis.get() ?: return@successUi + val endpoint = Endpoint("https://$host/") + val intent = Intent(activity, BrowserSignInActivity::class.java) + intent.data = Uri.parse(endpoint.construct("/oauth/authorize", + arrayOf("response_type", "code"), + arrayOf("client_id", app.clientId), + arrayOf("redirect_uri", MASTODON_CALLBACK_URL), + arrayOf("scope", scopes.joinToString(" ")))) + intent.putExtra(EXTRA_EXTRAS, Bundle { + this[EXTRA_HOST] = host + this[EXTRA_CLIENT_ID] = app.clientId + this[EXTRA_CLIENT_SECRET] = app.clientSecret + }) + activity.startActivityForResult(intent, REQUEST_BROWSER_MASTODON_SIGN_IN) + }.failUi { + val activity = weakThis.get() ?: return@failUi + // TODO show error message + }.alwaysUi { + executeAfterFragmentResumed { + it.supportFragmentManager.dismissDialogFragment("open_browser_auth") + } + } + } + + private fun performUserPassLogin(username: String, password: String) { if (signInTask != null && signInTask!!.status == AsyncTask.Status.RUNNING) { signInTask!!.cancel(true) } - val username = editUsername.text.toString() - val password = editPassword.text.toString() signInTask = SignInTask(this, username, password, apiConfig) AsyncTaskUtils.executeTask(signInTask) } + private fun finishMastodonBrowserLogin(host: String, clientId: String, clientSecret: String, + code: String) { + val weakThis = WeakReference(this) + executeAfterFragmentResumed { activity -> + ProgressDialogFragment.show(activity.supportFragmentManager, "open_browser_auth") + } and task { + val activity = weakThis.get() ?: throw InterruptedException() + val endpoint = Endpoint("https://$host/api/") + val oauth2 = newMicroBlogInstance(activity, endpoint, EmptyAuthorization(), + AccountType.MASTODON, MastodonOAuth2::class.java) + return@task oauth2.getToken(clientId, clientSecret, "code", code, + MASTODON_CALLBACK_URL) + }.successUi { token -> + DebugLog.d(msg = "$token") + }.failUi { + val activity = weakThis.get() ?: return@failUi + // TODO show error message + }.alwaysUi { + executeAfterFragmentResumed { + it.supportFragmentManager.dismissDialogFragment("open_browser_auth") + } + } + } + internal fun onSignInResult(result: SignInResponse) { val am = AccountManager.get(this) setSignInButton() @@ -373,11 +492,6 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, } } - internal fun setUsernamePassword(username: String, password: String) { - editUsername.setText(username) - editPassword.setText(password) - } - private fun updateDefaultFeatures() { val weakThis = WeakReference(this) executeAfterFragmentResumed { @@ -416,14 +530,11 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, private fun handleBrowserLoginResult(intent: Intent?) { if (intent == null) return - if (signInTask?.status == AsyncTask.Status.RUNNING) { - signInTask?.cancel(true) - } val verifier = intent.getStringExtra(EXTRA_OAUTH_VERIFIER) val requestToken = OAuthToken(intent.getStringExtra(EXTRA_REQUEST_TOKEN), intent.getStringExtra(EXTRA_REQUEST_TOKEN_SECRET)) signInTask = BrowserSignInTask(this, apiConfig, requestToken, verifier) - AsyncTaskUtils.executeTask(signInTask) + AsyncTaskUtils.executeTask(signInTask) } private fun setDefaultAPI() { @@ -719,9 +830,9 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, val editUsername = alertDialog.findViewById(R.id.username) as EditText val editPassword = alertDialog.findViewById(R.id.password) as EditText val activity = activity as SignInActivity - activity.setUsernamePassword(editUsername.text.toString(), - editPassword.text.toString()) - activity.doLogin() + val username = editUsername.text.toString() + val password = editPassword.text.toString() + activity.performUserPassLogin(username, password) } builder.setNegativeButton(android.R.string.cancel, null) @@ -797,15 +908,9 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, } - /** - * Created by mariotaku on 16/7/7. - */ - internal class BrowserSignInTask( - context: SignInActivity, - private val apiConfig: CustomAPIConfig, - private val requestToken: OAuthToken, - private val oauthVerifier: String? - ) : AbstractSignInTask(context) { + internal class BrowserSignInTask(context: SignInActivity, private val apiConfig: CustomAPIConfig, + private val requestToken: OAuthToken, private val oauthVerifier: String?) : + AbstractSignInTask(context) { private val context: Context @@ -865,64 +970,6 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, } - internal data class SignInResponse( - val alreadyLoggedIn: Boolean, - @Credentials.Type val credsType: String = Credentials.Type.EMPTY, - val credentials: Credentials, - val user: ParcelableUser, - val color: Int = 0, - val typeExtras: Pair - ) { - - private fun writeAccountInfo(action: (k: String, v: String?) -> Unit) { - action(ACCOUNT_USER_DATA_KEY, user.key.toString()) - action(ACCOUNT_USER_DATA_TYPE, typeExtras.first) - action(ACCOUNT_USER_DATA_CREDS_TYPE, credsType) - - action(ACCOUNT_USER_DATA_ACTIVATED, true.toString()) - action(ACCOUNT_USER_DATA_COLOR, toHexColor(color, format = HexColorFormat.RGB)) - - action(ACCOUNT_USER_DATA_USER, JsonSerializer.serialize(user)) - action(ACCOUNT_USER_DATA_EXTRAS, typeExtras.second?.let { JsonSerializer.serialize(it) }) - } - - private fun writeAuthToken(am: AccountManager, account: Account) { - val authToken = JsonSerializer.serialize(credentials) - am.setAuthToken(account, ACCOUNT_AUTH_TOKEN_TYPE, authToken) - } - - fun updateAccount(am: AccountManager) { - val account = AccountUtils.findByAccountKey(am, user.key) ?: return - writeAccountInfo { k, v -> - am.setUserData(account, k, v) - } - writeAuthToken(am, account) - } - - fun addAccount(am: AccountManager, randomizeAccountName: Boolean): Account { - var accountName: String - if (randomizeAccountName) { - val usedNames = ArraySet() - AccountUtils.getAccounts(am).mapTo(usedNames, Account::name) - do { - accountName = UUID.randomUUID().toString() - } while (accountName in usedNames) - } else { - accountName = generateAccountName(user.screen_name, user.key.host) - } - val account = Account(accountName, ACCOUNT_TYPE) - val accountPosition = AccountUtils.getAccounts(am).size - // Don't add UserData in this method, see http://stackoverflow.com/a/29776224/859190 - am.addAccountExplicitly(account, null, null) - writeAccountInfo { k, v -> - am.setUserData(account, k, v) - } - am.setUserData(account, ACCOUNT_USER_DATA_POSITION, accountPosition.toString()) - writeAuthToken(am, account) - return account - } - - } internal class SignInTask( activity: SignInActivity, @@ -1141,8 +1188,68 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, } + internal data class SignInResponse( + val alreadyLoggedIn: Boolean, + @Credentials.Type val credsType: String = Credentials.Type.EMPTY, + val credentials: Credentials, + val user: ParcelableUser, + val color: Int = 0, + val typeExtras: Pair + ) { + + private fun writeAccountInfo(action: (k: String, v: String?) -> Unit) { + action(ACCOUNT_USER_DATA_KEY, user.key.toString()) + action(ACCOUNT_USER_DATA_TYPE, typeExtras.first) + action(ACCOUNT_USER_DATA_CREDS_TYPE, credsType) + + action(ACCOUNT_USER_DATA_ACTIVATED, true.toString()) + action(ACCOUNT_USER_DATA_COLOR, toHexColor(color, format = HexColorFormat.RGB)) + + action(ACCOUNT_USER_DATA_USER, JsonSerializer.serialize(user)) + action(ACCOUNT_USER_DATA_EXTRAS, typeExtras.second?.let { JsonSerializer.serialize(it) }) + } + + private fun writeAuthToken(am: AccountManager, account: Account) { + val authToken = JsonSerializer.serialize(credentials) + am.setAuthToken(account, ACCOUNT_AUTH_TOKEN_TYPE, authToken) + } + + fun updateAccount(am: AccountManager) { + val account = AccountUtils.findByAccountKey(am, user.key) ?: return + writeAccountInfo { k, v -> + am.setUserData(account, k, v) + } + writeAuthToken(am, account) + } + + fun addAccount(am: AccountManager, randomizeAccountName: Boolean): Account { + var accountName: String + if (randomizeAccountName) { + val usedNames = ArraySet() + AccountUtils.getAccounts(am).mapTo(usedNames, Account::name) + do { + accountName = UUID.randomUUID().toString() + } while (accountName in usedNames) + } else { + accountName = generateAccountName(user.screen_name, user.key.host) + } + val account = Account(accountName, ACCOUNT_TYPE) + val accountPosition = AccountUtils.getAccounts(am).size + // Don't add UserData in this method, see http://stackoverflow.com/a/29776224/859190 + am.addAccountExplicitly(account, null, null) + writeAccountInfo { k, v -> + am.setUserData(account, k, v) + } + am.setUserData(account, ACCOUNT_USER_DATA_POSITION, accountPosition.toString()) + writeAuthToken(am, account) + return account + } + + } companion object { + const val REQUEST_BROWSER_TWITTER_SIGN_IN = 101 + const val REQUEST_BROWSER_MASTODON_SIGN_IN = 102 private val FRAGMENT_TAG_SIGN_IN_PROGRESS = "sign_in_progress" private val FRAGMENT_TAG_LOADING_DEFAULT_FEATURES = "loading_default_features" diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/CredentialsExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/CredentialsExtensions.kt index 284842c81..e4a01342b 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/CredentialsExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/CredentialsExtensions.kt @@ -6,6 +6,8 @@ import android.text.TextUtils import org.mariotaku.microblog.library.MicroBlog import org.mariotaku.microblog.library.MicroBlogException import org.mariotaku.microblog.library.fanfou.FanfouStream +import org.mariotaku.microblog.library.mastodon.Mastodon +import org.mariotaku.microblog.library.mastodon.MastodonOAuth2 import org.mariotaku.microblog.library.twitter.* import org.mariotaku.microblog.library.twitter.auth.BasicAuthorization import org.mariotaku.microblog.library.twitter.auth.EmptyAuthorization @@ -17,12 +19,10 @@ import org.mariotaku.restfu.http.MultiValueMap import org.mariotaku.restfu.oauth.OAuthAuthorization import org.mariotaku.restfu.oauth.OAuthEndpoint import org.mariotaku.restfu.oauth.OAuthToken +import org.mariotaku.restfu.oauth2.OAuth2Authorization import org.mariotaku.twidere.TwidereConstants.DEFAULT_TWITTER_API_URL_FORMAT import org.mariotaku.twidere.annotation.AccountType -import org.mariotaku.twidere.model.account.cred.BasicCredentials -import org.mariotaku.twidere.model.account.cred.Credentials -import org.mariotaku.twidere.model.account.cred.EmptyCredentials -import org.mariotaku.twidere.model.account.cred.OAuthCredentials +import org.mariotaku.twidere.model.account.cred.* import org.mariotaku.twidere.util.HttpClientFactory import org.mariotaku.twidere.util.MicroBlogAPIFactory import org.mariotaku.twidere.util.MicroBlogAPIFactory.sFanfouConstantPool @@ -50,6 +50,9 @@ fun Credentials.getAuthorization(cls: Class<*>?): Authorization { return OAuthAuthorization(consumer_key, consumer_secret, OAuthToken(access_token, access_token_secret)) } + is OAuth2Credentials -> { + return OAuth2Authorization(access_token) + } is BasicCredentials -> { return BasicAuthorization(username, password) } @@ -107,6 +110,14 @@ fun Credentials.getEndpoint(cls: Class<*>): Endpoint { domain = null versionSuffix = null } + Mastodon::class.java.isAssignableFrom(cls) -> { + domain = null + versionSuffix = null + } + MastodonOAuth2::class.java.isAssignableFrom(cls) -> { + domain = null + versionSuffix = null + } else -> throw UnsupportedOperationException("Unsupported class $cls") } val endpointUrl = MicroBlogAPIFactory.getApiUrl(apiUrlFormat, domain, versionSuffix) @@ -124,8 +135,7 @@ fun Credentials.getEndpoint(cls: Class<*>): Endpoint { fun Credentials.newMicroBlogInstance(context: Context, @AccountType accountType: String? = null, cls: Class): T { - return newMicroBlogInstance(context, getEndpoint(cls), getAuthorization(cls), accountType, - cls) + return newMicroBlogInstance(context, getEndpoint(cls), getAuthorization(cls), accountType, cls) } fun newMicroBlogInstance(context: Context, endpoint: Endpoint, auth: Authorization, diff --git a/twidere/src/main/res/values/strings.xml b/twidere/src/main/res/values/strings.xml index 3beaa3d8c..2fbce0d98 100644 --- a/twidere/src/main/res/values/strings.xml +++ b/twidere/src/main/res/values/strings.xml @@ -1323,4 +1323,5 @@ Blocked these users. %s\'s lists User\'s tweets + Invalid Mastodon username (username@mastodon-site)