|
|
|
|
@ -16,28 +16,64 @@
|
|
|
|
|
package com.keylesspalace.tusky.components.instanceinfo |
|
|
|
|
|
|
|
|
|
import android.util.Log |
|
|
|
|
import at.connyduck.calladapter.networkresult.fold |
|
|
|
|
import at.connyduck.calladapter.networkresult.NetworkResult |
|
|
|
|
import at.connyduck.calladapter.networkresult.getOrElse |
|
|
|
|
import at.connyduck.calladapter.networkresult.getOrThrow |
|
|
|
|
import at.connyduck.calladapter.networkresult.map |
|
|
|
|
import at.connyduck.calladapter.networkresult.onSuccess |
|
|
|
|
import at.connyduck.calladapter.networkresult.recover |
|
|
|
|
import com.keylesspalace.tusky.db.AccountManager |
|
|
|
|
import com.keylesspalace.tusky.db.AppDatabase |
|
|
|
|
import com.keylesspalace.tusky.db.EmojisEntity |
|
|
|
|
import com.keylesspalace.tusky.db.InstanceInfoEntity |
|
|
|
|
import com.keylesspalace.tusky.di.ApplicationScope |
|
|
|
|
import com.keylesspalace.tusky.entity.Emoji |
|
|
|
|
import com.keylesspalace.tusky.entity.Instance |
|
|
|
|
import com.keylesspalace.tusky.entity.InstanceV1 |
|
|
|
|
import com.keylesspalace.tusky.network.MastodonApi |
|
|
|
|
import com.keylesspalace.tusky.util.isHttpNotFound |
|
|
|
|
import java.util.concurrent.ConcurrentHashMap |
|
|
|
|
import javax.inject.Inject |
|
|
|
|
import javax.inject.Singleton |
|
|
|
|
import kotlinx.coroutines.CoroutineScope |
|
|
|
|
import kotlinx.coroutines.Dispatchers |
|
|
|
|
import kotlinx.coroutines.launch |
|
|
|
|
import kotlinx.coroutines.withContext |
|
|
|
|
|
|
|
|
|
@Singleton |
|
|
|
|
class InstanceInfoRepository @Inject constructor( |
|
|
|
|
private val api: MastodonApi, |
|
|
|
|
db: AppDatabase, |
|
|
|
|
accountManager: AccountManager |
|
|
|
|
private val accountManager: AccountManager, |
|
|
|
|
@ApplicationScope |
|
|
|
|
private val externalScope: CoroutineScope |
|
|
|
|
) { |
|
|
|
|
|
|
|
|
|
private val dao = db.instanceDao() |
|
|
|
|
private val instanceName = accountManager.activeAccount!!.domain |
|
|
|
|
private val instanceName |
|
|
|
|
get() = accountManager.activeAccount!!.domain |
|
|
|
|
|
|
|
|
|
/** In-memory cache for instance data, per instance domain. */ |
|
|
|
|
private var instanceInfoCache = ConcurrentHashMap<String, InstanceInfo>() |
|
|
|
|
|
|
|
|
|
fun precache() { |
|
|
|
|
// We are avoiding some duplicate work but we are not trying too hard. |
|
|
|
|
// We might request it multiple times in parallel which is not a big problem. |
|
|
|
|
// We might also get the results in random order or write them twice but it's also |
|
|
|
|
// not a problem. |
|
|
|
|
// We are just trying to avoid 2 things: |
|
|
|
|
// - fetching it when we already have it |
|
|
|
|
// - caching default value (we want to rather re-fetch if it fails) |
|
|
|
|
if (instanceInfoCache[instanceName] == null) { |
|
|
|
|
externalScope.launch { |
|
|
|
|
fetchAndPersistInstanceInfo().onSuccess { fetched -> |
|
|
|
|
instanceInfoCache[fetched.instance] = fetched.toInfoOrDefault() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
val cachedInstanceInfoOrFallback: InstanceInfo |
|
|
|
|
get() = instanceInfoCache[instanceName] ?: null.toInfoOrDefault() |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Returns the custom emojis of the instance. |
|
|
|
|
@ -58,97 +94,114 @@ class InstanceInfoRepository @Inject constructor(
|
|
|
|
|
* Will always try to fetch the most up-to-date data from the api, falls back to cache in case it is not available. |
|
|
|
|
* Never throws, returns defaults of vanilla Mastodon in case of error. |
|
|
|
|
*/ |
|
|
|
|
suspend fun getInstanceInfo(): InstanceInfo = withContext(Dispatchers.IO) { |
|
|
|
|
api.getInstance() |
|
|
|
|
.fold( |
|
|
|
|
{ instance -> |
|
|
|
|
val instanceEntity = InstanceInfoEntity( |
|
|
|
|
instance = instanceName, |
|
|
|
|
maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: DEFAULT_CHARACTER_LIMIT, |
|
|
|
|
maxPollOptions = instance.configuration?.polls?.maxOptions ?: DEFAULT_MAX_OPTION_COUNT, |
|
|
|
|
maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: DEFAULT_MAX_OPTION_LENGTH, |
|
|
|
|
minPollDuration = instance.configuration?.polls?.minExpirationSeconds ?: DEFAULT_MIN_POLL_DURATION, |
|
|
|
|
maxPollDuration = instance.configuration?.polls?.maxExpirationSeconds ?: DEFAULT_MAX_POLL_DURATION, |
|
|
|
|
charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL, |
|
|
|
|
version = instance.version, |
|
|
|
|
videoSizeLimit = instance.configuration?.mediaAttachments?.videoSizeLimitBytes?.toInt() ?: DEFAULT_VIDEO_SIZE_LIMIT, |
|
|
|
|
imageSizeLimit = instance.configuration?.mediaAttachments?.imageSizeLimitBytes?.toInt() ?: DEFAULT_IMAGE_SIZE_LIMIT, |
|
|
|
|
imageMatrixLimit = instance.configuration?.mediaAttachments?.imagePixelCountLimit?.toInt() ?: DEFAULT_IMAGE_MATRIX_LIMIT, |
|
|
|
|
maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS, |
|
|
|
|
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields, |
|
|
|
|
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength, |
|
|
|
|
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength |
|
|
|
|
) |
|
|
|
|
dao.upsert(instanceEntity) |
|
|
|
|
instanceEntity |
|
|
|
|
}, |
|
|
|
|
{ throwable -> |
|
|
|
|
if (throwable.isHttpNotFound()) { |
|
|
|
|
getInstanceInfoV1() |
|
|
|
|
} else { |
|
|
|
|
Log.w( |
|
|
|
|
TAG, |
|
|
|
|
"failed to instance, falling back to cache and default values", |
|
|
|
|
throwable |
|
|
|
|
) |
|
|
|
|
dao.getInstanceInfo(instanceName) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
).let { instanceInfo: InstanceInfoEntity? -> |
|
|
|
|
InstanceInfo( |
|
|
|
|
maxChars = instanceInfo?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, |
|
|
|
|
pollMaxOptions = instanceInfo?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, |
|
|
|
|
pollMaxLength = instanceInfo?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, |
|
|
|
|
pollMinDuration = instanceInfo?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION, |
|
|
|
|
pollMaxDuration = instanceInfo?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION, |
|
|
|
|
charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL, |
|
|
|
|
videoSizeLimit = instanceInfo?.videoSizeLimit ?: DEFAULT_VIDEO_SIZE_LIMIT, |
|
|
|
|
imageSizeLimit = instanceInfo?.imageSizeLimit ?: DEFAULT_IMAGE_SIZE_LIMIT, |
|
|
|
|
imageMatrixLimit = instanceInfo?.imageMatrixLimit ?: DEFAULT_IMAGE_MATRIX_LIMIT, |
|
|
|
|
maxMediaAttachments = instanceInfo?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS, |
|
|
|
|
maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS, |
|
|
|
|
maxFieldNameLength = instanceInfo?.maxFieldNameLength, |
|
|
|
|
maxFieldValueLength = instanceInfo?.maxFieldValueLength, |
|
|
|
|
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 -> |
|
|
|
|
suspend fun getUpdatedInstanceInfoOrFallback(): InstanceInfo = |
|
|
|
|
withContext(Dispatchers.IO) { |
|
|
|
|
fetchAndPersistInstanceInfo() |
|
|
|
|
.getOrElse { throwable -> |
|
|
|
|
Log.w( |
|
|
|
|
TAG, |
|
|
|
|
"failed to instance, falling back to cache and default values", |
|
|
|
|
"failed to load instance, falling back to cache and default values", |
|
|
|
|
throwable |
|
|
|
|
) |
|
|
|
|
dao.getInstanceInfo(instanceName) |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
}.toInfoOrDefault() |
|
|
|
|
|
|
|
|
|
private suspend fun InstanceInfoRepository.fetchAndPersistInstanceInfo(): NetworkResult<InstanceInfoEntity> = |
|
|
|
|
fetchRemoteInstanceInfo() |
|
|
|
|
.onSuccess { instanceInfoEntity -> |
|
|
|
|
dao.upsert(instanceInfoEntity) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private suspend fun fetchRemoteInstanceInfo(): NetworkResult<InstanceInfoEntity> { |
|
|
|
|
val instance = this.instanceName |
|
|
|
|
return api.getInstance() |
|
|
|
|
.map { it.toEntity() } |
|
|
|
|
.recover { t -> |
|
|
|
|
if (t.isHttpNotFound()) { |
|
|
|
|
api.getInstanceV1().map { it.toEntity(instance) }.getOrThrow() |
|
|
|
|
} else { |
|
|
|
|
throw t |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun InstanceInfoEntity?.toInfoOrDefault() = InstanceInfo( |
|
|
|
|
maxChars = this?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, |
|
|
|
|
pollMaxOptions = this?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, |
|
|
|
|
pollMaxLength = this?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, |
|
|
|
|
pollMinDuration = this?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION, |
|
|
|
|
pollMaxDuration = this?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION, |
|
|
|
|
charactersReservedPerUrl = this?.charactersReservedPerUrl |
|
|
|
|
?: DEFAULT_CHARACTERS_RESERVED_PER_URL, |
|
|
|
|
videoSizeLimit = this?.videoSizeLimit ?: DEFAULT_VIDEO_SIZE_LIMIT, |
|
|
|
|
imageSizeLimit = this?.imageSizeLimit ?: DEFAULT_IMAGE_SIZE_LIMIT, |
|
|
|
|
imageMatrixLimit = this?.imageMatrixLimit ?: DEFAULT_IMAGE_MATRIX_LIMIT, |
|
|
|
|
maxMediaAttachments = this?.maxMediaAttachments |
|
|
|
|
?: DEFAULT_MAX_MEDIA_ATTACHMENTS, |
|
|
|
|
maxFields = this?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS, |
|
|
|
|
maxFieldNameLength = this?.maxFieldNameLength, |
|
|
|
|
maxFieldValueLength = this?.maxFieldValueLength, |
|
|
|
|
version = this?.version, |
|
|
|
|
translationEnabled = this?.translationEnabled |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
private fun Instance.toEntity() = InstanceInfoEntity( |
|
|
|
|
instance = domain, |
|
|
|
|
maximumTootCharacters = this.configuration?.statuses?.maxCharacters |
|
|
|
|
?: DEFAULT_CHARACTER_LIMIT, |
|
|
|
|
maxPollOptions = this.configuration?.polls?.maxOptions ?: DEFAULT_MAX_OPTION_COUNT, |
|
|
|
|
maxPollOptionLength = this.configuration?.polls?.maxCharactersPerOption |
|
|
|
|
?: DEFAULT_MAX_OPTION_LENGTH, |
|
|
|
|
minPollDuration = this.configuration?.polls?.minExpirationSeconds |
|
|
|
|
?: DEFAULT_MIN_POLL_DURATION, |
|
|
|
|
maxPollDuration = this.configuration?.polls?.maxExpirationSeconds |
|
|
|
|
?: DEFAULT_MAX_POLL_DURATION, |
|
|
|
|
charactersReservedPerUrl = this.configuration?.statuses?.charactersReservedPerUrl |
|
|
|
|
?: DEFAULT_CHARACTERS_RESERVED_PER_URL, |
|
|
|
|
version = this.version, |
|
|
|
|
videoSizeLimit = this.configuration?.mediaAttachments?.videoSizeLimitBytes?.toInt() |
|
|
|
|
?: DEFAULT_VIDEO_SIZE_LIMIT, |
|
|
|
|
imageSizeLimit = this.configuration?.mediaAttachments?.imageSizeLimitBytes?.toInt() |
|
|
|
|
?: DEFAULT_IMAGE_SIZE_LIMIT, |
|
|
|
|
imageMatrixLimit = this.configuration?.mediaAttachments?.imagePixelCountLimit?.toInt() |
|
|
|
|
?: DEFAULT_IMAGE_MATRIX_LIMIT, |
|
|
|
|
maxMediaAttachments = this.configuration?.statuses?.maxMediaAttachments |
|
|
|
|
?: DEFAULT_MAX_MEDIA_ATTACHMENTS, |
|
|
|
|
maxFields = this.pleroma?.metadata?.fieldLimits?.maxFields, |
|
|
|
|
maxFieldNameLength = this.pleroma?.metadata?.fieldLimits?.nameLength, |
|
|
|
|
maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength, |
|
|
|
|
translationEnabled = this.configuration?.translation?.enabled |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
private fun InstanceV1.toEntity(instanceName: String) = |
|
|
|
|
InstanceInfoEntity( |
|
|
|
|
instance = instanceName, |
|
|
|
|
maximumTootCharacters = this.configuration?.statuses?.maxCharacters |
|
|
|
|
?: this.maxTootChars, |
|
|
|
|
maxPollOptions = this.configuration?.polls?.maxOptions |
|
|
|
|
?: this.pollConfiguration?.maxOptions, |
|
|
|
|
maxPollOptionLength = this.configuration?.polls?.maxCharactersPerOption |
|
|
|
|
?: this.pollConfiguration?.maxOptionChars, |
|
|
|
|
minPollDuration = this.configuration?.polls?.minExpiration |
|
|
|
|
?: this.pollConfiguration?.minExpiration, |
|
|
|
|
maxPollDuration = this.configuration?.polls?.maxExpiration |
|
|
|
|
?: this.pollConfiguration?.maxExpiration, |
|
|
|
|
charactersReservedPerUrl = this.configuration?.statuses?.charactersReservedPerUrl, |
|
|
|
|
version = this.version, |
|
|
|
|
videoSizeLimit = this.configuration?.mediaAttachments?.videoSizeLimit |
|
|
|
|
?: this.uploadLimit, |
|
|
|
|
imageSizeLimit = this.configuration?.mediaAttachments?.imageSizeLimit |
|
|
|
|
?: this.uploadLimit, |
|
|
|
|
imageMatrixLimit = this.configuration?.mediaAttachments?.imageMatrixLimit, |
|
|
|
|
maxMediaAttachments = this.configuration?.statuses?.maxMediaAttachments |
|
|
|
|
?: this.maxMediaAttachments, |
|
|
|
|
maxFields = this.pleroma?.metadata?.fieldLimits?.maxFields, |
|
|
|
|
maxFieldNameLength = this.pleroma?.metadata?.fieldLimits?.nameLength, |
|
|
|
|
maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength, |
|
|
|
|
translationEnabled = null, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
companion object { |
|
|
|
|
private const val TAG = "InstanceInfoRepo" |
|
|
|
|
|
|
|
|
|
|