implementing mastodon login flow

This commit is contained in:
Mariotaku Lee 2017-04-18 21:19:07 +08:00
parent d1a5de7489
commit 527078025f
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
17 changed files with 396 additions and 222 deletions

View File

@ -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',

View File

@ -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']}"

View File

@ -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 {
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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<Account> getFollowers(@Path("id") String id, @Query Paging paging)
throws MicroBlogException;
@GET("/api/v1/accounts/{id}/following")
@GET("/v1/accounts/{id}/following")
List<Account> getFollowing(@Path("id") String id, @Query Paging paging)
throws MicroBlogException;
@GET("/api/v1/accounts/{id}/statuses")
@GET("/v1/accounts/{id}/statuses")
List<Status> 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<Relationship> getRelationships(@Path("id") String id) throws MicroBlogException;
@GET("/api/v1/accounts/search")
@GET("/v1/accounts/search")
List<Account> searchAccounts(@Query("q") String query) throws MicroBlogException;
}

View File

@ -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;
}

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -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;

View File

@ -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";
}

View File

@ -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;

View File

@ -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)

View File

@ -110,6 +110,8 @@ open class BaseActivity : ChameleonActivity(), IBaseActivity<BaseActivity>, IThe
lateinit var defaultFeatures: DefaultFeatures
@Inject
lateinit var restHttpClient: RestHttpClient
@Inject
lateinit var mastodonApplicationRegistry: MastodonApplicationRegistry
protected val statusScheduleProvider: StatusScheduleProvider?
get() = statusScheduleProviderFactory.newInstance(this)

View File

@ -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<Any, Any, SingleResponse<OAuthToken>>() {
private val activityRef: WeakReference<BrowserSignInActivity> = WeakReference(activity)
private val apiConfig: CustomAPIConfig = activity.intent.getParcelableExtra(EXTRA_API_CONFIG)
override fun doInBackground(vararg params: Any): SingleResponse<OAuthToken> {
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<OAuthToken>) {
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()
}
}

View File

@ -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<AbstractSignInTask, Any>(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<AbstractSignInTask, Any>(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<String, AccountExtras?>
) {
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<String>()
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<String, AccountExtras?>
) {
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<String>()
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"

View File

@ -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 <T> Credentials.newMicroBlogInstance(context: Context, @AccountType accountType: String? = null,
cls: Class<T>): T {
return newMicroBlogInstance(context, getEndpoint(cls), getAuthorization(cls), accountType,
cls)
return newMicroBlogInstance(context, getEndpoint(cls), getAuthorization(cls), accountType, cls)
}
fun <T> newMicroBlogInstance(context: Context, endpoint: Endpoint, auth: Authorization,

View File

@ -1323,4 +1323,5 @@
<string name="users_blocked">Blocked these users.</string>
<string name="users_lists_with_name"><xliff:g id="name">%s</xliff:g>\'s lists</string>
<string name="users_statuses">User\'s tweets</string>
<string name="message_toast_invalid_mastodon_username">Invalid Mastodon username (username@mastodon-site)</string>
</resources>