Add support for v2/instance (#4062)

…with fallback to v1
This commit is contained in:
Levi Bard 2023-10-25 12:53:10 +02:00 committed by GitHub
parent 6e6cf05d11
commit 131ebabe85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 344 additions and 142 deletions

View File

@ -34,11 +34,11 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.K
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.FilterV1 import com.keylesspalace.tusky.entity.FilterV1
import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@ -192,7 +192,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
updateTagMuteState(mutedFilter != null) updateTagMuteState(mutedFilter != null)
}, },
{ throwable -> { throwable ->
if (throwable is HttpException && throwable.code() == 404) { if (throwable.isHttpNotFound()) {
mastodonApi.getFiltersV1().fold( mastodonApi.getFiltersV1().fold(
{ filters -> { filters ->
mutedFilterV1 = filters.firstOrNull { filter -> mutedFilterV1 = filters.firstOrNull { filter ->
@ -251,7 +251,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
} }
}, },
{ throwable -> { throwable ->
if (throwable is HttpException && throwable.code() == 404) { if (throwable.isHttpNotFound()) {
mastodonApi.createFilterV1( mastodonApi.createFilterV1(
hashedTag, hashedTag,
listOf(FilterV1.HOME), listOf(FilterV1.HOME),

View File

@ -35,11 +35,11 @@ import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.db.DraftsAlert import com.keylesspalace.tusky.db.DraftsAlert
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
class DraftsActivity : BaseActivity(), DraftActionListener { class DraftsActivity : BaseActivity(), DraftActionListener {
@ -131,7 +131,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
Log.w(TAG, "failed loading reply information", throwable) Log.w(TAG, "failed loading reply information", throwable)
if (throwable is HttpException && throwable.code() == 404) { if (throwable.isHttpNotFound()) {
// the original status to which a reply was drafted has been deleted // the original status to which a reply was drafted has been deleted
// let's open the ComposeActivity without reply information // let's open the ComposeActivity without reply information
Toast.makeText(context, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show() Toast.makeText(context, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show()

View File

@ -26,10 +26,10 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.FilterKeyword import com.keylesspalace.tusky.entity.FilterKeyword
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@ -282,7 +282,7 @@ class EditFilterActivity : BaseActivity() {
finish() finish()
}, },
{ throwable -> { throwable ->
if (throwable is HttpException && throwable.code() == 404) { if (throwable.isHttpNotFound()) {
api.deleteFilterV1(filter.id).fold( api.deleteFilterV1(filter.id).fold(
{ {
finish() finish()

View File

@ -8,9 +8,9 @@ import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.FilterKeyword import com.keylesspalace.tusky.entity.FilterKeyword
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() { class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() {
@ -108,7 +108,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
}, },
{ throwable -> { throwable ->
return ( return (
throwable is HttpException && throwable.code() == 404 && throwable.isHttpNotFound() &&
// Endpoint not found, fall back to v1 api // Endpoint not found, fall back to v1 api
createFilterV1(contexts, expiresInSeconds) createFilterV1(contexts, expiresInSeconds)
) )
@ -141,7 +141,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
return results.none { it.isFailure } return results.none { it.isFailure }
}, },
{ throwable -> { throwable ->
if (throwable is HttpException && throwable.code() == 404) { if (throwable.isHttpNotFound()) {
// Endpoint not found, fall back to v1 api // Endpoint not found, fall back to v1 api
if (updateFilterV1(contexts, expiresInSeconds)) { if (updateFilterV1(contexts, expiresInSeconds)) {
return true return true

View File

@ -9,10 +9,10 @@ import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
class FiltersViewModel @Inject constructor( class FiltersViewModel @Inject constructor(
@ -38,14 +38,13 @@ class FiltersViewModel @Inject constructor(
this@FiltersViewModel._state.value = State(filters, LoadingState.LOADED) this@FiltersViewModel._state.value = State(filters, LoadingState.LOADED)
}, },
{ throwable -> { throwable ->
if (throwable is HttpException && throwable.code() == 404) { if (throwable.isHttpNotFound()) {
api.getFiltersV1().fold( api.getFiltersV1().fold(
{ filters -> { filters ->
this@FiltersViewModel._state.value = State(filters.map { it.toFilter() }, LoadingState.LOADED) this@FiltersViewModel._state.value = State(filters.map { it.toFilter() }, LoadingState.LOADED)
}, },
{ throwable -> { _ ->
// TODO log errors (also below) // TODO log errors (also below)
this@FiltersViewModel._state.value = _state.value.copy(loadingState = LoadingState.ERROR_OTHER) this@FiltersViewModel._state.value = _state.value.copy(loadingState = LoadingState.ERROR_OTHER)
} }
) )
@ -68,7 +67,7 @@ class FiltersViewModel @Inject constructor(
} }
}, },
{ throwable -> { throwable ->
if (throwable is HttpException && throwable.code() == 404) { if (throwable.isHttpNotFound()) {
api.deleteFilterV1(filter.id).fold( api.deleteFilterV1(filter.id).fold(
{ {
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED) this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)

View File

@ -25,6 +25,7 @@ import com.keylesspalace.tusky.db.EmojisEntity
import com.keylesspalace.tusky.db.InstanceInfoEntity import com.keylesspalace.tusky.db.InstanceInfoEntity
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -63,27 +64,31 @@ class InstanceInfoRepository @Inject constructor(
{ instance -> { instance ->
val instanceEntity = InstanceInfoEntity( val instanceEntity = InstanceInfoEntity(
instance = instanceName, instance = instanceName,
maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars, maximumTootCharacters = instance.configuration.statuses.maxCharacters,
maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions, maxPollOptions = instance.configuration.polls.maxOptions,
maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars, maxPollOptionLength = instance.configuration.polls.maxCharactersPerOption,
minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration, minPollDuration = instance.configuration.polls.minExpirationSeconds,
maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration, maxPollDuration = instance.configuration.polls.maxExpirationSeconds,
charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl, charactersReservedPerUrl = instance.configuration.statuses.charactersReservedPerUrl,
version = instance.version, version = instance.version,
videoSizeLimit = instance.configuration?.mediaAttachments?.videoSizeLimit ?: instance.uploadLimit, videoSizeLimit = instance.configuration.mediaAttachments.videoSizeLimitBytes.toInt(),
imageSizeLimit = instance.configuration?.mediaAttachments?.imageSizeLimit ?: instance.uploadLimit, imageSizeLimit = instance.configuration.mediaAttachments.imageSizeLimitBytes.toInt(),
imageMatrixLimit = instance.configuration?.mediaAttachments?.imageMatrixLimit, imageMatrixLimit = instance.configuration.mediaAttachments.imagePixelCountLimit.toInt(),
maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments, maxMediaAttachments = instance.configuration.statuses.maxMediaAttachments,
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields, maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields,
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength, maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength,
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength,
) )
dao.upsert(instanceEntity) dao.upsert(instanceEntity)
instanceEntity instanceEntity
}, },
{ throwable -> { throwable ->
Log.w(TAG, "failed to instance, falling back to cache and default values", throwable) if (throwable.isHttpNotFound()) {
dao.getInstanceInfo(instanceName) getInstanceInfoV1()
} else {
Log.w(TAG, "failed to instance, falling back to cache and default values", throwable)
dao.getInstanceInfo(instanceName)
}
} }
).let { instanceInfo: InstanceInfoEntity? -> ).let { instanceInfo: InstanceInfoEntity? ->
InstanceInfo( InstanceInfo(
@ -100,11 +105,42 @@ class InstanceInfoRepository @Inject constructor(
maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS, maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS,
maxFieldNameLength = instanceInfo?.maxFieldNameLength, maxFieldNameLength = instanceInfo?.maxFieldNameLength,
maxFieldValueLength = instanceInfo?.maxFieldValueLength, maxFieldValueLength = instanceInfo?.maxFieldValueLength,
version = instanceInfo?.version version = instanceInfo?.version,
) )
} }
} }
private suspend fun getInstanceInfoV1(): InstanceInfoEntity? = withContext(Dispatchers.IO) {
api.getInstanceV1()
.fold(
{ instance ->
val instanceEntity = InstanceInfoEntity(
instance = instanceName,
maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars,
maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions,
maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars,
minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration,
maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration,
charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl,
version = instance.version,
videoSizeLimit = instance.configuration?.mediaAttachments?.videoSizeLimit ?: instance.uploadLimit,
imageSizeLimit = instance.configuration?.mediaAttachments?.imageSizeLimit ?: instance.uploadLimit,
imageMatrixLimit = instance.configuration?.mediaAttachments?.imageMatrixLimit,
maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments,
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields,
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength,
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength,
)
dao.upsert(instanceEntity)
instanceEntity
},
{ throwable ->
Log.w(TAG, "failed to instance, falling back to cache and default values", throwable)
dao.getInstanceInfo(instanceName)
}
)
}
companion object { companion object {
private const val TAG = "InstanceInfoRepo" private const val TAG = "InstanceInfoRepo"

View File

@ -20,6 +20,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.fold
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -36,11 +37,25 @@ class LoginWebViewViewModel @Inject constructor(
if (this.domain == null) { if (this.domain == null) {
this.domain = domain this.domain = domain
viewModelScope.launch { viewModelScope.launch {
api.getInstance(domain).fold({ instance -> api.getInstance().fold(
instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty() { instance ->
}, { throwable -> instanceRules.value = instance.rules.map { rule -> rule.text }
Log.w("LoginWebViewViewModel", "failed to load instance info", throwable) },
}) { throwable ->
if (throwable.isHttpNotFound()) {
api.getInstanceV1(domain).fold(
{ instance ->
instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty()
},
{ throwable ->
Log.w("LoginWebViewViewModel", "failed to load instance info", throwable)
}
)
} else {
Log.w("LoginWebViewViewModel", "failed to load instance info", throwable)
}
}
)
} }
} }
} }

View File

@ -45,11 +45,11 @@ import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException
abstract class TimelineViewModel( abstract class TimelineViewModel(
private val timelineCases: TimelineCases, private val timelineCases: TimelineCases,
@ -281,7 +281,7 @@ abstract class TimelineViewModel(
invalidate() invalidate()
}, },
{ throwable -> { throwable ->
if (throwable is HttpException && throwable.code() == 404) { if (throwable.isHttpNotFound()) {
// Fallback to client-side filter code // Fallback to client-side filter code
val filters = api.getFiltersV1().getOrElse { val filters = api.getFiltersV1().getOrElse {
Log.e(TAG, "Failed to fetch filters", it) Log.e(TAG, "Failed to fetch filters", it)

View File

@ -37,6 +37,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.util.toViewData
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -47,7 +48,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
class ViewThreadViewModel @Inject constructor( class ViewThreadViewModel @Inject constructor(
@ -391,7 +391,7 @@ class ViewThreadViewModel @Inject constructor(
updateStatuses() updateStatuses()
}, },
{ throwable -> { throwable ->
if (throwable is HttpException && throwable.code() == 404) { if (throwable.isHttpNotFound()) {
val filters = api.getFiltersV1().getOrElse { val filters = api.getFiltersV1().getOrElse {
Log.w(TAG, "Failed to fetch filters", it) Log.w(TAG, "Failed to fetch filters", it)
return@launch return@launch

View File

@ -1,98 +1,71 @@
/* Copyright 2018 Levi Bard
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.entity package com.keylesspalace.tusky.entity
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class Instance( data class Instance(
val uri: String, val domain: String,
// val title: String, // val title: String,
// val description: String,
// val email: String,
val version: String, val version: String,
// val urls: Map<String, String>, // @SerializedName("source_url") val sourceUrl: String,
// val stats: Map<String, Int>?, // val description: String,
// val thumbnail: String?, // val usage: Usage,
// val languages: List<String>, // val thumbnail: Thumbnail,
// @SerializedName("contact_account") val contactAccount: Account, // val languages: List<String>,
@SerializedName("max_toot_chars") val maxTootChars: Int?, val configuration: Configuration,
@SerializedName("poll_limits") val pollConfiguration: PollConfiguration?, // val registrations: Registrations,
val configuration: InstanceConfiguration?, // val contact: Contact,
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?, val rules: List<Rule>,
val pleroma: PleromaConfiguration?, val pleroma: PleromaConfiguration?,
@SerializedName("upload_limit") val uploadLimit: Int?,
val rules: List<InstanceRules>?
) { ) {
override fun hashCode(): Int { data class Usage(val users: Users) {
return uri.hashCode() data class Users(@SerializedName("active_month") val activeMonth: Int)
} }
data class Thumbnail(
override fun equals(other: Any?): Boolean { val url: String,
if (other !is Instance) { val blurhash: String?,
return false val versions: Versions?,
} ) {
val instance = other as Instance? data class Versions(
return instance?.uri.equals(uri) @SerializedName("@1x") val at1x: String?,
@SerializedName("@2x") val at2x: String?,
)
} }
data class Configuration(
val urls: Urls,
val accounts: Accounts,
val statuses: Statuses,
@SerializedName("media_attachments") val mediaAttachments: MediaAttachments,
val polls: Polls,
val translation: Translation,
) {
data class Urls(@SerializedName("streaming_api") val streamingApi: String)
data class Accounts(@SerializedName("max_featured_tags") val maxFeaturedTags: Int)
data class Statuses(
@SerializedName("max_characters") val maxCharacters: Int,
@SerializedName("max_media_attachments") val maxMediaAttachments: Int,
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int,
)
data class MediaAttachments(
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>,
@SerializedName("image_size_limit") val imageSizeLimitBytes: Long,
@SerializedName("image_matrix_limit") val imagePixelCountLimit: Long,
@SerializedName("video_size_limit") val videoSizeLimitBytes: Long,
@SerializedName("video_matrix_limit") val videoPixelCountLimit: Long,
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int,
)
data class Polls(
@SerializedName("max_options") val maxOptions: Int,
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int,
@SerializedName("min_expiration") val minExpirationSeconds: Int,
@SerializedName("max_expiration") val maxExpirationSeconds: Int,
)
data class Translation(val enabled: Boolean)
}
data class Registrations(
val enabled: Boolean,
@SerializedName("approval_required") val approvalRequired: Boolean,
val message: String?,
)
data class Contact(val email: String, val account: Account)
data class Rule(val id: String, val text: String)
} }
data class PollConfiguration(
@SerializedName("max_options") val maxOptions: Int?,
@SerializedName("max_option_chars") val maxOptionChars: Int?,
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?,
@SerializedName("min_expiration") val minExpiration: Int?,
@SerializedName("max_expiration") val maxExpiration: Int?
)
data class InstanceConfiguration(
val statuses: StatusConfiguration?,
@SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
val polls: PollConfiguration?
)
data class StatusConfiguration(
@SerializedName("max_characters") val maxCharacters: Int?,
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?
)
data class MediaAttachmentConfiguration(
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>?,
@SerializedName("image_size_limit") val imageSizeLimit: Int?,
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int?,
@SerializedName("video_size_limit") val videoSizeLimit: Int?,
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?,
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int?
)
data class PleromaConfiguration(
val metadata: PleromaMetadata?
)
data class PleromaMetadata(
@SerializedName("fields_limits") val fieldLimits: PleromaFieldLimits
)
data class PleromaFieldLimits(
@SerializedName("max_fields") val maxFields: Int?,
@SerializedName("name_length") val nameLength: Int?,
@SerializedName("value_length") val valueLength: Int?
)
data class InstanceRules(
val id: String,
val text: String
)

View File

@ -0,0 +1,98 @@
/* Copyright 2018 Levi Bard
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.entity
import com.google.gson.annotations.SerializedName
data class InstanceV1(
val uri: String,
// val title: String,
// val description: String,
// val email: String,
val version: String,
// val urls: Map<String, String>,
// val stats: Map<String, Int>?,
// val thumbnail: String?,
// val languages: List<String>,
// @SerializedName("contact_account") val contactAccount: Account,
@SerializedName("max_toot_chars") val maxTootChars: Int?,
@SerializedName("poll_limits") val pollConfiguration: PollConfiguration?,
val configuration: InstanceConfiguration?,
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
val pleroma: PleromaConfiguration?,
@SerializedName("upload_limit") val uploadLimit: Int?,
val rules: List<InstanceRules>?
) {
override fun hashCode(): Int {
return uri.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other !is InstanceV1) {
return false
}
val instance = other as InstanceV1?
return instance?.uri.equals(uri)
}
}
data class PollConfiguration(
@SerializedName("max_options") val maxOptions: Int?,
@SerializedName("max_option_chars") val maxOptionChars: Int?,
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?,
@SerializedName("min_expiration") val minExpiration: Int?,
@SerializedName("max_expiration") val maxExpiration: Int?
)
data class InstanceConfiguration(
val statuses: StatusConfiguration?,
@SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
val polls: PollConfiguration?
)
data class StatusConfiguration(
@SerializedName("max_characters") val maxCharacters: Int?,
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?
)
data class MediaAttachmentConfiguration(
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>?,
@SerializedName("image_size_limit") val imageSizeLimit: Int?,
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int?,
@SerializedName("video_size_limit") val videoSizeLimit: Int?,
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?,
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int?
)
data class PleromaConfiguration(
val metadata: PleromaMetadata?
)
data class PleromaMetadata(
@SerializedName("fields_limits") val fieldLimits: PleromaFieldLimits
)
data class PleromaFieldLimits(
@SerializedName("max_fields") val maxFields: Int?,
@SerializedName("name_length") val nameLength: Int?,
@SerializedName("value_length") val valueLength: Int?
)
data class InstanceRules(
val id: String,
val text: String
)

View File

@ -29,6 +29,7 @@ import com.keylesspalace.tusky.entity.FilterKeyword
import com.keylesspalace.tusky.entity.FilterV1 import com.keylesspalace.tusky.entity.FilterV1
import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.entity.InstanceV1
import com.keylesspalace.tusky.entity.Marker import com.keylesspalace.tusky.entity.Marker
import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.entity.MediaUploadResult import com.keylesspalace.tusky.entity.MediaUploadResult
@ -84,7 +85,10 @@ interface MastodonApi {
suspend fun getCustomEmojis(): NetworkResult<List<Emoji>> suspend fun getCustomEmojis(): NetworkResult<List<Emoji>>
@GET("api/v1/instance") @GET("api/v1/instance")
suspend fun getInstance(@Header(DOMAIN_HEADER) domain: String? = null): NetworkResult<Instance> suspend fun getInstanceV1(@Header(DOMAIN_HEADER) domain: String? = null): NetworkResult<InstanceV1>
@GET("api/v2/instance")
suspend fun getInstance(): NetworkResult<Instance>
@GET("api/v1/filters") @GET("api/v1/filters")
suspend fun getFiltersV1(): NetworkResult<List<FilterV1>> suspend fun getFiltersV1(): NetworkResult<List<FilterV1>>

View File

@ -40,3 +40,5 @@ fun Throwable.getErrorString(context: Context): String = getServerErrorMessage()
is IOException -> context.getString(R.string.error_network) is IOException -> context.getString(R.string.error_network)
else -> context.getString(R.string.error_generic) else -> context.getString(R.string.error_generic)
} }
fun Throwable.isHttpNotFound(): Boolean = (this as? HttpException)?.code() == 404

View File

@ -35,8 +35,11 @@ import com.keylesspalace.tusky.db.InstanceInfoEntity
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.entity.InstanceConfiguration import com.keylesspalace.tusky.entity.InstanceConfiguration
import com.keylesspalace.tusky.entity.InstanceV1
import com.keylesspalace.tusky.entity.StatusConfiguration import com.keylesspalace.tusky.entity.StatusConfiguration
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -51,6 +54,8 @@ import org.robolectric.Robolectric
import org.robolectric.Shadows.shadowOf import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import org.robolectric.fakes.RoboMenuItem import org.robolectric.fakes.RoboMenuItem
import retrofit2.HttpException
import retrofit2.Response
import java.util.Locale import java.util.Locale
/** /**
@ -87,6 +92,7 @@ class ComposeActivityTest {
notificationVibration = true, notificationVibration = true,
notificationLight = true notificationLight = true
) )
private var instanceV1ResponseCallback: (() -> InstanceV1)? = null
private var instanceResponseCallback: (() -> Instance)? = null private var instanceResponseCallback: (() -> Instance)? = null
private var composeOptions: ComposeActivity.ComposeOptions? = null private var composeOptions: ComposeActivity.ComposeOptions? = null
@ -102,6 +108,13 @@ class ComposeActivityTest {
apiMock = mock { apiMock = mock {
onBlocking { getCustomEmojis() } doReturn NetworkResult.success(emptyList()) onBlocking { getCustomEmojis() } doReturn NetworkResult.success(emptyList())
onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance -> onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
if (instance == null) {
NetworkResult.failure(HttpException(Response.error<ResponseBody>(404, "Not found".toResponseBody())))
} else {
NetworkResult.success(instance)
}
}
onBlocking { getInstanceV1() } doReturn instanceV1ResponseCallback?.invoke().let { instance ->
if (instance == null) { if (instance == null) {
NetworkResult.failure(Throwable()) NetworkResult.failure(Throwable())
} else { } else {
@ -192,22 +205,13 @@ class ComposeActivityTest {
@Test @Test
fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() { fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() {
instanceResponseCallback = { getInstanceWithCustomConfiguration(null) } instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(null) }
setupActivity() setupActivity()
assertEquals(InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters) assertEquals(InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters)
} }
@Test @Test
fun whenMaximumTootCharsIsPopulated_customLimitIsUsed() { fun whenMaximumTootCharsIsPopulated_customLimitIsUsed() {
val customMaximum = 1000
instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) }
setupActivity()
shadowOf(getMainLooper()).idle()
assertEquals(customMaximum, activity.maximumTootCharacters)
}
@Test
fun whenOnlyLegacyMaximumTootCharsIsPopulated_customLimitIsUsed() {
val customMaximum = 1000 val customMaximum = 1000
instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum) } instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum) }
setupActivity() setupActivity()
@ -215,10 +219,19 @@ class ComposeActivityTest {
assertEquals(customMaximum, activity.maximumTootCharacters) assertEquals(customMaximum, activity.maximumTootCharacters)
} }
@Test
fun whenOnlyLegacyMaximumTootCharsIsPopulated_customLimitIsUsed() {
val customMaximum = 1000
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(customMaximum) }
setupActivity()
shadowOf(getMainLooper()).idle()
assertEquals(customMaximum, activity.maximumTootCharacters)
}
@Test @Test
fun whenOnlyConfigurationMaximumTootCharsIsPopulated_customLimitIsUsed() { fun whenOnlyConfigurationMaximumTootCharsIsPopulated_customLimitIsUsed() {
val customMaximum = 1000 val customMaximum = 1000
instanceResponseCallback = { getInstanceWithCustomConfiguration(null, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) } instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(null, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) }
setupActivity() setupActivity()
shadowOf(getMainLooper()).idle() shadowOf(getMainLooper()).idle()
assertEquals(customMaximum, activity.maximumTootCharacters) assertEquals(customMaximum, activity.maximumTootCharacters)
@ -227,7 +240,7 @@ class ComposeActivityTest {
@Test @Test
fun whenDifferentCharLimitsArePopulated_statusConfigurationLimitIsUsed() { fun whenDifferentCharLimitsArePopulated_statusConfigurationLimitIsUsed() {
val customMaximum = 1000 val customMaximum = 1000
instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum * 2)) } instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum * 2)) }
setupActivity() setupActivity()
shadowOf(getMainLooper()).idle() shadowOf(getMainLooper()).idle()
assertEquals(customMaximum * 2, activity.maximumTootCharacters) assertEquals(customMaximum * 2, activity.maximumTootCharacters)
@ -270,7 +283,19 @@ class ComposeActivityTest {
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
val additionalContent = "Check out this @image #search result: " val additionalContent = "Check out this @image #search result: "
val customUrlLength = 16 val customUrlLength = 16
instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) } instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) }
setupActivity()
shadowOf(getMainLooper()).idle()
insertSomeTextInContent(additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + customUrlLength)
}
@Test
fun whenTextContainsUrl_onlyEllipsizedURLIsCounted_withCustomConfigurationV1() {
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
val additionalContent = "Check out this @image #search result: "
val customUrlLength = 16
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
setupActivity() setupActivity()
shadowOf(getMainLooper()).idle() shadowOf(getMainLooper()).idle()
insertSomeTextInContent(additionalContent + url) insertSomeTextInContent(additionalContent + url)
@ -283,7 +308,20 @@ class ComposeActivityTest {
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
val additionalContent = " Check out this @image #search result: " val additionalContent = " Check out this @image #search result: "
val customUrlLength = 18 // The intention is that this is longer than shortUrl.length val customUrlLength = 18 // The intention is that this is longer than shortUrl.length
instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) } instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) }
setupActivity()
shadowOf(getMainLooper()).idle()
insertSomeTextInContent(shortUrl + additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
}
@Test
fun whenTextContainsShortUrls_allUrlsGetEllipsized_withCustomConfigurationV1() {
val shortUrl = "https://tusky.app"
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
val additionalContent = " Check out this @image #search result: "
val customUrlLength = 18 // The intention is that this is longer than shortUrl.length
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
setupActivity() setupActivity()
shadowOf(getMainLooper()).idle() shadowOf(getMainLooper()).idle()
insertSomeTextInContent(shortUrl + additionalContent + url) insertSomeTextInContent(shortUrl + additionalContent + url)
@ -295,7 +333,19 @@ class ComposeActivityTest {
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
val additionalContent = " Check out this @image #search result: " val additionalContent = " Check out this @image #search result: "
val customUrlLength = 16 val customUrlLength = 16
instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) } instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) }
setupActivity()
shadowOf(getMainLooper()).idle()
insertSomeTextInContent(url + additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
}
@Test
fun whenTextContainsMultipleURLs_allURLsGetEllipsized_withCustomConfigurationV1() {
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
val additionalContent = " Check out this @image #search result: "
val customUrlLength = 16
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
setupActivity() setupActivity()
shadowOf(getMainLooper()).idle() shadowOf(getMainLooper()).idle()
insertSomeTextInContent(url + additionalContent + url) insertSomeTextInContent(url + additionalContent + url)
@ -491,8 +541,33 @@ class ComposeActivityTest {
activity.findViewById<EditText>(R.id.composeEditField).setText(text ?: "Some text") activity.findViewById<EditText>(R.id.composeEditField).setText(text ?: "Some text")
} }
private fun getInstanceWithCustomConfiguration(maximumLegacyTootCharacters: Int? = null, configuration: InstanceConfiguration? = null): Instance { private fun getInstanceWithCustomConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): Instance {
return Instance( return Instance(
domain = "https://example.token",
version = "2.6.3",
configuration = getConfiguration(maximumStatusCharacters, charactersReservedPerUrl),
pleroma = null,
rules = emptyList()
)
}
private fun getConfiguration(maximumStatusCharacters: Int?, charactersReservedPerUrl: Int?): Instance.Configuration {
return Instance.Configuration(
Instance.Configuration.Urls(streamingApi = ""),
Instance.Configuration.Accounts(1),
Instance.Configuration.Statuses(
maximumStatusCharacters ?: InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT,
InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS,
charactersReservedPerUrl ?: InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL
),
Instance.Configuration.MediaAttachments(emptyList(), 0, 0, 0, 0, 0),
Instance.Configuration.Polls(0, 0, 0, 0),
Instance.Configuration.Translation(false),
)
}
private fun getInstanceV1WithCustomConfiguration(maximumLegacyTootCharacters: Int? = null, configuration: InstanceConfiguration? = null): InstanceV1 {
return InstanceV1(
uri = "https://example.token", uri = "https://example.token",
version = "2.6.3", version = "2.6.3",
maxTootChars = maximumLegacyTootCharacters, maxTootChars = maximumLegacyTootCharacters,