Use nodeinfo endpoint to get info about the capabilities of the instance

This commit is contained in:
Matthieu 2020-08-22 22:34:21 +02:00
parent bd39bc681d
commit 7bca413d60
17 changed files with 446 additions and 236 deletions

View File

@ -22,7 +22,7 @@ test:
- adb shell settings put global transition_animation_scale 0.0 - adb shell settings put global transition_animation_scale 0.0
- adb shell settings put global animator_duration_scale 0.0 - adb shell settings put global animator_duration_scale 0.0
- ./gradlew build connectedCheck jacocoTestReport - ./gradlew build connectedCheck connectedDebugAndroidTest jacocoTestReport
- cat app/build/reports/jacoco/jacocoTestReport/html/index.html | grep -o 'Total[^%]*%' - cat app/build/reports/jacoco/jacocoTestReport/html/index.html | grep -o 'Total[^%]*%'

View File

@ -55,7 +55,7 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
@ -68,7 +68,7 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.2.17' implementation 'io.reactivex.rxjava2:rxjava:2.2.17'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "androidx.browser:browser:1.2.0" implementation "androidx.browser:browser:1.2.0"
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.2.0'
implementation 'com.github.connyduck:sparkbutton:4.0.0' implementation 'com.github.connyduck:sparkbutton:4.0.0'
def room_version = "2.2.5" def room_version = "2.2.5"
@ -140,14 +140,14 @@ dependencies {
debugImplementation "androidx.fragment:fragment-testing:$fragment_version" debugImplementation "androidx.fragment:fragment-testing:$fragment_version"
// Use the most recent version of CameraX // Use the most recent version of CameraX
def camerax_version = '1.0.0-beta07' def camerax_version = '1.0.0-beta08'
implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}"
// CameraX Lifecycle library // CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class // CameraX View class
implementation 'androidx.camera:camera-view:1.0.0-alpha14' implementation 'androidx.camera:camera-view:1.0.0-alpha15'
implementation 'com.karumi:dexter:6.2.1' implementation 'com.karumi:dexter:6.2.1'
@ -162,7 +162,7 @@ tasks.withType(Test) {
} }
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) { task jacocoTestReport(type: JacocoReport, dependsOn: ['connectedDebugAndroidTest', 'testDebugUnitTest', 'createDebugCoverageReport']) {
reports { reports {
xml.enabled = true xml.enabled = true

View File

@ -2,6 +2,7 @@ package com.h.pixeldroid
import android.content.Context import android.content.Context
import android.service.autofill.Validators.and
import android.widget.TextView import android.widget.TextView
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
@ -17,6 +18,7 @@ import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.fragments.feeds.postFeeds.PostViewHolder import com.h.pixeldroid.fragments.feeds.postFeeds.PostViewHolder
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.atPosition
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.clickChildViewWithId import com.h.pixeldroid.testUtility.CustomMatchers.Companion.clickChildViewWithId
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.first import com.h.pixeldroid.testUtility.CustomMatchers.Companion.first
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.getText import com.h.pixeldroid.testUtility.CustomMatchers.Companion.getText
@ -26,6 +28,8 @@ import com.h.pixeldroid.testUtility.CustomMatchers.Companion.typeTextInViewWithI
import com.h.pixeldroid.testUtility.MockServer import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.testUtility.initDB import com.h.pixeldroid.testUtility.initDB
import com.h.pixeldroid.utils.DBUtils import com.h.pixeldroid.utils.DBUtils
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -217,7 +221,8 @@ class HomeFeedTest {
(1, clickChildViewWithId(R.id.sensitiveWarning))) (1, clickChildViewWithId(R.id.sensitiveWarning)))
Thread.sleep(1000) Thread.sleep(1000)
onView(second(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.GONE))) onView(withId(R.id.list))
.check(matches(atPosition(1, not(withId(R.id.sensitiveWarning)))))
} }
@Test @Test

View File

@ -3,14 +3,18 @@ package com.h.pixeldroid.testUtility
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
import androidx.test.espresso.UiController import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.* import androidx.test.espresso.action.*
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import org.hamcrest.BaseMatcher import org.hamcrest.BaseMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher import org.hamcrest.Matcher
abstract class CustomMatchers { abstract class CustomMatchers {
companion object { companion object {
fun <T> first(matcher: Matcher<T>): Matcher<T>? { fun <T> first(matcher: Matcher<T>): Matcher<T>? {
@ -50,6 +54,24 @@ abstract class CustomMatchers {
} }
} }
fun atPosition(position: Int, itemMatcher: Matcher<View?>): Matcher<View?>? {
return object : BoundedMatcher<View?, RecyclerView>(RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item at position $position: ")
itemMatcher.describeTo(description)
}
override fun matchesSafely(view: RecyclerView): Boolean {
val viewHolder = view.findViewHolderForAdapterPosition(position)
?: // has no item on such position
return false
return itemMatcher.matches(viewHolder.itemView)
}
}
}
/** /**
* @param percent can be 1 or 0 * @param percent can be 1 or 0
* 1: swipes all the way up * 1: swipes all the way up
@ -60,8 +82,9 @@ abstract class CustomMatchers {
GeneralSwipeAction( GeneralSwipeAction(
Swipe.SLOW, Swipe.SLOW,
GeneralLocation.BOTTOM_CENTER, GeneralLocation.BOTTOM_CENTER,
if(percent) GeneralLocation.TOP_CENTER else GeneralLocation.CENTER, if (percent) GeneralLocation.TOP_CENTER else GeneralLocation.CENTER,
Press.FINGER) Press.FINGER
)
) )
} }
@ -75,8 +98,9 @@ abstract class CustomMatchers {
GeneralSwipeAction( GeneralSwipeAction(
Swipe.SLOW, Swipe.SLOW,
GeneralLocation.CENTER_RIGHT, GeneralLocation.CENTER_RIGHT,
if(percent) GeneralLocation.CENTER_LEFT else GeneralLocation.CENTER, if (percent) GeneralLocation.CENTER_LEFT else GeneralLocation.CENTER,
Press.FINGER) Press.FINGER
)
) )
} }

View File

@ -1,11 +1,13 @@
package com.h.pixeldroid package com.h.pixeldroid
import android.app.AlertDialog
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -13,21 +15,38 @@ import androidx.browser.customtabs.CustomTabsIntent
import com.h.pixeldroid.api.PixelfedAPI import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.db.AppDatabase import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.di.PixelfedAPIHolder import com.h.pixeldroid.di.PixelfedAPIHolder
import com.h.pixeldroid.objects.Account import com.h.pixeldroid.objects.*
import com.h.pixeldroid.objects.Application
import com.h.pixeldroid.objects.Instance
import com.h.pixeldroid.objects.Token
import com.h.pixeldroid.utils.DBUtils import com.h.pixeldroid.utils.DBUtils
import com.h.pixeldroid.utils.DBUtils.Companion.storeInstance
import com.h.pixeldroid.utils.Utils import com.h.pixeldroid.utils.Utils
import com.h.pixeldroid.utils.Utils.Companion.normalizeDomain import com.h.pixeldroid.utils.Utils.Companion.normalizeDomain
import io.reactivex.Single
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.functions.BiFunction
import io.reactivex.functions.Function3
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_login.* import kotlinx.android.synthetic.main.activity_login.*
import okhttp3.HttpUrl import okhttp3.HttpUrl
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import javax.inject.Inject import javax.inject.Inject
/**
Overview of the flow of the login process: (boxes are requests done in parallel,
since they do not depend on each other)
_________________________________
|[PixelfedAPI.registerApplication]|
|[PixelfedAPI.wellKnownNodeInfo] |
̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅ +----> [PixelfedAPI.nodeInfoSchema]
+----> [promptOAuth]
+---->____________________________
|[PixelfedAPI.instance] |
|[PixelfedAPI.obtainToken] |
̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅ +----> [PixelfedAPI.verifyCredentials]
*/
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
@ -119,31 +138,85 @@ class LoginActivity : AppCompatActivity() {
hideKeyboard() hideKeyboard()
loadingAnimation(true) loadingAnimation(true)
apiHolder.setDomain(normalizedDomain).registerApplication( pixelfedAPI = apiHolder.setDomain(normalizedDomain)
appName,"$oauthScheme://$PACKAGE_ID", SCOPE
).enqueue(object : Callback<Application> {
override fun onResponse(call: Call<Application>, response: Response<Application>) {
if (!response.isSuccessful) {
return failedRegistration()
}
preferences.edit()
.putString("domain", normalizedDomain)
.apply()
val credentials = response.body() as Application
val clientId = credentials.client_id ?: return failedRegistration()
preferences.edit()
.putString("clientID", clientId)
.putString("clientSecret", credentials.client_secret)
.apply()
promptOAuth(normalizedDomain, clientId)
}
override fun onFailure(call: Call<Application>, t: Throwable) { Single.zip(
return failedRegistration() pixelfedAPI.registerApplication(
appName,"$oauthScheme://$PACKAGE_ID", SCOPE
),
pixelfedAPI.wellKnownNodeInfo(),
BiFunction<Application, NodeInfoJRD, Pair<Application, NodeInfoJRD>> { application, nodeInfoJRD ->
// we get here when both results have come in:
Pair(application, nodeInfoJRD)
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<Pair<Application, NodeInfoJRD>> {
override fun onSuccess(pair: Pair<Application, NodeInfoJRD>) {
val (credentials, nodeInfoJRD) = pair
val clientId = credentials.client_id ?: return failedRegistration()
preferences.edit()
.putString("domain", normalizedDomain)
.putString("clientID", clientId)
.putString("clientSecret", credentials.client_secret)
.apply()
//c.f. https://nodeinfo.diaspora.software/protocol.html for more info
val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull {
it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
}?.href ?: return failedRegistration(getString(R.string.instance_error))
nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl)
}
override fun onError(e: Throwable) {
//Error in any of the two requests will get to this
Log.e("registerAppToServer", e.message.toString())
failedRegistration()
}
override fun onSubscribe(d: Disposable) {}
})
}
private fun nodeInfoSchema(
normalizedDomain: String,
clientId: String,
nodeInfoSchemaUrl: String
) {
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl).enqueue(object : Callback<NodeInfo> {
override fun onResponse(call: Call<NodeInfo>, response: Response<NodeInfo>) {
if (response.body() == null || !response.isSuccessful) {
return failedRegistration(getString(R.string.instance_error))
}
val nodeInfo = response.body() as NodeInfo
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
val builder = AlertDialog.Builder(this@LoginActivity)
builder.apply {
setMessage(R.string.instance_not_pixelfed_warning)
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
promptOAuth(normalizedDomain, clientId)
}
setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ ->
loadingAnimation(false)
wipeSharedSettings()
}
}
// Create the AlertDialog
builder.show()
return
} else {
promptOAuth(normalizedDomain, clientId)
}
}
override fun onFailure(call: Call<NodeInfo>, t: Throwable) {
failedRegistration(getString(R.string.instance_error))
} }
}) })
} }
private fun promptOAuth(normalizedDomain: String, client_id: String) { private fun promptOAuth(normalizedDomain: String, client_id: String) {
val url = "$normalizedDomain/oauth/authorize?" + val url = "$normalizedDomain/oauth/authorize?" +
@ -178,29 +251,45 @@ class LoginActivity : AppCompatActivity() {
} }
//Successful authorization //Successful authorization
val callback = object : Callback<Token> {
override fun onResponse(call: Call<Token>, response: Response<Token>) {
if (!response.isSuccessful || response.body() == null) {
return failedRegistration(getString(R.string.token_error))
}
authenticationSuccessful(response.body()!!.access_token)
}
override fun onFailure(call: Call<Token>, t: Throwable) {
return failedRegistration(getString(R.string.token_error))
}
}
pixelfedAPI = apiHolder.setDomain(domain) pixelfedAPI = apiHolder.setDomain(domain)
pixelfedAPI.obtainToken(
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
"authorization_code"
).enqueue(callback)
}
private fun authenticationSuccessful(accessToken: String) { //TODO check why we can't do onErrorReturn { null } which would make more sense ¯\_(ツ)_/¯
saveUserAndInstance(accessToken) //Also, maybe find a nicer way to do this, this feels hacky (although it can work fine)
wipeSharedSettings() val nullInstance = Instance(null, null, null, null, null, null, null, null)
val nullToken = Token(null, null, null, null)
Single.zip(
pixelfedAPI.instance().onErrorReturn { nullInstance },
pixelfedAPI.obtainToken(
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
"authorization_code"
).onErrorReturn { nullToken },
BiFunction<Instance, Token, Pair<Instance, Token>> { instance, token ->
// we get here when all results have come in:
Pair(instance, token)
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<Pair<Instance, Token>> {
override fun onSuccess(triple: Pair<Instance, Token>) {
val (instance, token) = triple
if(token == nullToken || token.access_token == null){
return failedRegistration(getString(R.string.token_error))
} else if(instance == nullInstance || instance.uri == null){
return failedRegistration(getString(R.string.instance_error))
}
DBUtils.storeInstance(db, instance)
storeUser(token.access_token, instance.uri)
wipeSharedSettings()
}
override fun onError(e: Throwable) {
Log.e("saveUserAndInstance", e.message.toString())
failedRegistration(getString(R.string.token_error))
}
override fun onSubscribe(d: Disposable) {}
})
} }
private fun failedRegistration(message: String = getString(R.string.registration_failed)) { private fun failedRegistration(message: String = getString(R.string.registration_failed)) {
@ -225,24 +314,6 @@ class LoginActivity : AppCompatActivity() {
} }
} }
private fun saveUserAndInstance(accessToken: String) {
pixelfedAPI.instance().enqueue(object : Callback<Instance> {
override fun onFailure(call: Call<Instance>, t: Throwable) {
return failedRegistration(getString(R.string.instance_error))
}
override fun onResponse(call: Call<Instance>, response: Response<Instance>) {
if (response.isSuccessful && response.body() != null) {
val instance = response.body() as Instance
storeInstance(db, instance)
storeUser(accessToken, instance.uri)
} else {
return failedRegistration(getString(R.string.instance_error))
}
}
})
}
private fun storeUser(accessToken: String, instance: String) { private fun storeUser(accessToken: String, instance: String) {
pixelfedAPI.verifyCredentials("Bearer $accessToken") pixelfedAPI.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> { .enqueue(object : Callback<Account> {

View File

@ -1,8 +1,8 @@
package com.h.pixeldroid.api package com.h.pixeldroid.api
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.objects.* import com.h.pixeldroid.objects.*
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single
import okhttp3.MultipartBody import okhttp3.MultipartBody
import retrofit2.Call import retrofit2.Call
import retrofit2.Retrofit import retrofit2.Retrofit
@ -10,9 +10,6 @@ import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.* import retrofit2.http.*
import retrofit2.http.Field import retrofit2.http.Field
import javax.inject.Inject
import javax.inject.Provider
/* /*
Implements the Pixelfed API Implements the Pixelfed API
@ -23,7 +20,12 @@ import javax.inject.Provider
interface PixelfedAPI { interface PixelfedAPI {
companion object { companion object {
@Deprecated(
"Use the DI-d PixelfedAPIHolder instead",
ReplaceWith("apiHolder.api")
)
fun create(baseUrl: String): PixelfedAPI { fun create(baseUrl: String): PixelfedAPI {
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(baseUrl) .baseUrl(baseUrl)
@ -41,7 +43,8 @@ interface PixelfedAPI {
@Field("redirect_uris") redirect_uris: String, @Field("redirect_uris") redirect_uris: String,
@Field("scopes") scopes: String? = null, @Field("scopes") scopes: String? = null,
@Field("website") website: String? = null @Field("website") website: String? = null
): Call<Application> ): Single<Application>
@FormUrlEncoded @FormUrlEncoded
@POST("/oauth/token") @POST("/oauth/token")
@ -52,7 +55,25 @@ interface PixelfedAPI {
@Field("scope") scope: String? = "read", @Field("scope") scope: String? = "read",
@Field("code") code: String? = null, @Field("code") code: String? = null,
@Field("grant_type") grant_type: String? = null @Field("grant_type") grant_type: String? = null
): Call<Token> ): Single<Token>
// get instance configuration
@GET("/api/v1/instance")
fun instance() : Single<Instance>
/**
* Instance info from the Nodeinfo .well_known (https://nodeinfo.diaspora.software/protocol.html) endpoint
*/
@GET("/.well-known/nodeinfo")
fun wellKnownNodeInfo() : Single<NodeInfoJRD>
/**
* Instance info from [NodeInfo] (https://nodeinfo.diaspora.software/schema.html) endpoint
*/
@GET
fun nodeInfoSchema(
@Url nodeInfo_schema_url: String
) : Call<NodeInfo>
@FormUrlEncoded @FormUrlEncoded
@POST("/api/v1/accounts/{id}/follow") @POST("/api/v1/accounts/{id}/follow")
@ -240,10 +261,6 @@ interface PixelfedAPI {
@Part file: MultipartBody.Part @Part file: MultipartBody.Part
): Observable<Attachment> ): Observable<Attachment>
// get instance configuration
@GET("/api/v1/instance")
fun instance() : Call<Instance>
// get discover // get discover
@GET("/api/v2/discover/posts") @GET("/api/v2/discover/posts")
fun discover( fun discover(

View File

@ -1,14 +1,14 @@
package com.h.pixeldroid.objects package com.h.pixeldroid.objects
data class Instance ( data class Instance (
val description: String, val description: String?,
val email: String, val email: String?,
val max_toot_chars: String = DEFAULT_MAX_TOOT_CHARS.toString(), val max_toot_chars: String? = DEFAULT_MAX_TOOT_CHARS.toString(),
val registrations: Boolean, val registrations: Boolean?,
val thumbnail: String, val thumbnail: String?,
val title: String, val title: String?,
val uri: String, val uri: String?,
val version: String val version: String?
) { ) {
companion object { companion object {
const val DEFAULT_MAX_TOOT_CHARS = 500 const val DEFAULT_MAX_TOOT_CHARS = 500

View File

@ -0,0 +1,69 @@
package com.h.pixeldroid.objects
/*
See https://nodeinfo.diaspora.software/schema.html and https://pixelfed.social/api/nodeinfo/2.0.json
A lot of attributes we don't need are omitted, if in the future they are needed we
can make new data classes for them.
*/
data class NodeInfo (
val version: String?,
val software: Software?,
val protocols: List<String>?,
val openRegistrations: Boolean?,
val metadata: PixelfedMetadata?
){
data class Software(
val name: String?,
val version: String?
)
data class PixelfedMetadata(
val nodeName: String?,
val software: Software?,
val config: PixelfedConfig
){
data class Software(
val homepage: String?,
val repo: String?
)
}
data class PixelfedConfig(
val open_registration: Boolean?,
val uploader: Uploader?,
val activitypub: ActivityPub?,
val features: Features?
){
data class Uploader(
val max_photo_size: String?,
val max_caption_length: String?,
val album_limit: String?,
val image_quality: String?,
val optimize_image: Boolean?,
val optimize_video: Boolean?,
val media_types: String?,
val enforce_account_limit: Boolean?
)
data class ActivityPub(
val enabled: Boolean?,
val remote_follow: Boolean?
)
data class Features(
val mobile_apis: Boolean?,
val circles: Boolean?,
val stories: Boolean?,
val video: Boolean?
)
}
}
data class NodeInfoJRD(
val links: List<Link>
){
data class Link(
val rel: String?,
val href: String?
)
}

View File

@ -1,8 +1,8 @@
package com.h.pixeldroid.objects package com.h.pixeldroid.objects
data class Token( data class Token(
val access_token: String, val access_token: String?,
val token_type: String, val token_type: String?,
val scope: String, val scope: String?,
val created_at: Int val created_at: Int?
) )

View File

@ -37,13 +37,13 @@ class DBUtils {
} }
fun storeInstance(db: AppDatabase, instance: Instance) { fun storeInstance(db: AppDatabase, instance: Instance) {
val maxTootChars = instance.max_toot_chars.toInt() val maxTootChars = instance.max_toot_chars?.toInt() ?: Instance.DEFAULT_MAX_TOOT_CHARS
val dbInstance = InstanceDatabaseEntity( val dbInstance = InstanceDatabaseEntity(
//make sure not to normalize to https when localhost, to allow testing //make sure not to normalize to https when localhost, to allow testing
uri = normalizeOrNot(instance.uri), uri = normalizeOrNot(instance.uri.orEmpty()),
title = instance.title, title = instance.title.orEmpty(),
max_toot_chars = maxTootChars, max_toot_chars = maxTootChars,
thumbnail = instance.thumbnail thumbnail = instance.thumbnail.orEmpty()
) )
db.instanceDao().insertInstance(dbInstance) db.instanceDao().insertInstance(dbInstance)
} }

View File

@ -9,6 +9,9 @@
<string name="auth_failed">"Could not authenticate"</string> <string name="auth_failed">"Could not authenticate"</string>
<string name="token_error">"Error getting token"</string> <string name="token_error">"Error getting token"</string>
<string name="instance_error">"Could not get instance information"</string> <string name="instance_error">"Could not get instance information"</string>
<string name="instance_not_pixelfed_warning">"This doesn't seem to be a Pixelfed instance, so the app could break in unexpected ways."</string>
<string name="instance_not_pixelfed_continue">"OK, continue anyway"</string>
<string name="instance_not_pixelfed_cancel">"Cancel logging in"</string>
<string name="title_activity_settings2">Settings</string> <string name="title_activity_settings2">Settings</string>
<!-- Theme Preferences --> <!-- Theme Preferences -->
<string name="theme_title">Application Theme</string> <string name="theme_title">Application Theme</string>

View File

@ -4,6 +4,8 @@ import com.github.tomakehurst.wiremock.client.WireMock.*
import com.github.tomakehurst.wiremock.junit.WireMockRule import com.github.tomakehurst.wiremock.junit.WireMockRule
import com.h.pixeldroid.api.PixelfedAPI import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.objects.* import com.h.pixeldroid.objects.*
import io.reactivex.Single
import okhttp3.internal.wait
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -106,9 +108,10 @@ class APIUnitTest {
.withHeader("Content-Type", "application/json") .withHeader("Content-Type", "application/json")
.withBody(""" {"id":3197,"name":"Pixeldroid","website":null,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":3197,"client_secret":"hhRwLupqUJPghKsZzpZtxNV67g5DBdPYCqW6XE3m","vapid_key":null}""" .withBody(""" {"id":3197,"name":"Pixeldroid","website":null,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":3197,"client_secret":"hhRwLupqUJPghKsZzpZtxNV67g5DBdPYCqW6XE3m","vapid_key":null}"""
))) )))
val call: Call<Application> = PixelfedAPI.create("http://localhost:8089") val call: Single<Application> = PixelfedAPI.create("http://localhost:8089")
.registerApplication("Pixeldroid", "urn:ietf:wg:oauth:2.0:oob", "read write follow") .registerApplication("Pixeldroid", "urn:ietf:wg:oauth:2.0:oob", "read write follow")
val application: Application = call.execute().body()!!
val application: Application = call.toFuture().get()
assertEquals("3197", application.client_id) assertEquals("3197", application.client_id)
assertEquals("hhRwLupqUJPghKsZzpZtxNV67g5DBdPYCqW6XE3m", application.client_secret) assertEquals("hhRwLupqUJPghKsZzpZtxNV67g5DBdPYCqW6XE3m", application.client_secret)
assertEquals("Pixeldroid", application.name) assertEquals("Pixeldroid", application.name)
@ -133,10 +136,10 @@ class APIUnitTest {
val OAUTH_SCHEME = "oauth2redirect" val OAUTH_SCHEME = "oauth2redirect"
val SCOPE = "read write follow" val SCOPE = "read write follow"
val PACKAGE_ID = "com.h.pixeldroid" val PACKAGE_ID = "com.h.pixeldroid"
val call: Call<Token> = PixelfedAPI.create("http://localhost:8089") val call: Single<Token> = PixelfedAPI.create("http://localhost:8089")
.obtainToken("123", "ssqdfqsdfqds", "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE, "abc", .obtainToken("123", "ssqdfqsdfqds", "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE, "abc",
"authorization_code") "authorization_code")
val token: Token = call.execute().body()!! val token: Token = call.toFuture().get()
assertEquals("ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0", token.access_token) assertEquals("ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0", token.access_token)
assertEquals("Bearer", token.token_type) assertEquals("Bearer", token.token_type)
assertEquals("read write follow push", token.scope) assertEquals("read write follow push", token.scope)

View File

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.3.72' ext.kotlin_version = '1.4.0'
repositories { repositories {
google() google()
jcenter() jcenter()

Binary file not shown.

View File

@ -1,6 +1,6 @@
#Sun Jul 26 16:18:03 CEST 2020 #Fri Aug 21 12:53:39 CEST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip

227
gradlew vendored
View File

@ -1,5 +1,21 @@
#!/usr/bin/env sh #!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed 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
#
# https://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.
#
############################################################################## ##############################################################################
## ##
## Gradle start up script for UN*X ## Gradle start up script for UN*X
@ -10,38 +26,38 @@
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" PRG="$0"
# Need this for relative symlinks. # Need this for relative symlinks.
while [ -h "$PRG" ]; do while [ -h "$PRG" ] ; do
ls=$(ls -ld "$PRG") ls=`ls -ld "$PRG"`
link=$(expr "$ls" : '.*-> \(.*\)$') link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' >/dev/null; then if expr "$link" : '/.*' > /dev/null; then
PRG="$link" PRG="$link"
else else
PRG=$(dirname "$PRG")"/$link" PRG=`dirname "$PRG"`"/$link"
fi fi
done done
SAVED="$(pwd)" SAVED="`pwd`"
cd "$(dirname \"$PRG\")/" >/dev/null cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="$(pwd -P)" APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=$(basename "$0") APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS="" DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD="maximum"
warn() { warn () {
echo "$*" echo "$*"
} }
die() { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} }
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
@ -49,124 +65,121 @@ cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "$(uname)" in case "`uname`" in
CYGWIN*) CYGWIN* )
cygwin=true cygwin=true
;; ;;
Darwin*) Darwin* )
darwin=true darwin=true
;; ;;
MINGW*) MINGW* )
msys=true msys=true
;; ;;
NONSTOP*) NONSTOP* )
nonstop=true nonstop=true
;; ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ]; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD="$JAVA_HOME/jre/sh/java"
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD="$JAVA_HOME/bin/java"
fi fi
if [ ! -x "$JAVACMD" ]; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=$(ulimit -H -n) MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ]; then if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT" MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi fi
ulimit -n $MAX_FD
if [ $? -ne 0 ]; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi fi
# For Darwin, add options to specify how the application appears in the dock # For Darwin, add options to specify how the application appears in the dock
if $darwin; then if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi fi
# For Cygwin, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if $cygwin; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=$(cygpath --path --mixed "$APP_HOME") APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=$(cygpath --path --mixed "$CLASSPATH") CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=$(cygpath --unix "$JAVACMD")
# We build the pattern for arguments to be converted via cygpath JAVACMD=`cygpath --unix "$JAVACMD"`
ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
SEP=""
for dir in $ROOTDIRSRAW; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ]; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@"; do
CHECK=$(echo "$arg" | egrep -c "$OURCYGPATTERN" -)
CHECK2=$(echo "$arg" | egrep -c "^-") ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition # We build the pattern for arguments to be converted via cygpath
eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg") ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
else SEP=""
eval $(echo args$i)="\"$arg\"" for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi fi
i=$((i + 1)) # Now convert the arguments - kludge to limit ourselves to /bin/sh
done i=0
case $i in for arg in "$@" ; do
0) set -- ;; CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
1) set -- "$args0" ;; CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;; if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
4) set -- "$args0" "$args1" "$args2" "$args3" ;; eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; else
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; eval `echo args$i`="\"$arg\""
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; fi
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; i=`expr $i + 1`
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; done
esac case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi fi
# Escape application args # Escape application args
save() { save () {
for i; do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"; done for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " " echo " "
} }
APP_ARGS=$(save "$@") APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules # Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

43
gradlew.bat vendored
View File

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS= set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -35,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -45,28 +64,14 @@ echo location of your Java installation.
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell