fix: Disable "Scheduled post" support on GoToSocial accounts (#1025)

GoToSocial servers don't support scheduled posts; they return the wrong
type, and this can cause a loop of posting.

The GoToSocial bug to implement scheduled posts is
https://github.com/superseriousbusiness/gotosocial/issues/1006.

Fix this by adding a new server capability for scheduled post support,
using it for most servers, and disabling it for GoToSocial.

If scheduled post support is not available for an account:

- The "Scheduled posts" menu option is not shown.
- The scheduling button (clock) when composing a post is hidden, so the
user cannot set scheduling parameters.

Fixes #963
This commit is contained in:
Nik Clayton 2024-10-18 15:37:10 +02:00 committed by GitHub
parent 8fac5c3d4d
commit c8aa4fd374
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 101 additions and 17 deletions

View File

@ -81,9 +81,11 @@ import app.pachli.core.common.util.unsafeLazy
import app.pachli.core.data.repository.Lists import app.pachli.core.data.repository.Lists
import app.pachli.core.data.repository.ListsRepository import app.pachli.core.data.repository.ListsRepository
import app.pachli.core.data.repository.ListsRepository.Companion.compareByListTitle import app.pachli.core.data.repository.ListsRepository.Companion.compareByListTitle
import app.pachli.core.data.repository.ServerRepository
import app.pachli.core.database.model.AccountEntity import app.pachli.core.database.model.AccountEntity
import app.pachli.core.designsystem.EmbeddedFontFamily import app.pachli.core.designsystem.EmbeddedFontFamily
import app.pachli.core.designsystem.R as DR import app.pachli.core.designsystem.R as DR
import app.pachli.core.model.ServerOperation
import app.pachli.core.model.Timeline import app.pachli.core.model.Timeline
import app.pachli.core.navigation.AboutActivityIntent import app.pachli.core.navigation.AboutActivityIntent
import app.pachli.core.navigation.AccountActivityIntent import app.pachli.core.navigation.AccountActivityIntent
@ -160,12 +162,15 @@ import com.mikepenz.materialdrawer.model.interfaces.nameRes
import com.mikepenz.materialdrawer.model.interfaces.nameText import com.mikepenz.materialdrawer.model.interfaces.nameText
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.util.addItemAtPosition
import com.mikepenz.materialdrawer.util.addItems import com.mikepenz.materialdrawer.util.addItems
import com.mikepenz.materialdrawer.util.addItemsAtPosition import com.mikepenz.materialdrawer.util.addItemsAtPosition
import com.mikepenz.materialdrawer.util.getPosition
import com.mikepenz.materialdrawer.util.updateBadge import com.mikepenz.materialdrawer.util.updateBadge
import com.mikepenz.materialdrawer.widget.AccountHeaderView import com.mikepenz.materialdrawer.widget.AccountHeaderView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import io.github.z4kn4fein.semver.constraints.toConstraint
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -205,6 +210,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
@Inject @Inject
lateinit var androidNotificationsAreEnabled: AndroidNotificationsAreEnabledUseCase lateinit var androidNotificationsAreEnabled: AndroidNotificationsAreEnabledUseCase
@Inject
lateinit var serverRepository: ServerRepository
private val binding by viewBinding(ActivityMainBinding::inflate) private val binding by viewBinding(ActivityMainBinding::inflate)
override val actionButton by unsafeLazy { binding.composeButton } override val actionButton by unsafeLazy { binding.composeButton }
@ -402,6 +410,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
} }
} }
lifecycleScope.launch {
serverRepository.flow.collect {
refreshMainDrawerItems(intent.pachliAccountId, addSearchButton = hideTopToolbar)
}
}
selectedEmojiPack = sharedPreferencesRepository.getString(EMOJI_PREFERENCE, "") selectedEmojiPack = sharedPreferencesRepository.getString(EMOJI_PREFERENCE, "")
onBackPressedDispatcher.addCallback(this, onBackPressedCallback) onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
@ -749,6 +763,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}, },
DividerDrawerItem(), DividerDrawerItem(),
primaryDrawerItem { primaryDrawerItem {
identifier = DRAWER_ITEM_DRAFTS
nameRes = R.string.action_access_drafts nameRes = R.string.action_access_drafts
iconRes = R.drawable.ic_notebook iconRes = R.drawable.ic_notebook
onClick = { onClick = {
@ -757,15 +772,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
) )
} }
}, },
primaryDrawerItem {
nameRes = R.string.action_access_scheduled_posts
iconRes = R.drawable.ic_access_time
onClick = {
startActivityWithDefaultTransition(
ScheduledStatusActivityIntent(context, pachliAccountId),
)
}
},
primaryDrawerItem { primaryDrawerItem {
identifier = DRAWER_ITEM_ANNOUNCEMENTS identifier = DRAWER_ITEM_ANNOUNCEMENTS
nameRes = R.string.title_announcements nameRes = R.string.title_announcements
@ -840,6 +846,23 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
} }
} }
// If the server supports scheduled posts then add a "Scheduled posts" item
// after the "Drafts" item.
if (serverRepository.flow.replayCache.lastOrNull()?.get()?.can(ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED, ">= 1.0.0".toConstraint()) == true) {
binding.mainDrawer.addItemAtPosition(
binding.mainDrawer.getPosition(DRAWER_ITEM_DRAFTS) + 1,
primaryDrawerItem {
nameRes = R.string.action_access_scheduled_posts
iconRes = R.drawable.ic_access_time
onClick = {
startActivityWithDefaultTransition(
ScheduledStatusActivityIntent(binding.mainDrawer.context, pachliAccountId),
)
}
},
)
}
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
// Add a "Developer tools" entry. Code that makes it easier to // Add a "Developer tools" entry. Code that makes it easier to
// set the app state at runtime belongs here, it will never // set the app state at runtime belongs here, it will never
@ -1260,6 +1283,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
/** Drawer identifier for the "Lists" section header. */ /** Drawer identifier for the "Lists" section header. */
private const val DRAWER_ITEM_LISTS: Long = 15 private const val DRAWER_ITEM_LISTS: Long = 15
/** Drawer identifier for the "Drafts" item. */
private const val DRAWER_ITEM_DRAFTS = 16L
} }
} }

View File

@ -505,6 +505,15 @@ class ComposeActivity :
} }
} }
// Hide the "Schedule" button if the server can't schedule. Simply
// disabling it could be confusing to users wondering why they can't
// use it.
lifecycleScope.launch {
viewModel.serverCanSchedule.collect {
binding.composeScheduleButton.visible(it)
}
}
lifecycleScope.launch { lifecycleScope.launch {
viewModel.media.combine(viewModel.poll) { media, poll -> viewModel.media.combine(viewModel.poll) { media, poll ->
val active = poll == null && val active = poll == null &&

View File

@ -35,6 +35,8 @@ import app.pachli.core.common.string.mastodonLength
import app.pachli.core.common.string.randomAlphanumericString import app.pachli.core.common.string.randomAlphanumericString
import app.pachli.core.data.repository.AccountManager import app.pachli.core.data.repository.AccountManager
import app.pachli.core.data.repository.InstanceInfoRepository import app.pachli.core.data.repository.InstanceInfoRepository
import app.pachli.core.data.repository.ServerRepository
import app.pachli.core.model.ServerOperation
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions.ComposeKind import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions.ComposeKind
import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Attachment
@ -49,17 +51,22 @@ import at.connyduck.calladapter.networkresult.fold
import com.github.michaelbull.result.Err import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result import com.github.michaelbull.result.Result
import com.github.michaelbull.result.get
import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.mapBoth import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError import com.github.michaelbull.result.mapError
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.z4kn4fein.semver.constraints.toConstraint
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -73,6 +80,7 @@ class ComposeViewModel @Inject constructor(
private val serviceClient: ServiceClient, private val serviceClient: ServiceClient,
private val draftHelper: DraftHelper, private val draftHelper: DraftHelper,
instanceInfoRepo: InstanceInfoRepository, instanceInfoRepo: InstanceInfoRepository,
private val serverRepository: ServerRepository,
) : ViewModel() { ) : ViewModel() {
/** The current content */ /** The current content */
@ -140,6 +148,11 @@ class ComposeViewModel @Inject constructor(
private val _statusLength = MutableStateFlow(0) private val _statusLength = MutableStateFlow(0)
val statusLength = _statusLength.asStateFlow() val statusLength = _statusLength.asStateFlow()
/** Flow of whether or not the server can schedule posts. */
val serverCanSchedule = serverRepository.flow.map {
it.get()?.can(ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED, ">= 1.0.0".toConstraint()) == true
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
private lateinit var composeKind: ComposeKind private lateinit var composeKind: ComposeKind
// Used in ComposeActivity to pass state to result function when cropImage contract inflight // Used in ComposeActivity to pass state to result function when cropImage contract inflight

View File

@ -77,4 +77,13 @@ enum class ServerOperation(id: String, versions: List<Version>) {
ORG_JOINMASTODON_SEARCH_QUERY_IN_LIBRARY("org.joinmastodon.search.query:in:library", listOf(Version(major = 1))), ORG_JOINMASTODON_SEARCH_QUERY_IN_LIBRARY("org.joinmastodon.search.query:in:library", listOf(Version(major = 1))),
ORG_JOINMASTODON_SEARCH_QUERY_IN_PUBLIC("org.joinmastodon.search.query:in:public", listOf(Version(major = 1))), ORG_JOINMASTODON_SEARCH_QUERY_IN_PUBLIC("org.joinmastodon.search.query:in:public", listOf(Version(major = 1))),
ORG_JOINMASTODON_SEARCH_QUERY_BY_DATE("org.joinmastodon.search.query:in:public", listOf(Version(major = 1))), ORG_JOINMASTODON_SEARCH_QUERY_BY_DATE("org.joinmastodon.search.query:in:public", listOf(Version(major = 1))),
/** Post a status with a `scheduled_at` property, and edit scheduled statuses. */
ORG_JOINMASTODON_STATUSES_SCHEDULED(
"org.joinmastodon.statuses.scheduled",
listOf(
// Initial introduction in Mastodon 2.7.0.
Version(major = 1),
),
),
} }

View File

@ -52,6 +52,7 @@ import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IN_PU
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_REPLY import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_REPLY
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_LANGUAGE import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_LANGUAGE
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_TRANSLATE import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_TRANSLATE
import app.pachli.core.network.Server.Error.UnparseableVersion import app.pachli.core.network.Server.Error.UnparseableVersion
import app.pachli.core.network.model.InstanceV1 import app.pachli.core.network.model.InstanceV1
@ -241,6 +242,11 @@ data class Server(
when (kind) { when (kind) {
// Glitch has the same version number as upstream Mastodon // Glitch has the same version number as upstream Mastodon
GLITCH, MASTODON -> { GLITCH, MASTODON -> {
// Scheduled statuses
when {
v >= "2.7.0".toVersion() -> c[ORG_JOINMASTODON_STATUSES_SCHEDULED] = "1.0.0".toVersion()
}
// Client filtering // Client filtering
when { when {
v >= "3.1.0".toVersion() -> c[ORG_JOINMASTODON_FILTERS_CLIENT] = "1.1.0".toVersion() v >= "3.1.0".toVersion() -> c[ORG_JOINMASTODON_FILTERS_CLIENT] = "1.1.0".toVersion()
@ -281,6 +287,8 @@ data class Server(
} }
GOTOSOCIAL -> { GOTOSOCIAL -> {
// Can't do scheduled posts, https://github.com/superseriousbusiness/gotosocial/issues/1006
// Filters // Filters
when { when {
// Implemented in https://github.com/superseriousbusiness/gotosocial/pull/2936 // Implemented in https://github.com/superseriousbusiness/gotosocial/pull/2936
@ -305,12 +313,18 @@ data class Server(
FIREFISH -> { } FIREFISH -> { }
// Sharkey can't filter, https://activitypub.software/TransFem-org/Sharkey/-/issues/492 // Sharkey can't filter, https://activitypub.software/TransFem-org/Sharkey/-/issues/492
SHARKEY -> { } SHARKEY -> {
// Assume scheduled support (may be wrong).
c[ORG_JOINMASTODON_STATUSES_SCHEDULED] = "1.0.0".toVersion()
}
FRIENDICA -> { FRIENDICA -> {
// Assume filter support (may be wrong) // Assume filter support (may be wrong).
c[ORG_JOINMASTODON_FILTERS_SERVER] = "1.0.0".toVersion() c[ORG_JOINMASTODON_FILTERS_SERVER] = "1.0.0".toVersion()
// Assume scheduled support (may be wrong).
c[ORG_JOINMASTODON_STATUSES_SCHEDULED] = "1.0.0".toVersion()
// Search // Search
when { when {
// Friendica has a number of search operators that are not in Mastodon. // Friendica has a number of search operators that are not in Mastodon.
@ -323,10 +337,16 @@ data class Server(
} }
} }
// Everything else. Assume server side filtering and no translation. This may be an // Everything else. Assume:
// incorrect assumption. //
// - server side filtering
// - scheduled status support
// - no translation
//
// This may be an incorrect assumption.
AKKOMA, FEDIBIRD, HOMETOWN, ICESHRIMP, PIXELFED, PLEROMA, UNKNOWN -> { AKKOMA, FEDIBIRD, HOMETOWN, ICESHRIMP, PIXELFED, PLEROMA, UNKNOWN -> {
c[ORG_JOINMASTODON_FILTERS_SERVER] = "1.0.0".toVersion() c[ORG_JOINMASTODON_FILTERS_SERVER] = "1.0.0".toVersion()
c[ORG_JOINMASTODON_STATUSES_SCHEDULED] = "1.0.0".toVersion()
} }
} }
return c return c

View File

@ -41,6 +41,7 @@ import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IN_LI
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_REPLY import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_REPLY
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_LANGUAGE import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_LANGUAGE
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_TRANSLATE import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_TRANSLATE
import app.pachli.core.network.model.Account import app.pachli.core.network.model.Account
import app.pachli.core.network.model.Configuration import app.pachli.core.network.model.Configuration
@ -148,6 +149,7 @@ class ServerTest(
ORG_JOINMASTODON_FILTERS_CLIENT to "1.1.0".toVersion(), ORG_JOINMASTODON_FILTERS_CLIENT to "1.1.0".toVersion(),
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(), ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_SEARCH_QUERY_FROM to "1.0.0".toVersion(), ORG_JOINMASTODON_SEARCH_QUERY_FROM to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
), ),
), ),
), ),
@ -170,6 +172,7 @@ class ServerTest(
ORG_JOINMASTODON_FILTERS_CLIENT to "1.1.0".toVersion(), ORG_JOINMASTODON_FILTERS_CLIENT to "1.1.0".toVersion(),
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(), ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_SEARCH_QUERY_FROM to "1.0.0".toVersion(), ORG_JOINMASTODON_SEARCH_QUERY_FROM to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_TRANSLATE to "1.0.0".toVersion(), ORG_JOINMASTODON_STATUSES_TRANSLATE to "1.0.0".toVersion(),
), ),
), ),
@ -205,6 +208,7 @@ class ServerTest(
ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE to "1.0.0".toVersion(), ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE to "1.0.0".toVersion(),
ORG_JOINMASTODON_SEARCH_QUERY_IN_LIBRARY to "1.0.0".toVersion(), ORG_JOINMASTODON_SEARCH_QUERY_IN_LIBRARY to "1.0.0".toVersion(),
ORG_JOINMASTODON_SEARCH_QUERY_BY_DATE to "1.0.0".toVersion(), ORG_JOINMASTODON_SEARCH_QUERY_BY_DATE to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_TRANSLATE to "1.1.0".toVersion(), ORG_JOINMASTODON_STATUSES_TRANSLATE to "1.1.0".toVersion(),
), ),
), ),
@ -212,7 +216,7 @@ class ServerTest(
), ),
arrayOf( arrayOf(
Triple( Triple(
"GoToSocial has no translation or filtering", "GoToSocial has no translation, filtering, or scheduling",
NodeInfo.Software("gotosocial", "0.13.1 git-ccecf5a"), NodeInfo.Software("gotosocial", "0.13.1 git-ccecf5a"),
defaultInstance, defaultInstance,
), ),
@ -260,7 +264,7 @@ class ServerTest(
), ),
arrayOf( arrayOf(
Triple( Triple(
"Pleroma can filter", "Pleroma can filter, schedule",
NodeInfo.Software("pleroma", "2.6.50-875-g2eb5c453.service-origin+soapbox"), NodeInfo.Software("pleroma", "2.6.50-875-g2eb5c453.service-origin+soapbox"),
defaultInstance, defaultInstance,
), ),
@ -270,13 +274,14 @@ class ServerTest(
version = "2.6.50-875-g2eb5c453.service-origin+soapbox".toVersion(), version = "2.6.50-875-g2eb5c453.service-origin+soapbox".toVersion(),
capabilities = mapOf( capabilities = mapOf(
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(), ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
), ),
), ),
), ),
), ),
arrayOf( arrayOf(
Triple( Triple(
"Akkoma can filter", "Akkoma can filter, schedule",
NodeInfo.Software("akkoma", "3.9.3-0-gd83f5f66f-blob"), NodeInfo.Software("akkoma", "3.9.3-0-gd83f5f66f-blob"),
defaultInstance, defaultInstance,
), ),
@ -286,6 +291,7 @@ class ServerTest(
version = "3.9.3-0-gd83f5f66f-blob".toVersion(), version = "3.9.3-0-gd83f5f66f-blob".toVersion(),
capabilities = mapOf( capabilities = mapOf(
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(), ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
), ),
), ),
), ),
@ -306,7 +312,7 @@ class ServerTest(
), ),
arrayOf( arrayOf(
Triple( Triple(
"Friendica can filter", "Friendica can filter, schedule",
NodeInfo.Software("friendica", "2023.05-1542"), NodeInfo.Software("friendica", "2023.05-1542"),
defaultInstance, defaultInstance,
), ),
@ -316,6 +322,7 @@ class ServerTest(
version = "2023.5.0".toVersion(), version = "2023.5.0".toVersion(),
capabilities = mapOf( capabilities = mapOf(
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(), ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
), ),
), ),
), ),