From 5cfe6d055b974d9a4a54095175b04612842d788b Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Tue, 23 Jan 2024 20:27:25 +0100 Subject: [PATCH] fix: Improve parsing of Friendica (and other server) version formats (#376) Previous code could return an error on Friendica version strings like `2024.03-dev-1547`. Fix this: - Extend the list of explicitly supported servers to include Fedibird, Friendica, Glitch, Hometown, Iceshrimp, Pixelfed, and Sharkey. - Add version parsing routines for these servers. - Test the version parsing routines fetching every server and version seen by Fediverse Observer (~ 2,000 servers) and ensuring that the server and version information can be parsed. Improve the error message: - Show the hostname with a `ServerRepository` error Clean up the code: - Remove the custom `resultOf` and `mapResult` functions, they have equivalents in newer versions of the library (like `runSuspendCatching`) Fixes #372 --- .editorconfig | 5 + app/lint-baseline.xml | 2 +- .../java/app/pachli/fragment/SFragment.kt | 1 + .../app/pachli/network/ServerRepository.kt | 10 +- app/src/main/res/values/strings.xml | 2 +- .../app/pachli/core/common/PachliError.kt | 49 - .../kotlin/app/pachli/core/network/Server.kt | 171 +- .../app/pachli/core/network/ServerTest.kt | 77 + .../src/test/resources/server-versions.json5 | 1876 +++++++++++++++++ settings.gradle.kts | 1 + tools/mkserverversions/README.md | 24 + tools/mkserverversions/build.gradle.kts | 49 + tools/mkserverversions/settings.gradle.kts | 30 + .../src/main/graphql/ServerVersions.graphql | 6 + .../src/main/graphql/schema.graphqls | 353 ++++ .../app/pachli/mkserverversions/Main.kt | 111 + 16 files changed, 2680 insertions(+), 87 deletions(-) create mode 100644 core/network/src/test/resources/server-versions.json5 create mode 100644 tools/mkserverversions/README.md create mode 100644 tools/mkserverversions/build.gradle.kts create mode 100644 tools/mkserverversions/settings.gradle.kts create mode 100644 tools/mkserverversions/src/main/graphql/ServerVersions.graphql create mode 100644 tools/mkserverversions/src/main/graphql/schema.graphqls create mode 100644 tools/mkserverversions/src/main/kotlin/app/pachli/mkserverversions/Main.kt diff --git a/.editorconfig b/.editorconfig index 9f545e498..2ede3f2b9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -25,3 +25,8 @@ max_line_length = off [*.{yml,yaml}] indent_size = 2 + +# Disable ktlint on generated source code, see +# https://github.com/JLLeitschuh/ktlint-gradle/issues/746 +[**/build/generated/source/**] +ktlint = disabled diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index f242c57c2..8c79fbe87 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -3534,7 +3534,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> diff --git a/app/src/main/java/app/pachli/fragment/SFragment.kt b/app/src/main/java/app/pachli/fragment/SFragment.kt index 2c4a585d9..214891543 100644 --- a/app/src/main/java/app/pachli/fragment/SFragment.kt +++ b/app/src/main/java/app/pachli/fragment/SFragment.kt @@ -127,6 +127,7 @@ abstract class SFragment : Fragment() { result.onFailure { val msg = getString( R.string.server_repository_error, + accountManager.activeAccount!!.domain, it.msg(requireContext()), ) Timber.e(msg) diff --git a/app/src/main/java/app/pachli/network/ServerRepository.kt b/app/src/main/java/app/pachli/network/ServerRepository.kt index a26148338..523fcb257 100644 --- a/app/src/main/java/app/pachli/network/ServerRepository.kt +++ b/app/src/main/java/app/pachli/network/ServerRepository.kt @@ -27,7 +27,7 @@ import app.pachli.core.network.model.nodeinfo.NodeInfo import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.NodeInfoApi import app.pachli.network.ServerRepository.Error.Capabilities -import app.pachli.network.ServerRepository.Error.GetInstanceInfo +import app.pachli.network.ServerRepository.Error.GetInstanceInfoV1 import app.pachli.network.ServerRepository.Error.GetNodeInfo import app.pachli.network.ServerRepository.Error.GetWellKnownNodeInfo import app.pachli.network.ServerRepository.Error.UnsupportedSchema @@ -75,8 +75,8 @@ class ServerRepository @Inject constructor( } /** - * @return the current server or a [Server.Error] subclass error if the - * server can not be determined. + * @return the server info or a [Server.Error] if the server info can not + * be determined. */ private suspend fun getServer(): Result = binding { // Fetch the /.well-known/nodeinfo document @@ -107,7 +107,7 @@ class ServerRepository @Inject constructor( { mastodonApi.getInstanceV1().fold( { Server.from(nodeInfo.software, it).mapError(::Capabilities) }, - { Err(GetInstanceInfo(it)) }, + { Err(GetInstanceInfoV1(it)) }, ) }, ).bind() @@ -139,7 +139,7 @@ class ServerRepository @Inject constructor( source = error, ) - data class GetInstanceInfo(val throwable: Throwable) : Error( + data class GetInstanceInfoV1(val throwable: Throwable) : Error( R.string.server_repository_error_get_instance_info, throwable.localizedMessage, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3df9580bb..73a615640 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -766,7 +766,7 @@ Your description here:\n\n ----\n - Could not fetch server info: %1$s + Could not fetch server info for %1$s: %2$s fetching /.well-known/nodeinfo failed: %1$s /.well-known/nodeinfo did not contain understandable schemas fetching nodeinfo %1$s failed: %2$s diff --git a/core/common/src/main/kotlin/app/pachli/core/common/PachliError.kt b/core/common/src/main/kotlin/app/pachli/core/common/PachliError.kt index 616288e4e..ae1ecb2f5 100644 --- a/core/common/src/main/kotlin/app/pachli/core/common/PachliError.kt +++ b/core/common/src/main/kotlin/app/pachli/core/common/PachliError.kt @@ -19,12 +19,6 @@ package app.pachli.core.common import android.content.Context import androidx.annotation.StringRes -import com.github.michaelbull.result.Err -import com.github.michaelbull.result.Ok -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.getError -import com.github.michaelbull.result.getOr -import kotlin.coroutines.cancellation.CancellationException /** * Base class for errors throughout the app. @@ -83,46 +77,3 @@ open class PachliError( return context.getString(resourceId, *args.toTypedArray()) } } - -// See https://www.jacobras.nl/2022/04/resilient-use-cases-with-kotlin-result-coroutines-and-annotations/ - -/** - * Like [runCatching], but with proper coroutines cancellation handling. Also only catches [Exception] instead of [Throwable]. - * - * Cancellation exceptions need to be rethrown. See https://github.com/Kotlin/kotlinx.coroutines/issues/1814. - */ -inline fun resultOf(block: () -> R): Result { - return try { - Ok(block()) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Err(e) - } -} - -/** - * Like [runCatching], but with proper coroutines cancellation handling. Also only catches [Exception] instead of [Throwable]. - * - * Cancellation exceptions need to be rethrown. See https://github.com/Kotlin/kotlinx.coroutines/issues/1814. - */ -inline fun T.resultOf(block: T.() -> R): Result { - return try { - Ok(block()) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Err(e) - } -} - -/** - * Like [mapCatching], but uses [resultOf] instead of [runCatching]. - */ -inline fun Result.mapResult(transform: (value: T) -> R): Result { - val successResult = getOr { null } // getOrNull() - return when { - successResult != null -> resultOf { transform(successResult) } - else -> Err(getError() ?: error("Unreachable state")) - } -} diff --git a/core/network/src/main/kotlin/app/pachli/core/network/Server.kt b/core/network/src/main/kotlin/app/pachli/core/network/Server.kt index e72af1267..78ed4525f 100644 --- a/core/network/src/main/kotlin/app/pachli/core/network/Server.kt +++ b/core/network/src/main/kotlin/app/pachli/core/network/Server.kt @@ -18,13 +18,21 @@ package app.pachli.core.network import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.Companion.PRIVATE import app.pachli.core.common.PachliError -import app.pachli.core.common.resultOf import app.pachli.core.network.Server.Error.UnparseableVersion import app.pachli.core.network.ServerKind.AKKOMA +import app.pachli.core.network.ServerKind.FEDIBIRD +import app.pachli.core.network.ServerKind.FRIENDICA +import app.pachli.core.network.ServerKind.GLITCH import app.pachli.core.network.ServerKind.GOTOSOCIAL +import app.pachli.core.network.ServerKind.HOMETOWN +import app.pachli.core.network.ServerKind.ICESHRIMP import app.pachli.core.network.ServerKind.MASTODON +import app.pachli.core.network.ServerKind.PIXELFED import app.pachli.core.network.ServerKind.PLEROMA +import app.pachli.core.network.ServerKind.SHARKEY import app.pachli.core.network.ServerKind.UNKNOWN import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_CLIENT import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_SERVER @@ -32,14 +40,19 @@ import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_STATUSES_TRANSLA import app.pachli.core.network.model.InstanceV1 import app.pachli.core.network.model.InstanceV2 import app.pachli.core.network.model.nodeinfo.NodeInfo +import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result +import com.github.michaelbull.result.andThen import com.github.michaelbull.result.binding -import com.github.michaelbull.result.getOrElse +import com.github.michaelbull.result.coroutines.runSuspendCatching import com.github.michaelbull.result.mapError +import com.github.michaelbull.result.recover +import com.github.michaelbull.result.toResultOr import io.github.z4kn4fein.semver.Version import io.github.z4kn4fein.semver.constraints.Constraint import io.github.z4kn4fein.semver.satisfies import io.github.z4kn4fein.semver.toVersion +import java.text.ParseException import kotlin.collections.set /** @@ -97,7 +110,7 @@ data class Server( * Constructs a server from its [NodeInfo] and [InstanceV2] details. */ fun from(software: NodeInfo.Software, instanceV2: InstanceV2): Result = binding { - val serverKind = ServerKind.from(software.name) + val serverKind = ServerKind.from(software) val version = parseVersionString(serverKind, software.version).bind() val capabilities = capabilitiesFromServerVersion(serverKind, version) @@ -120,7 +133,7 @@ data class Server( * Constructs a server from its [NodeInfo] and [InstanceV1] details. */ fun from(software: NodeInfo.Software, instanceV1: InstanceV1): Result = binding { - val serverKind = ServerKind.from(software.name) + val serverKind = ServerKind.from(software) val version = parseVersionString(serverKind, software.version).bind() val capabilities = capabilitiesFromServerVersion(serverKind, version) @@ -130,37 +143,108 @@ data class Server( /** * Parse a [version] string from the given [serverKind] in to a [Version]. */ - private fun parseVersionString(serverKind: ServerKind, version: String): Result = binding { - // Real world examples of version strings from nodeinfo - // pleroma - 2.6.50-875-g2eb5c453.service-origin+soapbox - // akkoma - 3.9.3-0-gd83f5f66f-blob - // firefish - 1.1.0-dev29-hf1 - // hometown - 4.0.10+hometown-1.1.1 - // cherrypick - 4.6.0+cs-8f0ba0f - // gotosocial - 0.13.1-SNAPSHOT git-dfc7656 + @VisibleForTesting(otherwise = PRIVATE) + fun parseVersionString(serverKind: ServerKind, version: String): Result { + val result = runSuspendCatching { + Version.parse(version, strict = false) + }.mapError { UnparseableVersion(version, it) } - val semver = when (serverKind) { - // These servers have semver compatible versions - AKKOMA, MASTODON, PLEROMA, UNKNOWN -> { - resultOf { Version.parse(version, strict = false) } - .mapError { UnparseableVersion(version, it) }.bind() + if (result is Ok) return result + + return when (serverKind) { + // These servers should have semver compatible versions, but perhaps + // the server operator has changed them. Try looking for a matching + // .. somewhere in the version string and hope + // it's correct + AKKOMA, FEDIBIRD, GLITCH, HOMETOWN, MASTODON, PIXELFED, UNKNOWN -> { + val rx = """(?\d+)\.(?\d+).(?\d+)""".toRegex() + rx.find(version) + .toResultOr { UnparseableVersion(version, ParseException("unexpected null", 0)) } + .andThen { + val adjusted = "${it.groups["major"]?.value}.${it.groups["minor"]?.value}.${it.groups["patch"]?.value}" + runSuspendCatching { Version.parse(adjusted, strict = false) } + .mapError { UnparseableVersion(version, it) } + } } - // GoToSocial does not report a semver compatible version, expect something - // where the possible version number is space-separated, like "0.13.1 git-ccecf5a" + + // Friendica does not report a semver compatible version, expect something + // where the version looks like "yyyy.mm", with an optional suffix that + // starts with a "-". The date-like parts of the string may have leading + // zeros. + // + // Try to extract the "yyyy.mm", without any leading zeros, append ".0". + // https://github.com/friendica/friendica/issues/11264 + FRIENDICA -> { + val rx = """^0*(?\d+)\.0*(?\d+)""".toRegex() + rx.find(version) + .toResultOr { UnparseableVersion(version, ParseException("unexpected null", 0)) } + .andThen { + val adjusted = "${it.groups["major"]?.value}.${it.groups["minor"]?.value}.0" + runSuspendCatching { Version.parse(adjusted, strict = false) } + .mapError { UnparseableVersion(version, it) } + } + } + + // GoToSocial does not always report a semver compatible version, and is all + // over the place, including: + // + // - "" (empty) + // - "git-8ab30d0" + // - "kalaclista git-212fecf" + // - "f4fcffc8b56ef73c184ae17892b69181961c15c7" + // + // as well as instances where the version number is semver compatible, but is + // separated by whitespace or a "_". + // // https://github.com/superseriousbusiness/gotosocial/issues/1953 + // + // Since GoToSocial has comparatively few features at the moment just fall + // back to "0.0.0" if there are problems. GOTOSOCIAL -> { - // Try and parse as semver, just in case - resultOf { Version.parse(version, strict = false) } - .getOrElse { - // Didn't parse, use the first component, fall back to 0.0.0 - val components = version.split(" ") - resultOf { Version.parse(components[0], strict = false) } - .getOrElse { "0.0.0".toVersion() } + // Failed, split on spaces and parse the first component + val components = version.split(" ", "_") + runSuspendCatching { Version.parse(components[0], strict = false) } + .recover { "0.0.0".toVersion() } + } + + // IceShrimp uses "yyyy.mm.dd" with leading zeros in the month and day + // components, similar to Friendica. + // https://iceshrimp.dev/iceshrimp/iceshrimp/issues/502 and + // https://iceshrimp.dev/iceshrimp/iceshrimp-rewrite/issues/1 + ICESHRIMP -> { + val rx = """^0*(?\d+)\.0*(?\d+)\.0*(?\d+)""".toRegex() + rx.find(version).toResultOr { UnparseableVersion(version, ParseException("unexpected null", 0)) } + .andThen { + val adjusted = "${it.groups["major"]?.value}.${it.groups["minor"]?.value ?: 0}.${it.groups["patch"]?.value ?: 0}" + runSuspendCatching { Version.parse(adjusted, strict = false) } + .mapError { UnparseableVersion(adjusted, it) } + } + } + + // Seen "Pleroma 0.9.0 d93789dfde3c44c76a56732088a897ddddfe9716" in + // the wild + PLEROMA -> { + val rx = """Pleroma (?\d+)\.(?\d+)\.(?\d+)""".toRegex() + rx.find(version).toResultOr { UnparseableVersion(version, ParseException("unexpected null", 0)) } + .andThen { + val adjusted = "${it.groups["major"]?.value}.${it.groups["minor"]?.value}.${it.groups["patch"]?.value}" + runSuspendCatching { Version.parse(adjusted, strict = false) } + .mapError { UnparseableVersion(adjusted, it) } + } + } + + // Uses format "yyyy.mm.dd" with an optional ".beta..." suffix. + // https://git.joinsharkey.org/Sharkey/Sharkey/issues/371 + SHARKEY -> { + val rx = """^(?\d+)\.(?\d+)\.(?\d+)""".toRegex() + rx.find(version).toResultOr { UnparseableVersion(version, ParseException("unexpected null", 0)) } + .andThen { + val adjusted = "${it.groups["major"]?.value}.${it.groups["minor"]?.value}.${it.groups["patch"]?.value}" + runSuspendCatching { Version.parse(adjusted, strict = false) } + .mapError { UnparseableVersion(adjusted, it) } } } } - - semver } /** @@ -188,7 +272,7 @@ data class Server( // Everything else. Assume server side filtering and no translation. This may be an // incorrect assumption. - AKKOMA, PLEROMA, UNKNOWN -> { + AKKOMA, FEDIBIRD, FRIENDICA, GLITCH, HOMETOWN, ICESHRIMP, PIXELFED, PLEROMA, SHARKEY, UNKNOWN -> { c[ORG_JOINMASTODON_FILTERS_SERVER] = "1.0.0".toVersion() } } @@ -210,20 +294,45 @@ data class Server( } } +/** + * Servers that are known to implement the Mastodon client API + */ enum class ServerKind { AKKOMA, + FEDIBIRD, + FRIENDICA, + GLITCH, GOTOSOCIAL, + HOMETOWN, + ICESHRIMP, MASTODON, PLEROMA, + PIXELFED, + SHARKEY, + + /** + * Catch-all for servers we don't recognise but that responded to either + * /api/v1/instance or /api/v2/instance + */ UNKNOWN, ; companion object { - fun from(s: String) = when (s.lowercase()) { + fun from(s: NodeInfo.Software) = when (s.name.lowercase()) { "akkoma" -> AKKOMA + "fedibird" -> FEDIBIRD + "friendica" -> FRIENDICA "gotosocial" -> GOTOSOCIAL - "mastodon" -> MASTODON + "hometown" -> HOMETOWN + "iceshrimp" -> ICESHRIMP + "mastodon" -> { + // Glitch doesn't report a different software name it stuffs it + // in the version (https://github.com/glitch-soc/mastodon/issues/2582). + if (s.version.contains("+glitch")) GLITCH else MASTODON + } + "pixelfed" -> PIXELFED "pleroma" -> PLEROMA + "sharkey" -> SHARKEY else -> UNKNOWN } } diff --git a/core/network/src/test/kotlin/app/pachli/core/network/ServerTest.kt b/core/network/src/test/kotlin/app/pachli/core/network/ServerTest.kt index 9e55ca2be..09a0874e7 100644 --- a/core/network/src/test/kotlin/app/pachli/core/network/ServerTest.kt +++ b/core/network/src/test/kotlin/app/pachli/core/network/ServerTest.kt @@ -18,6 +18,7 @@ package app.pachli.core.network import app.pachli.core.network.ServerKind.AKKOMA +import app.pachli.core.network.ServerKind.FRIENDICA import app.pachli.core.network.ServerKind.GOTOSOCIAL import app.pachli.core.network.ServerKind.MASTODON import app.pachli.core.network.ServerKind.PLEROMA @@ -43,7 +44,10 @@ import app.pachli.core.network.model.nodeinfo.NodeInfo import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result import com.google.common.truth.Truth.assertWithMessage +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import io.github.z4kn4fein.semver.toVersion +import java.io.BufferedReader import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -238,6 +242,22 @@ class ServerTest( ), ), ), + arrayOf( + Triple( + "Friendica can filter", + NodeInfo.Software("friendica", "2023.05-1542"), + defaultInstance, + ), + Ok( + Server( + kind = FRIENDICA, + version = "2023.5.0".toVersion(), + capabilities = mapOf( + ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(), + ), + ), + ), + ), ) } } @@ -252,3 +272,60 @@ class ServerTest( .isEqualTo(want) } } + +class ServerVersionTest() { + private val gson = Gson() + + private fun loadJsonAsString(fileName: String): String { + return javaClass.getResourceAsStream("/$fileName")!! + .bufferedReader().use(BufferedReader::readText) + } + + /** + * Test that parsing all possible versions succeeds. + * + * To do this tools/mkserverversions generates a JSON file that + * contains a map of server names to a list of server version strings + * that have been seen by Fediverse Observer. These version strings + * are then parsed and are all expected to parse correctly. + */ + @Test + fun parseVersionString() { + val mapType: TypeToken>> = + object : TypeToken>>() {} + + val serverVersions = gson.fromJson( + loadJsonAsString("server-versions.json5"), + mapType, + ) + + // Sanity check that data was loaded correctly. Expect at least 5 distinct keys + assertWithMessage("number of server types in server-versions.json5 is too low") + .that(serverVersions.size) + .isGreaterThan(5) + + for (entry in serverVersions.entries) { + for (version in entry.value) { + val serverKind = ServerKind.from( + NodeInfo.Software( + name = entry.key, + version = version, + ), + ) + + // Skip unknown/unsupported servers, as their version strings + // could be anything. + if (serverKind == UNKNOWN) continue + + val result = Server.parseVersionString( + serverKind, + version, + ) + + assertWithMessage("${entry.key} : $version") + .that(result) + .isInstanceOf(Ok::class.java) + } + } + } +} diff --git a/core/network/src/test/resources/server-versions.json5 b/core/network/src/test/resources/server-versions.json5 new file mode 100644 index 000000000..426c1d34b --- /dev/null +++ b/core/network/src/test/resources/server-versions.json5 @@ -0,0 +1,1876 @@ +// GENERATED BY "runtools mkserverversions", DO NOT HAND EDIT +{ + "98gravity electro": [ + "0.0.1" + ], + "acropolis": [ + "0.7.99.0" + ], + "activipypub": [ + "1.0.0" + ], + "activitypub-core": [ + "0.1.27", + "0.2.129", + "0.4.35" + ], + "activitypub-firebase": [ + "1.0.0" + ], + "akkoma": [ + "3.10.2", + "3.10.2-0-gba1ed37", + "3.10.2-158-g59af68c6-develop", + "3.10.2-2212-g0d59cb6c-froth-akkoma", + "3.10.2-uw1", + "3.10.3", + "3.10.3+bnakkoma", + "3.10.3-0-g368b22f-develop", + "3.10.3-0-g43c5fd5", + "3.10.3-0-g6fb91d7", + "3.10.3-0-g98f0820-develop", + "3.10.3-0-gc887dd4", + "3.10.3-1-g43c5fd5d", + "3.10.3-1546-ge5b36d60-akkoma", + "3.10.3-162-ge7788f3c-develop", + "3.10.3-194-g43c5fd5d", + "3.10.3-197-g98f0820c-develop", + "3.10.3-197-g98f0820c-sn0w+PE", + "3.10.3-200-g5c164028-develop", + "3.10.3-201-gc8e08e9c-develop", + "3.10.3-202-g1b838627-1-CI-COMMIT-TAG---", + "3.10.3-203-gd1af78ab-develop", + "3.10.3-212-g6cc523bd-develop", + "3.10.3-221-g6fb91d79-develop", + "3.10.3-224-g14ff522b-backoff-http", + "3.10.4", + "3.10.4+PE", + "3.10.4-0-g0af8e93", + "3.10.4-0-g0c6b12086-birds", + "3.10.4-0-gebfb617", + "3.10.4-1-ga4c15863-no-fetch-with-local-limit-3-10-4", + "3.10.4-1-gebfb617b", + "3.10.4-2-gde9afa1f-tailswish", + "3.10.4-3-g8660dcae", + "3.10.4-4-g74f94627-suyaplace", + "3.10.4-6-g534841dd-heads-v3-10-4", + "3.10.4-606-g1063b7da-develop", + "3.10.4-607-g88021a96-noeroma", + "3.10.5+spirit", + "3.3.1", + "3.3.1-0-gaf90a4e51", + "3.3.1-0-gc0eecb55b-develop", + "3.3.1-0-gc6043c9ee-main", + "3.3.1-45-g16a31872-develop", + "3.4.0", + "3.4.0-0-gf91b89673", + "3.4.0-111-g6453297e-develop", + "3.4.0-117-gde1bbc02-develop", + "3.5.0-0-g20a652841-hostdon-akkoma-latest", + "3.5.0-0-g63f2d1cbe", + "3.5.0-0-gd9508474b", + "3.5.0-12-g63f2d1cb", + "3.5.0-14-gd9508474", + "3.5.0-26-g54fdf3a5", + "3.5.0-develop", + "3.6.0", + "3.6.0-0-g36cb19d", + "3.6.0-0-g45a11aa-develop", + "3.6.0-155-g71d08991", + "3.6.0-9-g36cb19db", + "3.7.1", + "3.7.1-0-gfef4bae", + "3.7.1-205-g82c1b7d4-akroma", + "3.7.1-yf", + "3.8.0", + "3.8.0-0-gccae7ef", + "3.9.2", + "3.9.2-0-g39a878f", + "3.9.3", + "3.9.3+dev", + "3.9.3-0-g30b9e626f-main", + "3.9.3-0-g7e45343-develop", + "3.9.3-0-g80519fe", + "3.9.3-0-g8c956bc-develop", + "3.9.3-0-g9d7c877", + "3.9.3-0-gd83f5f66f-blob", + "3.9.3-1-ge20de760-tokc-stats", + "3.9.3-140-ga6473c86-web-protocol-handler", + "3.9.3-1489-g44514535-fox", + "3.9.3-15-g9d7c877d", + "3.9.3-2-gfb8081e1-develop", + "3.9.3-28-g9d7c877d", + "3.9.3-28-g9d7c877d-akkoma", + "3.9.3-29-gc1306989-nya", + "3.9.3-95-g801fe9fe-develop", + "3.9.3-heads-v3-9-3" + ], + "andrea und jürgen": [ + "0.6.0-beta5" + ], + "ap.39sho": [ + "0.0.1" + ], + "apd": [ + "v0.13.0 git-72d0f46" + ], + "areionskey": [ + "3.1.0" + ], + "betula": [ + "1.2.0" + ], + "birdsitelive": [ + "0.20.0", + "0.22.0", + "0.23.0", + "1.0.0" + ], + "bonfire": [ + "0.9.5-beta.27", + "0.9.8-beta.26" + ], + "bookwyrm": [ + "0.4.6", + "0.5.3", + "0.5.4", + "0.6.0", + "0.6.2", + "0.6.3", + "0.6.4", + "0.6.6", + "0.7.0", + "0.7.1", + "0.7.2" + ], + "bovine": [ + "0.2.5" + ], + "bridgy-fed": [ + "20240123t140632" + ], + "brutalinks": [ + "master-8e7ebec1" + ], + "bugle": [ + "1.0.0" + ], + "calckey": [ + "13.0.6-bark", + "13.1.2", + "13.1.4.1", + "13.2.0-beta6", + "13.2.0-beta9h", + "13.2.0-dev17", + "14.0.0-d10+38d8bd", + "14.0.0-rc", + "14.0.0-rc3" + ], + "capubara": [ + "0.1" + ], + "castcloud": [ + "0.6.0-beta1" + ], + "castling.club": [ + "n/a" + ], + "catnip": [ + "2023.09.13-rc1" + ], + "catodon": [ + "23.12-alpha.3" + ], + "cherrypick": [ + "13.14.2-cp-4.2.0", + "4.5.0", + "4.5.1", + "4.6.0", + "4.6.0+cs-7a02ecb", + "4.6.0-beta.6", + "4.6.0-beta.7" + ], + "citizen4": [ + "0.6.1" + ], + "clanspub": [ + "1.0.0" + ], + "cloud.syvi.net": [ + "0.6.0-rc2" + ], + "coma": [ + "v0.0.1" + ], + "comal": [ + "1.0.0" + ], + "concurrent": [ + "2b053fd" + ], + "cycloud": [ + "0.6.1" + ], + "dailyrucks": [ + "0.1-nightly" + ], + "denali": [ + "v4993" + ], + "diaspora": [ + "0.7.10.0", + "0.7.10.0-p663da1ef", + "0.7.12.0-p09a425cb", + "0.7.13.0-pc2a991fe", + "0.7.14.0", + "0.7.14.0-p4ca68a71", + "0.7.15.0", + "0.7.15.0-p1d098282", + "0.7.16.0", + "0.7.17.0", + "0.7.17.0-p02eba842", + "0.7.18.0-p31beb6a8", + "0.7.18.0-p9485a026", + "0.7.18.1", + "0.7.18.1-p52f206fa", + "0.7.18.2", + "0.7.18.2-p127f12e5", + "0.7.18.2-p1f635894", + "0.7.18.2-p20b41cf5", + "0.7.18.2-p4902f8a0", + "0.7.18.2-p518652e0", + "0.7.18.2-p6da4038c", + "0.7.18.2-p84e7e411", + "0.7.18.2-pa63281cd", + "0.7.18.2-pf042f5d4", + "0.7.4.0-pd0313756", + "0.7.5.0-p42ceb8b3", + "0.7.99.0", + "1.0.0-dev-p0a545c70", + "1.0.0-dev-p3030c1a8", + "1.0.0-dev-p389b1870", + "1.0.0-dev-p3c8d75d6", + "1.0.0-dev-p5af17a0f", + "1.0.0-dev-p744f5449", + "1.0.0-dev-p94c751b0", + "1.0.0-dev-pa3df4d76", + "1.0.0-dev-pd45930a1", + "1.0.0-dev-pde3b1804" + ], + "ditto": [ + "0.0.0" + ], + "divedb": [ + "0.1.0" + ], + "dolphin": [ + "1.4.0-20220709205730", + "1.4.0-20230211123100", + "1.4.0-20231128081000" + ], + "drupal": [ + "1.0.0", + "10.1.6" + ], + "ecko": [ + "3.4.9001" + ], + "emissary": [ + "0.1.0" + ], + "epicyon": [ + "0.0.0" + ], + "f-cloud": [ + "0.6.1" + ], + "falco nimbus": [ + "0.6.1" + ], + "fchannel": [ + "0.1.1" + ], + "federated": [ + "0.0.1" + ], + "fedibird": [ + "0.1" + ], + "fediblock instance": [ + "1.0.0" + ], + "fedipage": [ + "2.2.0" + ], + "ffca parent community": [ + "0.6.1" + ], + "firefish": [ + "+neko:240104.2", + "1.0.0", + "1.0.1", + "1.0.3", + "1.0.3-dev667", + "1.0.3-dragon2", + "1.0.3-ryan.1", + "1.0.3-shibimega", + "1.0.4", + "1.0.4-beta", + "1.0.4-beta2", + "1.0.4-beta31", + "1.0.4-beta31-hinasense.jp.0", + "1.0.4-dev5", + "1.0.5", + "1.0.5-dev11", + "1.0.5-dev12", + "1.0.5-dev14-piyoskey", + "1.0.5-dev17", + "1.0.5-dev18", + "1.0.5-dev19", + "1.0.5-dev19+nyan", + "1.0.5-dev20", + "1.0.5-dev21", + "1.0.5-dev21+neko:231117.b", + "1.0.5-dev22", + "1.0.5-dev22+neko:231128.b", + "1.0.5-dev6", + "1.0.5-dev7", + "1.0.5-rc", + "1.0.5-rc+neko:231227.a", + "1.0.5-rc+neko:240103.3", + "1.0.5-rc+neko:240105.d", + "1.0.5-rc+neko:240107.54", + "1.0.5-rc+neko:240111.fd", + "1.0.5-rc+neko:240113.22", + "1.0.5-rc+neko:240120.83", + "1.0.5-rc+neko:240120.ea", + "1.0.5-rc+neko:240123.1f", + "1.0.5-rc-choco", + "1.0.5-rc-shrimpkey1.0.2", + "1.0.5-rc.eepy", + "1.1.0-dev29-hf1", + "A.K:NoDM@1.0.5-rc" + ], + "fnuk nextcloud": [ + "0.6.1" + ], + "forgejo": [ + "1.19.2-0", + "1.20.0+dev-2548-gb5757a6691", + "1.20.1+0", + "1.20.5+0", + "1.21.2+0", + "1.21.3+0", + "1.21.3-0", + "1.21.4+0", + "1.21.4+gay" + ], + "foundkey": [ + "13.0.0-preview5", + "13.0.0-preview6" + ], + "foxhole": [ + "Foxhole-0.1.1+bbda14f3" + ], + "friendica": [ + "2019.12-1327", + "2020.03-1338", + "2020.07-1-1355", + "2020.09-1-1369", + "2020.09-dev-1364", + "2021.01-1384", + "2021.07-1424", + "2021.09-1434", + "2021.12-rc-1448", + "2022.03-1452", + "2022.06-1469", + "2022.10-1484", + "2022.12-1502", + "2022.12-dev-1486", + "2022.12-dev-1488", + "2023.01-1502", + "2023.03-dev-1516", + "2023.03-dev-1517", + "2023.04-1-1518", + "2023.04-1518", + "2023.05-1518", + "2023.06-1518", + "2023.09-dev-1524", + "2023.09-dev-1534", + "2023.09-rc-1536", + "2023.09-rc-1537", + "2023.09-rc-1539", + "2023.09-rc-1540", + "2023.09-rc-1541", + "2023.09-rc-1542", + "2023.12-1542", + "2024.03-dev-1542", + "2024.03-dev-1543", + "2024.03-dev-1544", + "2024.03-dev-1545", + "2024.03-dev-1546", + "2024.03-dev-1547", + "2024.03-dev-1548", + "3.5.2-1227" + ], + "funkwhale": [ + "0.21", + "0.21.1+git.f14858c2", + "1.1", + "1.1.4", + "1.1.4+git.3c8c1524", + "1.2.0", + "1.2.10", + "1.2.10+git.d075c6ae", + "1.2.10+git.fbaa6e7b", + "1.2.3", + "1.2.3+git.dc35000d", + "1.2.5", + "1.2.7", + "1.2.7+git.ffe306ed", + "1.2.8", + "1.2.8+git.fb10d1a3", + "1.2.9", + "1.2.9+git.817c8fbc", + "1.3.0", + "1.3.1", + "1.3.1+git.cc7fde67", + "1.3.3", + "1.3.3+git.03f43519", + "1.3.4", + "1.4.0", + "1.4.0.dev0+59687b2f", + "1.4.0.dev0+5ce00a92", + "1.4.0.dev0+c3ae40ca", + "1.4.0rc2" + ], + "gancio": [ + "1.4.0", + "1.4.3", + "1.4.4", + "1.5.6", + "1.6.1", + "1.6.11", + "1.6.12", + "1.6.13", + "1.6.14", + "1.6.17", + "1.6.2", + "1.7.0", + "1.7.1", + "1.8.0" + ], + "gitea": [ + "1.20.4", + "1.20.6", + "1.21.0", + "1.21.2", + "1.21.3", + "1.21.4", + "v1.21.1" + ], + "glitchcafe": [ + "4.1.2+cafe.a9de073" + ], + "gnusocial": [ + "1.20.9-release", + "2.0.0-alpha0", + "2.0.0-beta0", + "2.0.0-dev", + "2.0.1-beta0", + "2.0.2-beta0" + ], + "goblin": [ + "0.0.1" + ], + "gotosocial": [ + "", + "0.10.0 git-89ee9d5", + "0.10.0-rc1 git-2a99df0", + "0.10.0-rc2 git-12b6cdc", + "0.10.0_2", + "0.10.0_3", + "0.10.1-SNAPSHOT git-cec29e2", + "0.11.0", + "0.11.0 git-815b529", + "0.11.0-rc2 git-b727454", + "0.11.1", + "0.11.1 git-c7a46e0", + "0.11.1-SNAPSHOT git-4b59451", + "0.11.1-SNAPSHOT git-b093947", + "0.12.0 git-9114c5c", + "0.12.0-rc2 git-805c67b", + "0.12.1 git-5fdc005", + "0.12.2", + "0.12.2 git-096c517", + "0.13.0", + "0.13.0 git-f4fcffc", + "0.13.0-rc1 git-0e2c342", + "0.13.0-rc2 git-d0bb8f0", + "0.13.1", + "0.13.1 git-ccecf5a", + "0.13.1-SNAPSHOT git-138cbe4", + "0.13.1-SNAPSHOT git-238cc19", + "0.13.1-SNAPSHOT git-33dbd3a", + "0.13.1-SNAPSHOT git-67e11a1", + "0.13.1-SNAPSHOT git-9607b48", + "0.13.1-SNAPSHOT git-97a1fd9", + "0.13.1-SNAPSHOT git-b2cacd6", + "0.13.1-SNAPSHOT git-dfc7656", + "0.13.2-SNAPSHOT-pkv git-a78ee34", + "0.14.0-138cbe", + "0.3.7 git-b6be973", + "0.5.1-SNAPSHOT git-05a8baa", + "0.5.1-SNAPSHOT git-81c1fe0", + "0.5.1-SNAPSHOT git-847a466", + "0.5.1-SNAPSHOT git-ad08c6c", + "0.5.2 git-c31f219", + "0.6.0 git-f9e5ec9", + "0.7.0-SNAPSHOT git-6a6647d", + "0.7.1 git-adb5966", + "0.7.1_2", + "0.8.1 git-4dbf9a2", + "0.9.0 git-282be6f", + "0.9.0-SNAPSHOT git-282be6f", + "0.9.0-rc1 git-2b7c815", + "0.9.1-SNAPSHOT git-0ebb672", + "0.9.1-SNAPSHOT git-282be6f", + "3.3.0", + "cat git-4cb3b59", + "f4fcffc8b56ef73c184ae17892b69181961c15c7", + "git-8ab30d0", + "kalaclista git-212fecf", + "v0.10.0 git-0fd77df", + "v0.10.0 git-56486ec", + "v0.11.1 git-c7a46e0", + "v0.12.0 git-0b978f2", + "v0.12.0 git-5fd2e42", + "v0.12.2+allowlist", + "v0.13.0 git-31e37d2", + "v0.13.0 git-42a6310", + "v0.13.0 git-68f7e35", + "v0.13.0 git-9607b48", + "v0.13.0 git-dfc7656", + "v0.13.0 git-ebf550b", + "v0.13.0 git-f4fcffc", + "v0.13.0-rc1 git-5f02d9c", + "v0.13.0-rc2 git-d0bb8f0", + "v0.13.1 git-ccecf5a", + "v0.5.0 git-45ae719", + "v0.6.0", + "v0.6.0-rc3 git-bc917a4", + "v0.8.0 git-093cf2a", + "v0.8.0 git-55a281a", + "v0.8.0 git-b4d1302", + "v0.9.0 git-282be6f", + "v0.9.0 git-c4cf632", + "vHEAD+allowlist+beholding [2023-11-17-14:59:50]" + ], + "groundpolis-milkey": [ + "2022.07.03-milkey-2.8" + ], + "guppe groups": [ + "1.5.2" + ], + "gush": [ + "DEVELOPMENT" + ], + "hajkey": [ + "2023.09.13-rc1" + ], + "hashtag.place": [ + "0.0.1" + ], + "hometown": [ + "1.0.6+3.5.2", + "1.0.7+3.5.5", + "3.4.6+1.0.5", + "3.5.14+hometown-1.0.8", + "4.0.10+hometown-1.1.1", + "4.0.2+hometown-1.1.0", + "4.0.2+hometown-1.1.1", + "4.0.4+hometown-1.1.1", + "4.0.5+hometown-1.1.1", + "4.0.6+hometown-1.1.1", + "4.1.0rc1+hometown-1.1.1", + "4.2.3", + "4.2.3+queergroup" + ], + "honk": [ + "0.10.2-0", + "1.1.1-poweredbyidols", + "1.2.0-dave", + "cursed 🙈", + "develop/git-409c85aa9450+", + "develop/git-975ba8e3ac4a+", + "develop/hg-d9644d5136cf+" + ], + "hourlyphoto": [ + "0.1", + "0.9" + ], + "hubzilla": [ + "3.2.1", + "3.8.8", + "4.0.3", + "4.4.1", + "5.2.2", + "6.0.1", + "6.4.2", + "7.0.3", + "7.2.2", + "7.4", + "7.6.1", + "7.8.6", + "8.0", + "8.2", + "8.4.2", + "8.6.2", + "8.6.3", + "8.8.1", + "8.8.2", + "8.8.4", + "8.8.5", + "8.8.6", + "8.8.7" + ], + "iceshrimp": [ + "2023.11-dev-556b7718c", + "2023.11.4", + "2023.12", + "2023.12-pre1-dev-889090267", + "2023.12-pre3", + "2023.12-pre4", + "2023.12-pre4-dev-bb4ffc6c5", + "2023.12.1", + "2023.12.1-dev-0a3fedfd4", + "2023.12.1-dev-197002df8", + "2023.12.1-dev-42fe936e9", + "2023.12.1-dev-4d9b8df5c", + "2023.12.1-dev-6b91be1b0", + "2023.12.1-jormungandr.1", + "2023.12.1.withdrawal1", + "2024.1.19.1" + ], + "immers space": [ + "5.1.0", + "6.0.0", + "6.0.1" + ], + "incestoma": [ + "6.9.1-118-g26e64b4b", + "6.9.1-212-gf5626728-develop" + ], + "ixsiid/mstdn": [ + "0.1.0" + ], + "juick": [ + "2.x" + ], + "kbin": [ + "0.10.1" + ], + "kepi": [ + "0.7" + ], + "klaud": [ + "0.6.0-rc2" + ], + "kmyblue": [ + "4.2.3+kmyblue.5.13-LTS", + "4.3.0-alpha.0+kmyblue.10.0", + "4.3.0-alpha.0+kmyblue.10.0.", + "4.3.0-alpha.0+kmyblue.11.0-dev" + ], + "kotori": [ + "0.0.5" + ], + "ktistec": [ + "2.0.0-3", + "2.0.0-6", + "2.0.0-8", + "2.0.0-9" + ], + "lcars": [ + "0.6.1" + ], + "lemmon": [ + "0.0.1" + ], + "lemmy": [ + "", + "-va-11-hall-a", + "0.16.6", + "0.16.7", + "0.17.0", + "0.17.1", + "0.17.3", + "0.17.4", + "0.18.0", + "0.18.0-rc.5", + "0.18.1", + "0.18.1-rc.10", + "0.18.1-rc.4", + "0.18.1-rc.9", + "0.18.2", + "0.18.3", + "0.18.3-rc.3", + "0.18.4", + "0.18.4-beta.5", + "0.18.4-beta.8", + "0.18.4-custom0", + "0.18.4-fl0w", + "0.18.4-kt.2", + "0.18.4-nick", + "0.18.5", + "0.18.5-nsfw", + "0.19.0", + "0.19.0-beta.5", + "0.19.0-beta.6", + "0.19.0-rc.1", + "0.19.0-rc.13", + "0.19.0-rc.2", + "0.19.0-rc.3", + "0.19.0-rc.8", + "0.19.1", + "0.19.1-rc.2", + "0.19.1-rc.2-19-g0e6669f61", + "0.19.2", + "0.19.2+tedomum.2", + "0.19.2-nsfw", + "0.19.2-rc.3.publish4", + "0.19.3", + "ff2bdac", + "unknown version" + ], + "lotide": [ + "0.8.0-pre" + ], + "lycheebridge": [ + "2024.1.1" + ], + "magnetar": [ + "14.0.0-alpha+magnetar-0.2.0" + ], + "mastodon": [ + "1.6.1+iaqd", + "3.0.0", + "3.0.1", + "3.0.2", + "3.1.0rc2", + "3.1.1", + "3.1.3", + "3.1.4", + "3.1.5", + "3.2.0", + "3.2.0rc1", + "3.2.1", + "3.2.1-qoto", + "3.2.2", + "3.3.0", + "3.3.0+glitch", + "3.3.0rc2", + "3.3.2", + "3.4.0", + "3.4.0rc2", + "3.4.1", + "3.4.1+glitch", + "3.4.10", + "3.4.10+glitch+atsu1125", + "3.4.3", + "3.4.4", + "3.4.5+cs", + "3.4.6", + "3.4.6+mootech4", + "3.4.6ht", + "3.4.7", + "3.5.0", + "3.5.0+glitch", + "3.5.0~hello", + "3.5.1", + "3.5.10", + "3.5.10 06f906acac", + "3.5.10+chillout.1", + "3.5.11", + "3.5.12", + "3.5.14", + "3.5.14+iceage", + "3.5.14+rinsuki.2", + "3.5.14-86db5d350", + "3.5.15", + "3.5.16", + "3.5.2", + "3.5.2+glitch_0509_e8b8ac8", + "3.5.3", + "3.5.3+glitch", + "3.5.3+glitch+puwa", + "3.5.3+kibicat", + "3.5.3~ Female Friendly Instance", + "3.5.3~hello", + "3.5.4", + "3.5.5", + "3.5.6", + "3.5.7", + "3.5.7-casino", + "3.5.8", + "3.5.9", + "4.0.0", + "4.0.0rc1", + "4.0.0rc1+glitch", + "4.0.0rc2", + "4.0.0rc3", + "4.0.1", + "4.0.1+glitch", + "4.0.10", + "4.0.11", + "4.0.12", + "4.0.2", + "4.0.2+glitch", + "4.0.3", + "4.0.4", + "4.0.5", + "4.0.5 (with modifications)", + "4.0.6", + "4.0.7", + "4.0.8", + "4.0.9", + "4.1.0", + "4.1.0+glitch", + "4.1.0rc1", + "4.1.0rc2", + "4.1.0rc3", + "4.1.1", + "4.1.1+glitch", + "4.1.1+glitch+bird", + "4.1.1+glitch+closedsocial", + "4.1.1+glitch+skyevg", + "4.1.10", + "4.1.10+snailedit.1", + "4.1.10~moew", + "4.1.11", + "4.1.11+stolat", + "4.1.11+yufushiro", + "4.1.11-3-woof", + "4.1.11-cw0", + "4.1.2", + "4.1.2+glitch", + "4.1.2+glitch+closedsocial", + "4.1.2+glitch+th+vtsocial", + "4.1.3", + "4.1.3+chitter", + "4.1.3+edge-dfedf0e", + "4.1.3+glitch", + "4.1.3+glitch+cutiecity", + "4.1.3+glitchcity", + "4.1.3+patches", + "4.1.4", + "4.1.4+glitch", + "4.1.4+glitch+cat+1.0.0", + "4.1.4+glitch+cat+1.0.0+maus", + "4.1.4+glitch+cat+1.0.0+nya-1.2.2", + "4.1.4+glitch+cat+meow+1.0.0.1", + "4.1.4+glitch+cathode", + "4.1.4+idyl", + "4.1.4+mathspy", + "4.1.4-beer-3", + "4.1.4-ht", + "4.1.4sp1+glitch+queeraf", + "4.1.4~angry", + "4.1.4~cdp1337", + "4.1.4~moew", + "4.1.4~wxw", + "4.1.5", + "4.1.5+glitch", + "4.1.5-844568c", + "4.1.6", + "4.1.6+glitch", + "4.1.6-abiz", + "4.1.6-gh23240", + "4.1.7", + "4.1.7+aniwork+aniwork", + "4.1.8", + "4.1.8+beachcity1.10.0", + "4.1.8+ertona.1", + "4.1.8+trans_rights-0aef33b-23262162413", + "4.1.8-donte", + "4.1.9", + "4.2.0", + "4.2.0+4240d8b", + "4.2.0+asonix-changes", + "4.2.0+chuckya", + "4.2.0+glitch", + "4.2.0+glitch.+AGATHA+AGATHA", + "4.2.0+types", + "4.2.0-beta1", + "4.2.0-beta1+glitch", + "4.2.0-beta2", + "4.2.0-beta2+glitch", + "4.2.0-beta2+glitch_0830_786e586", + "4.2.0-beta3", + "4.2.0-beta3+glitch", + "4.2.0-bfd", + "4.2.0-nightly.2023-09-20+glitch", + "4.2.0-nightly.2023-09-22+glitch", + "4.2.0-nightly.2023-09-25+glitch", + "4.2.0-nightly.2023-09-27", + "4.2.0-nightly.2023-09-30+chuckya", + "4.2.0-nightly.2023-10-07+glitch", + "4.2.0-rc2", + "4.2.0-rc2+glitch", + "4.2.0-rc2+glitch.jawa", + "4.2.0-rc2+glitch.th", + "4.2.1", + "4.2.1++desertflood--patch2", + "4.2.1++mnetwork1.9", + "4.2.1+dirty", + "4.2.1+est", + "4.2.1+island", + "4.2.1+nzws", + "4.2.1+orcas2", + "4.2.1+uwu", + "4.2.1+whippyEdition", + "4.2.1+yai", + "4.2.1-mrt", + "4.2.1-smore.patch.2", + "4.2.2", + "4.2.2+asonix-changes", + "4.2.2-stable+ff1", + "4.2.3", + "4.2.3+-kurry-2.0", + "4.2.3+-sif1.3", + "4.2.3+alphatown", + "4.2.3+doesstuffsocial-mods", + "4.2.3+ff1", + "4.2.3+fosspride", + "4.2.3+magincia", + "4.2.3+mscdn", + "4.2.3+occm.web1", + "4.2.3+pr-28367-d3dbf08", + "4.2.3+pvz2312252229", + "4.2.3+tokyo", + "4.2.3+uri1.38", + "4.2.3-nightly.2023-12-26", + "4.2.3-notalive", + "4.2.3-plusminus.1", + "4.2.3-plusminus.2", + "4.2.3-slis", + "4.2.3-stable+ff1", + "4.2.3-stable+ts1", + "4.2.3-theatlsocial-20240114", + "4.2.3-undefined+undefined", + "4.2.3_ef67daisuki_club", + "4.2.4", + "4.3.0-alpha.0", + "4.3.0-alpha.0+0d54f37f-ruby-3.3.0", + "4.3.0-alpha.0+97ce0cc061dd0d14372b5e80ebd6d387c34e7aad", + "4.3.0-alpha.0+YRYRi", + "4.3.0-alpha.0+akkodon", + "4.3.0-alpha.0+babka", + "4.3.0-alpha.0+bar", + "4.3.0-alpha.0+besties", + "4.3.0-alpha.0+chuckya", + "4.3.0-alpha.0+chuckya.main-d96fd9f28", + "4.3.0-alpha.0+dimension", + "4.3.0-alpha.0+farlands", + "4.3.0-alpha.0+foxsay+2024-01-18", + "4.3.0-alpha.0+glitch", + "4.3.0-alpha.0+glitch+cat+1.0.11+nya-1.2.2", + "4.3.0-alpha.0+glitch+cat+1.0.12", + "4.3.0-alpha.0+glitch+pegelinux", + "4.3.0-alpha.0+glitch.0104_dfbf960", + "4.3.0-alpha.0+glitch.0113_e76b7eb", + "4.3.0-alpha.0+glitch.0120_915cd36", + "4.3.0-alpha.0+glitch.0121_915cd36", + "4.3.0-alpha.0+glitch.0122_915cd36", + "4.3.0-alpha.0+glitch.1116_7e5d007", + "4.3.0-alpha.0+glitch.1121_769ab0c", + "4.3.0-alpha.0+glitch.1122_a21fe86", + "4.3.0-alpha.0+glitch.1211_98f5042", + "4.3.0-alpha.0+glitch.cs.usocial", + "4.3.0-alpha.0+glitch.donphan.social", + "4.3.0-alpha.0+glitch.polyam", + "4.3.0-alpha.0+glitch.th", + "4.3.0-alpha.0+io", + "4.3.0-alpha.0+jordemort", + "4.3.0-alpha.0+maud", + "4.3.0-alpha.0+mementomods-2024-01-14 + Mastodon Bird UI 2.0.0rc", + "4.3.0-alpha.0+nnn", + "4.3.0-alpha.0+pr-28119-9934dd1", + "4.3.0-alpha.0+pr-28693-384ec56", + "4.3.0-alpha.0+pr-28823+arbolitoloco", + "4.3.0-alpha.0+qdon.glitch", + "4.3.0-alpha.0+tncafe", + "4.3.0-alpha.0+tom", + "4.3.0-alpha.0+tontoelquelolea", + "4.3.0-alpha.0+vegan", + "4.3.0-alpha.0+~b612+glitch", + "4.3.0-alpha.0.12+glitch+COTS", + "4.3.0-bark+prod", + "4.3.0-nightly.2023-10-05", + "4.3.0-nightly.2023-10-11+glitch", + "4.3.0-nightly.2023-10-21+glitch", + "4.3.0-nightly.2023-10-22+glitch", + "4.3.0-nightly.2023-10-24+glitch", + "4.3.0-nightly.2023-10-25+glitch", + "4.3.0-nightly.2023-11-20", + "4.3.0-nightly.2023-11-27+glitch", + "4.3.0-nightly.2023-12-04", + "4.3.0-nightly.2023-12-07+glitch", + "4.3.0-nightly.2023-12-08+glitch", + "4.3.0-nightly.2023-12-13+glitch", + "4.3.0-nightly.2023-12-18+glitch", + "4.3.0-nightly.2023-12-21+glitch", + "4.3.0-nightly.2023-12-27+glitch", + "4.3.0-nightly.2023-12-29+chuckya", + "4.3.0-nightly.2023-12-29+glitch", + "4.3.0-nightly.2023-12-30+glitch", + "4.3.0-nightly.2024-01-05", + "4.3.0-nightly.2024-01-06", + "4.3.0-nightly.2024-01-06+glitch", + "4.3.0-nightly.2024-01-07+glitch", + "4.3.0-nightly.2024-01-08+glitch", + "4.3.0-nightly.2024-01-11+glitch", + "4.3.0-nightly.2024-01-12", + "4.3.0-nightly.2024-01-12+glitch", + "4.3.0-nightly.2024-01-13+glitch", + "4.3.0-nightly.2024-01-15+chuckya", + "4.3.0-nightly.2024-01-15+glitch", + "4.3.0-nightly.2024-01-16+glitch", + "4.3.0-nightly.2024-01-17+chuckya", + "4.3.0-nightly.2024-01-17+glitch", + "4.3.0-nightly.2024-01-18+glitch", + "4.3.0-nightly.2024-01-19+chuckya", + "4.3.0-nightly.2024-01-19+glitch", + "4.3.0-nightly.2024-01-20", + "4.3.0-nightly.2024-01-20+glitch", + "4.3.0-nightly.2024-01-21", + "4.3.0-nightly.2024-01-21+glitch", + "4.3.0-nightly.2024-01-22", + "4.3.0-nightly.2024-01-22+glitch", + "4.3.0-nightly.2024-01-23+glitch", + "4.3.0-rc2+glitch", + "4.3.0-sharlayan.dev-1ab28", + "4.3.3-alpha.0", + "4.3.3-alpha.0+glitch", + "4.3.3-nap-ver", + "4.3.3-nightly.2023-12-31" + ], + "mbin": [ + "1.1.0", + "1.2.1", + "1.3.0" + ], + "meisskey": [ + "10.102.666-m544", + "10.102.684-m544", + "10.102.685-m544", + "10.102.686-m544" + ], + "microblogpub": [ + "2.0.0+3c074948", + "2.0.0+40459020", + "2.0.0+4e1bb330", + "2.0.0+562f1d32", + "2.0.0+9c8693ea", + "2.0.0+a435cd33", + "2.0.0+caf12afc", + "2.0.0+dev", + "2.0.0+dev+nyt", + "2.0.0+dfe7f0db", + "2.0.0+e986ee08", + "2.0.0+f5cddb31", + "2.0.0+ynh1", + "Microblog.pub 669fec0", + "Microblog.pub de607e9e3946+" + ], + "microdotblog": [ + "2.1" + ], + "misskey": [ + "1.1.0-missingkey.develop", + "10.103.0.3-ypf", + "11.37.1-20230617014515", + "11.37.1-20231008191543", + "11.37.1-20231128072711", + "11.37.1-20231227020351", + "11.37.1-20231227020351+b987f44+aca0b9f", + "12.101.1", + "12.108.1", + "12.108.1-nekomiya-20231227211900", + "12.110.0", + "12.110.1", + "12.111.1", + "12.112.2", + "12.116.1", + "12.117.0", + "12.118.1-nem-v05", + "12.119.0", + "12.119.0+birb4-1", + "12.119.1", + "12.119.2", + "12.119.2-fix.6.2", + "12.119.2-taiyme-v1.2.0", + "12.119.2-taiyme-v1.2.0-17", + "12.120.0-simkey-2023-04-02", + "12.120.0-simkey-2023-12-03", + "12.64.2-1.2", + "12.74.1", + "12.75.0", + "12.98.0", + "13.10.2", + "13.10.3", + "13.10.3-simkey", + "13.10.3-xs", + "13.11.2", + "13.11.3", + "13.12.0", + "13.12.1", + "13.12.2", + "13.13.1", + "13.13.2", + "13.14.1", + "13.14.1-cc.c-0.0.1", + "13.14.2", + "13.14.2-negskey.4.0", + "13.14.2-neos-love.3", + "13.14.2-sh.1", + "13.14.2-steskey-v31", + "13.2.6", + "13.4.0", + "13.4.0-Aton-1", + "13.6.1", + "13.8.1", + "13.9.1", + "13.9.2", + "2023.10.0", + "2023.10.0-beta.2", + "2023.10.1", + "2023.10.1-estampie", + "2023.10.2", + "2023.10.2-kakunpc.0", + "2023.11.0", + "2023.11.0+reo2248", + "2023.11.0-7", + "2023.11.0-a97ac24", + "2023.11.0-maikaze", + "2023.11.1", + "2023.11.1+PiScript.0.0.3", + "2023.11.1-3dcg-love.4", + "2023.11.1-9ine-4.1.4", + "2023.11.1-doujin.3", + "2023.11.1-fluffysocial.5", + "2023.11.1-kabocab2", + "2023.11.1-mewl1", + "2023.11.1-mkoj", + "2023.11.1-nade1.4.6", + "2023.11.1-nmk1.1.3", + "2023.11.1-osky.1.0.9", + "2023.11.1-sidemisskey", + "2023.12.0", + "2023.12.0-beta.1", + "2023.12.0-beta.3", + "2023.12.0-mame.1", + "2023.12.0-square.1", + "2023.12.1", + "2023.12.1+munochi", + "2023.12.1-cbrx5", + "2023.12.2", + "2023.12.2+2", + "2023.12.2+24011923", + "2023.12.2+bscone.c", + "2023.12.2+cremebrul_ee.1", + "2023.12.2+pbzweihander.0", + "2023.12.2+serafuku", + "2023.12.2+tirr.1", + "2023.12.2+yurigarden.0", + "2023.12.2-1", + "2023.12.2-Hoshisaki-r1", + "2023.12.2-NJ-1.3.3", + "2023.12.2-Wayaskey-2023.11.0", + "2023.12.2-akatsukey-v2.0.3", + "2023.12.2-bsk-4.0.13", + "2023.12.2-bsk-5.0.8", + "2023.12.2-dev-art+1.0", + "2023.12.2-dream1.10.0", + "2023.12.2-drg-1.0.1", + "2023.12.2-focalorus", + "2023.12.2-geoplanetary.3", + "2023.12.2-ikaskey", + "2023.12.2-kakurega.1.28.5", + "2023.12.2-kinel.1", + "2023.12.2-ll1", + "2023.12.2-ltn.24972", + "2023.12.2-mashiro.5.1", + "2023.12.2-minegumo", + "2023.12.2-mt224244", + "2023.12.2-na2na-v2", + "2023.12.2-neko", + "2023.12.2-nyaaapp.1", + "2023.12.2-p1.15", + "2023.12.2-pie-3.1.6", + "2023.12.2-poskey-9d9955a", + "2023.12.2-psr.6.1.6", + "2023.12.2-resonite-love.1", + "2023.12.2.mogeko.1", + "2023.12.2.papi.dev.2.hotfix.3", + "2023.12.2~hashi", + "2023.9.0", + "2023.9.0-beta.5", + "2023.9.1", + "2023.9.3", + "2023.9.3-1", + "2023.9.3-kids", + "2023.9.3-sn231125", + "2024.1.0+date:1.21+00:35", + "2024.1.0-dev.2-miyaco.18", + "2024.1.0-io", + "2024.2.0-beta.2", + "2024.2.0-beta.3", + "2024.2.0-beta.3-kaseiski", + "2024.2.0-beta.4", + "2024.2.0-beta.4-PrisMisskey.1", + "2024.2.0-beta.4-WKH0.1", + "2024.2.0-beta.4.papi" + ], + "mitra": [ + "1.12.0", + "2.5.0", + "2.6.0", + "2.7.2" + ], + "mobilizon": [ + "1.2.3-dirty", + "1.3.0", + "2.0.1", + "2.0.2", + "2.1.0", + "3.0.0", + "3.0.3", + "3.0.4", + "3.1.0", + "3.1.1", + "3.1.3", + "3.1.3-dirty", + "3.2.0", + "3.2.0-118-gad597db2", + "3.2.0-beta.2-6-g19f595c2d-dirty", + "3.2.0-dirty", + "4.0.0", + "4.0.0-rc.1-dirty", + "4.0.1", + "4.0.2", + "4.0.2-dirty" + ], + "mon cloud à moi que j\u0027ai": [ + "0.6.1" + ], + "monkeecloud": [ + "0.6.1" + ], + "mostr": [ + "1.0.0" + ], + "myrmidon": [ + "1.0d2" + ], + "n/a": [ + "2019.09-1323" + ], + "nagitodon": [ + "4.2.3+nagitodon.5.13-LTS." + ], + "neodb": [ + "0.9.1.7", + "0.9.5.2", + "main" + ], + "nexkey": [ + "12.23Q4.8", + "12.24Q1.1" + ], + "nextcloud": [ + "0.6.1" + ], + "nextcloud on global-social.net": [ + "0.6.1" + ], + "nextcloud social": [ + "0.5.0-beta3", + "0.6.0-beta1", + "0.6.0-rc1", + "0.6.1" + ], + "nextcloudpi": [ + "0.6.1" + ], + "none": [ + "0.0.0" + ], + "notestock": [ + "1.0" + ], + "noyaskey": [ + "13.13.2-takamatsu" + ], + "ocamlot": [ + "1.0.0" + ], + "octopusapp": [ + "0.2.5" + ], + "ohagi": [ + "1.0.0" + ], + "openlink-virtuoso": [ + "08.03" + ], + "owncast": [ + "0.0.11", + "0.0.12", + "0.0.13", + "0.0.13-docker", + "0.1.0", + "0.1.1", + "0.1.2", + "0.1.3", + "0.1.3 20231226-custom", + "20230513-nightly", + "20230721-nightly", + "20230906-nightly", + "20231208-nightly", + "20240123-nightly", + "dev", + "v0.1.0" + ], + "p3k": [ + "0.9.0" + ], + "paragon cloud": [ + "0.6.0-beta5" + ], + "paul.kinlan.me": [ + "0.0.1" + ], + "peertube": [ + "1.1.0", + "1.2.1", + "1.3.0", + "1.3.1", + "2.0.0", + "2.1.1", + "2.2.0", + "2.3.0", + "2.4.0", + "3.0.0", + "3.0.1", + "3.1.0", + "3.2.0", + "3.2.1", + "3.3.0", + "3.4.0", + "3.4.0-rc.1", + "3.4.1", + "4.0.0", + "4.1.0", + "4.1.1", + "4.2.0", + "4.2.1", + "4.2.2", + "4.3.0", + "4.3.1", + "5.0.0", + "5.0.1", + "5.1.0", + "5.1.0-rc.1", + "5.2.0", + "5.2.1", + "6.0.0", + "6.0.0-rc.1", + "6.0.1", + "6.0.2", + "6.0.3" + ], + "pelican-activitypub": [ + "0.1" + ], + "pict clip": [ + "2.0.0" + ], + "piefed": [ + "0.1" + ], + "pixelfed": [ + "0.10.0", + "0.10.10", + "0.11.1", + "0.11.2", + "0.11.3", + "0.11.4", + "0.11.5", + "0.11.6", + "0.11.7", + "0.11.8", + "0.11.9", + "0.11.9+mscdn" + ], + "pleroma": [ + "0.9.0", + "1.0.6", + "1.0.7-2-ga067cf0f", + "1.1.50-2026-g0d24ab04-develop", + "1.1.7-release-1-1-8", + "1.1.8-5-ge372917f-release-1-1-8", + "1.1.9", + "2.0.3-stable", + "2.0.5-stable", + "2.0.50-2551-ge1c8c599-develop", + "2.0.50-323-gb36ee880-bidule", + "2.0.6-stable", + "2.0.7-stable", + "2.1.0-4-gd1644efd-xqz-v2-1-0", + "2.1.2", + "2.1.50-517-gbc3cf0fe-develop", + "2.1.50-854-g294628d9-develop", + "2.2.0-8-g15550f7c", + "2.2.1", + "2.2.2", + "2.2.2-30-gbe47abb6", + "2.2.2-release-2-2-3", + "2.2.50-438-g5a5ff508-develop", + "2.2.50-622-ge4f1d8f4-develop", + "2.3.0", + "2.3.0-1-gb221d77a", + "2.4.0-516-gdc63aaf8", + "2.4.0-heads-v2-4-0", + "2.4.1", + "2.4.2", + "2.4.2-local", + "2.4.3", + "2.4.4", + "2.4.4-5-gb2909492-stereophonic-stable", + "2.4.5-10-g3a6cf093-master-pooza", + "2.4.5-2-gd8e32646", + "2.4.51-1013-g8517bc18-develop", + "2.4.51-1201-g6e0c3499-develop", + "2.4.51-468-gd7c53da7-develop", + "2.4.51-472-g8517bc18", + "2.4.52-1748-g98ccf6cb.develop+soapbox", + "2.4.53+soapbox", + "2.4.53-0-gdaef22d83.main+soapbox", + "2.4.53-1829-g044719737.develop+soapbox", + "2.4.53-1957-g4f61f19f.develop+soapbox", + "2.4.53-1991-g7a145177.develop+soapbox", + "2.4.53-1997-g9fc73622.develop+soapbox", + "2.4.53-2-g232170cb.develop+soapbox", + "2.4.53-2092-g8a189859.develop+soapbox", + "2.4.53-2098-g232170cb.develop+soapbox", + "2.4.53-2103-g0d08f049.develop+soapbox", + "2.4.53-2105-g75fee846.develop+soapbox", + "2.4.53-4-g64945b3b.develop+soapbox", + "2.4.53-7-g0d08f049.develop+soapbox", + "2.4.53-845-g8042e0eb-develop", + "2.4.53-855-gda0ef154-develop", + "2.4.53-863-g9f708037", + "2.4.53-888-g3d1828f4-develop", + "2.4.53-9-g75fee846.develop+soapbox", + "2.4.53-904-gbb63f72c-develop", + "2.4.53-906-gf40ccce7-develop", + "2.4.53-912-g7f0b3161-develop", + "2.4.55-925-gf60cb0f7-develop", + "2.5.0", + "2.5.0-1081-g5a8a91c9-v2-5-0", + "2.5.1", + "2.5.1-1080-gfd46f83d", + "2.5.1-4-g242dc2f8-develop-20231230", + "2.5.2", + "2.5.2-1-g513bd12b", + "2.5.2-13-g2d193861", + "2.5.2-7-g2f259166-release-2-5-2", + "2.5.2-makepkg", + "2.5.3", + "2.5.4", + "2.5.5", + "2.5.5+debian.1.1", + "2.5.5-0-g255cf67-xxx", + "2.5.5-1123-gf966abe4", + "2.5.5-52-gf966abe4", + "2.5.50-10-g51b45132-develop", + "2.5.50-149-g5576f7d1-develop", + "2.5.50-190-g4c0ab884.develop+soapbox", + "2.5.50-194-g9af0657e.develop+soapbox", + "2.5.50-2286-g4c0ab884.develop+soapbox", + "2.5.50-2288-g648773c0.develop+soapbox", + "2.5.50-4-g2bc69111-develop", + "2.5.50-46-g19933a06-develop", + "2.5.50-48-g8a0162cd-develop", + "2.5.50-885-g2c0fb7aa-main", + "2.5.51-0-g78a6f56-develop", + "2.5.51-128-g248f914e-develop", + "2.5.51-130-g143676f5", + "2.5.51-152-g0524e66a-develop", + "2.5.51-160-gcd9d6a12", + "2.5.51-2385-g401e832f.develop+soapbox", + "2.5.51-2395-g6e34df89.develop+soapbox", + "2.5.51-289-g401e832f.develop+soapbox", + "2.5.51-299-g6e34df89.develop+soapbox", + "2.5.51-305-g6112d45a.develop+soapbox", + "2.5.51-316-gd9316c48.develop+soapbox", + "2.5.52-226-g93ad16cc", + "2.5.52-235-g589301ce", + "2.5.52-235-g589301ce-develop", + "2.5.52-254-g48e490cd-develop", + "2.5.52-262-g02629169", + "2.5.52-2682-ga8f2000f.develop+soapbox", + "2.5.52-270-g2b9cd25c", + "2.5.52-284-g7da6a82d-develop", + "2.5.52-289-gd1b7f5b1-bbs-kawa-kun-com", + "2.5.52-296-ge3820716-develop", + "2.5.52-438-ge9ad89f0+soapbox", + "2.5.52-438-ge9ad89f0.develop+soapbox", + "2.5.52-450-g8f073333.develop+soapbox", + "2.5.52-569-g12bba871.develop+soapbox", + "2.5.53-314-g1062185b", + "2.5.54-0-ga94cf2ad", + "2.5.54-114-g4e355b85-develop", + "2.5.54-130-g17c336de", + "2.5.54-1815-gbf50f18e+soapbox", + "2.5.54-2720-g9bec0223.develop+soapbox", + "2.5.54-2739-g43ee0327.develop+soapbox", + "2.5.54-2742-gd92eef29.develop+soapbox", + "2.5.54-321-g17c336de-develop", + "2.5.54-325-gb729a8b1-develop", + "2.5.54-333-g1e685c83-develop", + "2.5.54-338-ga94cf2ad-develop", + "2.5.54-341-g807e1cc1-yukiho", + "2.5.54-412-ge3ea311c-develop", + "2.5.54-594-gb094e92c.develop+soapbox", + "2.5.54-596-gbf50f18e.develop+soapbox", + "2.5.54-603-g95261979.develop+soapbox", + "2.5.54-614-g95261979.develop+soapbox", + "2.5.54-622-g9db714fe.develop+soapbox", + "2.5.54-624-g9bec0223.develop+soapbox", + "2.5.54-634-g374b0998.develop+soapbox", + "2.5.54-636-g6ceceef8.develop+soapbox", + "2.5.54-640-gacbec640.develop+soapbox", + "2.5.54-644-gd860a667.develop+soapbox", + "2.5.54-646-gd92eef29.develop+soapbox", + "2.5.54-648-g8455726b+soapbox", + "2.6.0", + "2.6.0-0-g2c7e0b20", + "2.6.0-0-gc093384", + "2.6.0-1-g53740a43-spline", + "2.6.0-1-gc51a33bc", + "2.6.0-1499-g6f654d53", + "2.6.0-1515-g6f654d53", + "2.6.0-3143-g6f654d53", + "2.6.0-405-g6f654d53", + "2.6.0-478-gc8a285d4-eientei", + "2.6.0-8-g937ffe7a", + "2.6.1", + "2.6.1+debian.1", + "2.6.1-0-g0b35fdde2-develop", + "2.6.1-0-g804659661-syobar", + "2.6.1-1-g4a2279c3", + "2.6.1-1528-g6722b7f3", + "2.6.1-418-g6722b7f3", + "2.6.1-422-gb41af9c2-magistra", + "2.6.1-452-gf8258d17", + "2.6.1-712-g828e016c-fluffytail", + "2.6.1-fedi-cc", + "2.6.1-local-v2-6-1", + "2.6.50+soapbox", + "2.6.50-0-g4ebfc01-develop", + "2.6.50-1172-gd3022f70+soapbox", + "2.6.50-15-ga51f3937-develop", + "2.6.50-158-g6a6a631c-develop", + "2.6.50-171-g5f74aada-develop", + "2.6.50-179-ga7f82ff8-develop", + "2.6.50-2-g4c5b45ed", + "2.6.50-2-g4c5b45ed-develop", + "2.6.50-263-g02acf7c0-develop", + "2.6.50-266-g7622a839-develop", + "2.6.50-2966-gfdbcf998.develop+soapbox", + "2.6.50-3-gbeabb27f-develop", + "2.6.50-3063-gab0a0d77.gleasonator+soapbox", + "2.6.50-4-g11c52060", + "2.6.50-6614-g034189af-shigusegubu", + "2.6.50-870-gfdbcf998.develop+soapbox", + "2.6.50-872-gcb3e0462.develop+soapbox", + "2.6.50-872-gcb3e0462.rebased-develop+soapbox", + "2.6.50-873-g02d0580b.neets-fork+soapbox", + "2.6.50-874-g491f84f2.develop+soapbox", + "2.6.50-874-g491f84f2.main+soapbox", + "2.6.50-875-gcbad89b2.service-origin+soapbox", + "2.6.50-876-g833e59fe.develop+soapbox", + "2.6.50-876-g833e59fe.main+soapbox", + "2.6.50-876-gb86caf6e.service-origin+soapbox", + "2.6.50-878-g48ce0783.develop+soapbox", + "2.6.50-878-g48ce0783.main+nekobox", + "2.6.50-878-g48ce0783.main+soapbox", + "2.6.50-880-g6ea0d8e7.develop+soapbox", + "2.6.50-880-g6ea0d8e7.main+soapbox", + "2.6.50-929-gd105219c.backend+bigbuffet", + "2.6.50-962-g399fbe61.develop+soapbox", + "2.6.51-0-g434cf5c8", + "2.6.51-0-g9d6de14-pl-kpherox-dev", + "2.6.51-0-geb8409822-ryona-dev", + "2.6.51-111-gf74f5e0a", + "2.6.51-1823-g00d35993-securomoe-develop", + "2.6.51-1858-gcc6412f8-no-acl", + "2.6.51-1950-gdaddc123-dev-lanodan2", + "2.6.51-277-g99b07c81-develop", + "2.6.51-292-g344c798b-develop", + "2.6.51-301-g5f1d7073-develop", + "2.6.51-330-ga6fc97ff-develop", + "2.6.51-332-g8ac44586-develop", + "2.6.51-338-gf74f5e0a-develop", + "2.6.51-340-g67a5542a-develop", + "2.6.51-348-gd4b88978-develop", + "2.6.51-350-g9cc46c55-develop", + "2.6.51-356-g9b39bc6a-develop", + "2.6.51-377-g4c20713e-develop", + "2.6.51-379-g81a13b4b-develop", + "2.6.51-403-g3c65a289-develop", + "2.6.51-404-g5d2c7be5-pony-telemetry", + "2.6.51-451-g548434f8", + "2.6.51-455-gd802e65c-develop", + "2.6.51-457-gf7b3681e", + "2.6.51-482-geae99b13-bikeshed-webpush", + "2.6.51-505-g9aa3ba55-shitposterclub", + "2.6.51-9311-g0aff56e3-neckbeard", + "Pleroma 0.9.0 d93789dfde3c44c76a56732088a897ddddfe9716" + ], + "pleroma_anni": [ + "2.6.0-2-g6b975ebd" + ], + "plume": [ + "0.3.0", + "0.4.0", + "0.6.0", + "0.6.1-dev", + "0.7.0", + "0.7.1", + "0.7.1-dev", + "0.7.2", + "0.7.3-dev", + "0.7.3-dev-fork" + ], + "pods-social": [ + "0.0.0" + ], + "postgrunge": [ + "0.0.1" + ], + "postmarks": [ + "0.0.1" + ], + "prismo": [ + "0.7.0.rc1" + ], + "project-alice": [ + "0.1.0" + ], + "psh": [ + "2.3.0" + ], + "pub": [ + "0.0.0-devel" + ], + "quolibet": [ + "1.0" + ], + "redmatrix": [ + "7.6.1" + ], + "riverscape": [ + "0.0.1" + ], + "rosekey": [ + "2024.01.00.dev2" + ], + "rumisskey": [ + "2023.12.31" + ], + "schleicloud": [ + "0.6.1" + ], + "scrapblog": [ + "0.0.1" + ], + "sequel": [ + "0.19.0" + ], + "sergcloud": [ + "0.6.1" + ], + "servilo": [ + "0.6.1" + ], + "sharkey": [ + "2023.10.1.beta1", + "2023.11.2", + "2023.11.2.beta1", + "2023.12.0", + "2023.12.0.beta1", + "2023.9.1.beta1", + "2024.1.0.beta1", + "2024.1.0.beta1.custard5", + "2024.1.0.beta2", + "2024.1.0.beta2.eepy8", + "2024.1.0.beta2.kopper8", + "2024.2.0-beta1" + ], + "shuttlecraft": [ + "0.0.1" + ], + "simcloud": [ + "0.6.0-rc2" + ], + "simple social network": [ + "1.0" + ], + "smithereen": [ + "0.5.1-dev.33+c85ed8bc", + "0.5.1-dev.34.dirty+f963c476" + ], + "snac": [ + "1337-gyptazy", + "2.26", + "2.27-dev", + "2.31-dev", + "2.36-dev", + "2.42", + "2.42-dev", + "2.43", + "2.43-dev", + "2.44", + "2.44-dev", + "2.45", + "2.45-dev" + ], + "socialhome": [ + "0.18.0", + "0.18.0-dev" + ], + "socialsoup": [ + "0.0.2" + ], + "spicy shoggoth nextcloud": [ + "0.6.1" + ], + "squidcity": [ + "0.1-eel" + ], + "srijancloud": [ + "0.6.0-rc2" + ], + "sutty-distributed-press": [ + "0.1.0" + ], + "tafarn": [ + "0.1.0" + ], + "takahe": [ + "0.10.0-dev", + "0.10.1", + "0.7.0", + "0.9.0", + "0.9.0-dev" + ], + "takesama": [ + "2023-06-26" + ], + "the ladners\u0027 cloud": [ + "0.6.1" + ], + "the lukes cloud": [ + "0.6.1" + ], + "tilera cloud": [ + "0.6.1" + ], + "timeline": [ + "0.1.0" + ], + "tksm": [ + "1.0.0" + ], + "tokoroten": [ + "0.1.0" + ], + "tootik": [ + "0.7.0", + "?" + ], + "transit fedilerts": [ + "0.1.0" + ], + "tsundere": [ + "0.0.1" + ], + "twitter": [ + "0.1.0" + ], + "undefined": [ + "undefined" + ], + "untergang die cloud": [ + "0.6.0-beta5" + ], + "verces": [ + "0.1.0" + ], + "vestuk-cloud": [ + "0.6.1" + ], + "wafrn": [ + "0.0.2" + ], + "waq": [ + "0.1.0" + ], + "wild league": [ + "0.0.1" + ], + "wildebeest": [ + "0.0.1", + "0.1.0" + ], + "wolkenkuckucksheim": [ + "0.6.0-beta5" + ], + "writefreely": [ + "", + "0.12.0", + "0.12.0-277-g967ee96", + "0.13.0", + "0.13.1", + "0.13.2", + "0.13.2+patch1", + "0.13.2-83-g9580cff", + "0.14.0", + "0.14.0-1-g62f9b29", + "0.14.0-141-g1aa62e0", + "0.14.0-88-gb85afa1", + "0.8.1" + ], + "wxwclub": [ + "0.0.5" + ], + "x-activitypub-bridge": [ + "0.5.0" + ], + "yoiyami": [ + "v6-beta25_based_on_12.119.2", + "v6-beta26_based_on_12.119.2" + ], + "zap": [ + "22.12.30" + ], + "えちからどっとねっと": [ + "0.6.1" + ], + "ぷよ橋activitypub": [ + "0.1.0" + ] +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 37076a1d9..b48a1e3aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,4 +47,5 @@ include(":core:navigation") include(":core:network") include(":core:testing") include(":tools:mklanguages") +include(":tools:mkserverversions") include(":checks") diff --git a/tools/mkserverversions/README.md b/tools/mkserverversions/README.md new file mode 100644 index 000000000..861c17cae --- /dev/null +++ b/tools/mkserverversions/README.md @@ -0,0 +1,24 @@ +# mkserverversions + +## Synopsis + +`mkserverversions` creates a JSON5 file that maps different Fediverse server +names to a list of the different version strings seen for those servers, as +recorded by [Fediverse Observer](https://fediverse.observer). + +This is used as input data for `core/network/ServerTest`, to ensure that the +version parsing code can handle real-world version strings. + +Run `mkserverversions` every month to update the test data, and update the +parsing code if any of the tests fail. + +## Usage + +From the parent directory, run: + +```shell +./runtools mkserverversions +``` + +Verify the modifications made to `server-versions.json5`, re-run the tests, +and commit the result. diff --git a/tools/mkserverversions/build.gradle.kts b/tools/mkserverversions/build.gradle.kts new file mode 100644 index 000000000..c3fbb5b4b --- /dev/null +++ b/tools/mkserverversions/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Pachli Association + * + * This file is a part of Pachli. + * + * 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. + * + * Pachli 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 Pachli; if not, + * see . + */ + +plugins { + id("com.apollographql.apollo3") version "3.8.2" +} + +application { + mainClass.set("app.pachli.mkserverversions.MainKt") +} + +dependencies { + // GraphQL client + implementation("com.apollographql.apollo3:apollo-runtime:3.8.2") + + // Logging + implementation("io.github.oshai:kotlin-logging-jvm:5.1.0") + implementation("ch.qos.logback:logback-classic:1.4.11") + + implementation(libs.gson) + + // Testing + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.1") // for parameterized tests +} + +tasks.test { + useJUnitPlatform() +} + +apollo { + service("service") { + packageName.set("app.pachli.mkserverversions.fediverseobserver") + } +} diff --git a/tools/mkserverversions/settings.gradle.kts b/tools/mkserverversions/settings.gradle.kts new file mode 100644 index 000000000..ca0d9fd86 --- /dev/null +++ b/tools/mkserverversions/settings.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Pachli Association + * + * This file is a part of Pachli. + * + * 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. + * + * Pachli 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 Pachli; if not, + * see . + */ + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "mkserverversions" diff --git a/tools/mkserverversions/src/main/graphql/ServerVersions.graphql b/tools/mkserverversions/src/main/graphql/ServerVersions.graphql new file mode 100644 index 000000000..f3b1a0aff --- /dev/null +++ b/tools/mkserverversions/src/main/graphql/ServerVersions.graphql @@ -0,0 +1,6 @@ +query ServerVersions { + nodes(status: "UP") { + softwarename + fullversion + } +} diff --git a/tools/mkserverversions/src/main/graphql/schema.graphqls b/tools/mkserverversions/src/main/graphql/schema.graphqls new file mode 100644 index 000000000..4510829b6 --- /dev/null +++ b/tools/mkserverversions/src/main/graphql/schema.graphqls @@ -0,0 +1,353 @@ +type Node { + "A unique idenifier the database creates" + id: Int! + "The name in the nodeinfo" + name: String + "The HTML Title tag of the server homepage" + metatitle: String + "The HTML Description tag of the server homepage" + metadescription: String + "The full json of the nodeinfo" + metanodeinfo: String + "The full json of the IP data from the IP" + metalocation: String + "The owner defined in your nodeinfo" + owner: String + "The HTML Image tag of the server homepage" + metaimage: String + "The Onion address from your web server header Onion-Location" + onion: String + "The i2p address from your web server header X-I2P-Location" + i2p: String + "The link to the terms of service based on best known for your software (owner editable)" + terms: String + "The link to the privacy policy based on best known for your software (owner editable)" + pp: String + "The link to the support based on best known for your software (owner editable)" + support: String + "The camo json data string from the nodeinfo" + camo: String + "The zipcode found from the IP location data" + zipcode: String + "The domain of server" + domain: String + "A open text field for the public comments on the server" + podmin_statement: String + "The main code version from the git repository for the software" + masterversion: String + "The short human readable version from the nodeinfo" + shortversion: String + "The software name from the nodeinfo" + softwarename: String + "Days we have been checking the server" + daysmonitored: Int + "Months we have been checking your server" + monthsmonitored: Int + "The full version found on the server from the nodeinfo" + fullversion: String + "Score based on the checks of the server being online each time checked" + score: Int + "The IP from looking at the DNS records" + ip: String + "Detected language from your html title and description tags on the site" + detectedlanguage: String + "Detected country code from the IP data" + country: String + "Detected country name from the IP data" + countryname: String + "Detected city name from the IP data" + city: String + "Detected state name from the IP data" + state: String + "Detected lattitude from the IP data" + lat: String + "Detected longitude name from the IP data" + long: String + "The IPv6 Address from DNS records" + ipv6: String + "True or False that the SSL certificate valid" + sslvalid: String + "If server allow new users to register or not from the nodeinfo" + signup: Boolean + "All total users on the server from the nodeinfo" + total_users: Int + "All total users last 6 months on the server from the nodeinfo" + active_users_halfyear: Int + "All total users last 1 month on the server from the nodeinfo" + active_users_monthly: Int + "All posts from the server from the nodeinfo" + local_posts: Int + "Percent server up from our checks" + uptime_alltime: String + "Current status of the server. 1 = UP. More at https://gitlab.com/diasporg/poduptime/-/blob/master/lib/PodStatus.php" + status: Int + "Latency connecting to the server" + latency: Int + "If you support XMPP from the nodeinfo" + service_xmpp: Boolean + "Json string of the services the server supports from the nodeinfo" + services: String + "Json string of the protocols the server supports from the nodeinfo" + protocols: String + "When the SSL certificate expires" + sslexpire: String + "DNSSEC Signature True or False" + dnssec: Boolean + "Type of Webserver" + servertype: String + "All comments on the server from the nodeinfo" + comment_counts: Int + "How the server is weighed in the table and search results" + weight: Int + "Date this data was updated" + date_updated: String + "Date last checked your server" + date_laststats: String + "Date added your server" + date_created: String + "Date server score hit 0" + date_diedoff: String + "Date server language was checked" + date_lastlanguage: String +} + +type Nodes { + "A unique idenifier the database creates" + id: Int! + "The name in the nodeinfo" + name: String + "The HTML Title tag of the server homepage" + metatitle: String + "The HTML Description tag of the server homepage" + metadescription: String + "The full json of the nodeinfo" + metanodeinfo: String + "The full json of the IP data from the IP" + metalocation: String + "The owner defined in your nodeinfo" + owner: String + "The HTML Image tag of the server homepage" + metaimage: String + "The Onion address from your web server header Onion-Location" + onion: String + "The i2p address from your web server header X-I2P-Location" + i2p: String + "The link to the terms of service based on best known for your software (owner editable)" + terms: String + "The link to the privacy policy based on best known for your software (owner editable)" + pp: String + "The link to the support based on best known for your software (owner editable)" + support: String + "The camo json data string from the nodeinfo" + camo: String + "The zipcode found from the IP location data" + zipcode: String + "The domain of server" + domain: String + "A open text field for the public comments on the server" + podmin_statement: String + "The main code version from the git repository for the software" + masterversion: String + "The short human readable version from the nodeinfo" + shortversion: String + "The software name from the nodeinfo" + softwarename: String + "Days we have been checking the server" + daysmonitored: Int + "Months we have been checking your server" + monthsmonitored: Int + "The full version found on the server from the nodeinfo" + fullversion: String + "Score based on the checks of the server being online each time checked" + score: Int + "The IP from looking at the DNS records" + ip: String + "Detected language from your html title and description tags on the site" + detectedlanguage: String + "Detected country code from the IP data" + country: String + "Detected country name from the IP data" + countryname: String + "Detected city name from the IP data" + city: String + "Detected state name from the IP data" + state: String + "Detected lattitude from the IP data" + lat: String + "Detected longitude name from the IP data" + long: String + "The IPv6 Address from DNS records" + ipv6: String + "True or False that the SSL certificate valid" + sslvalid: String + "If server allow new users to register or not from the nodeinfo" + signup: Boolean + "All total users on the server from the nodeinfo" + total_users: Int + "All total users last 6 months on the server from the nodeinfo" + active_users_halfyear: Int + "All total users last 1 month on the server from the nodeinfo" + active_users_monthly: Int + "All posts from the server from the nodeinfo" + local_posts: Int + "Percent server up from our checks" + uptime_alltime: String + "Current status of the server. 1 = UP. More at https://gitlab.com/diasporg/poduptime/-/blob/master/lib/PodStatus.php" + status: Int + "Latency connecting to the server" + latency: Int + "If you support XMPP from the nodeinfo" + service_xmpp: Boolean + "Json string of the services the server supports from the nodeinfo" + services: String + "Json string of the protocols the server supports from the nodeinfo" + protocols: String + "When the SSL certificate expires" + sslexpire: String + "Type of Webserver" + servertype: String + "DNSSEC Signature True or False" + dnssec: Boolean + "All comments on the server from the nodeinfo" + comment_counts: Int + "How the server is weighed in the table and search results" + weight: Int + "Date this data was updated" + date_updated: String + "Date last checked your server" + date_laststats: String + "Date added your server" + date_created: String + "Date server score hit 0" + date_diedoff: String + "Date server language was checked" + date_lastlanguage: String +} + +type Click { + "A unique idenifier the database creates" + id: Int! + "The domain of server" + domain: String + "Clicks by a user to this domain" + manualclick: Int + "Auto sends from the go.php auto pick method" + autoclick: Int + "Datestamp of the click" + date_clicked: String +} + +type Check { + "A unique idenifier the database creates" + id: Int! + "The domain of server" + domain: String + "Was server online" + online: Boolean + "If error what was it" + error: String + "Latency from checking server" + latency: Int + "Total users in nodeinfo" + total_users: Int + "Active users in last 6 months from nodeinfo" + active_users_halfyear: Int + "Active users in last month from nodeinfo" + active_users_monthly: Int + "Total posts in nodeinfo" + local_posts: Int + "Total comments in nodeinfo" + comment_counts: Int + "Version code in human format" + shortversion: String + "Full version code" + version: String + "Date Checked" + date_checked: String +} + +type MonthlyStat { + "A unique idenifier the database creates" + id: Int! + "Software Name" + softwarename: String + "Total users for the Software" + total_users: Int + "Total active users for the Software Halfyear" + total_active_users_halfyear: Int + "Total active users for the Software Monthly" + total_active_users_monthly: Int + "Total posts for the Software" + total_posts: Int + "Total comments for the Software" + total_comments: Int + "Total servers for the Software" + total_pods: Int + "Total uptime for Software" + total_uptime: Int + "Date last updated the monthly stats, this is always represented as the first day of the month for these stats" + date_checked: String +} + +type DailyStat { + "A unique idenifier the database creates" + id: Int! + "Software Name" + softwarename: String + "Total users for the Software" + total_users: Int + "Total active users for the Software Halfyear" + total_active_users_halfyear: Int + "Total active users for the Software Monthly" + total_active_users_monthly: Int + "Total posts for the Software" + total_posts: Int + "Total comments for the Software" + total_comments: Int + "Total servers for the Software" + total_pods: Int + "Total uptime for Software" + total_uptime: Int + "Date last updated the monthly stats, this is always represented as the first day of the month for these stats" + date_checked: String +} + +type MasterVersion { + "A unique idenifier the database creates" + id: Int! + "Software name from the git repo" + software: String + "Version label from git repo" + version: String + "Last development branch update" + devlastcommit: String + "The date of the git release" + releasedate: String + "Date masterversions updated from git" + date_checked: String +} + +type Software { + "Name of software" + softwarename: String + count: Int +} + +type Query { + "A full list of all servers in the database." + nodes ("UP, DOWN, RECHECK, PAUSED, SYSTEM_DELETED, USER_DELETED" status: String, "Name of a federated software type, like diaspora or mastodon" softwarename: String): [Node] + "A single server by domain name" + node (domain: String): [Node] + "All auto and manual clicks to a server" + clicks (limit: Int, domain: String): [Click] + "All checks ran to servers" + checks (limit: Int, domain: String, "true, false" online: String): [Check] + "All monthlystats data" + monthlystats ("Name of a federated software type, like diaspora or mastodon or all" softwarename: String, "true, false - only the latest result" mostcurrent: String): [MonthlyStat] + "All dailystats data" + dailystats ("Name of a federated software type, like diaspora or mastodon or all" softwarename: String, "true, false - only the latest result" mostcurrent: String): [DailyStat] + "All git master code versions" + masterversions: [MasterVersion] + "All software types data" + softwares: [Software] +} diff --git a/tools/mkserverversions/src/main/kotlin/app/pachli/mkserverversions/Main.kt b/tools/mkserverversions/src/main/kotlin/app/pachli/mkserverversions/Main.kt new file mode 100644 index 000000000..c0f4086f2 --- /dev/null +++ b/tools/mkserverversions/src/main/kotlin/app/pachli/mkserverversions/Main.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2024 Pachli Association + * + * This file is a part of Pachli. + * + * 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. + * + * Pachli 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 Pachli; if not, + * see . + */ + +package app.pachli.mkserverversions + +import app.pachli.mkserverversions.fediverseobserver.ServerVersionsQuery +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import com.apollographql.apollo3.ApolloClient +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.UsageError +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.google.gson.GsonBuilder +import io.github.oshai.kotlinlogging.DelegatingKLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import java.nio.file.Path +import java.nio.file.Paths +import java.util.TreeMap +import java.util.TreeSet +import kotlin.io.path.Path +import kotlin.io.path.div +import kotlin.io.path.exists +import kotlinx.coroutines.runBlocking + +private val log = KotlinLogging.logger {} + +const val DEST_DIR = "core/network/src/test/resources" + +class App : CliktCommand(help = """Update server-versions.json5""") { + private val verbose by option("-n", "--verbose", help = "show additional information").flag() + + /** + * Returns the full path to the Pachli `.../core/network/src/test/resources` directory, + * starting from the given [start] directory, walking up the tree if it can't be found + * there. + * + * @return the path, or null if it's not a subtree of [start] or any of its parents. + */ + private fun findResourcePath(start: Path): Path? { + val suffix = Path(DEST_DIR) + + var prefix = start + var resourcePath: Path + do { + resourcePath = prefix / suffix + if (resourcePath.exists()) return resourcePath + prefix = prefix.parent + } while (prefix != prefix.root) + + return null + } + + override fun run() = runBlocking { + System.setProperty("file.encoding", "UTF8") + ((log as? DelegatingKLogger<*>)?.underlyingLogger as Logger).level = if (verbose) Level.INFO else Level.WARN + + val cwd = Paths.get("").toAbsolutePath() + log.info { "working directory: $cwd" } + + val resourcePath = findResourcePath(cwd) ?: throw UsageError("could not find $DEST_DIR in tree") + + val apolloClient = ApolloClient.Builder() + .serverUrl("https://api.fediverse.observer/") + .build() + + val response = apolloClient.query(ServerVersionsQuery()).execute() + + if (response.hasErrors()) { + response.errors?.forEach { + log.error { it } + } + return@runBlocking + } + + val serverVersionsPath = resourcePath / "server-versions.json5" + val w = serverVersionsPath.toFile().printWriter() + w.println("// GENERATED BY \"runtools mkserverversions\", DO NOT HAND EDIT") + + // Use TreeMap and TreeSet for a consistent sort order in the output to + // minimise git diffs when the output is regenerated + val m = TreeMap>() + + response.data?.nodes.orEmpty().filterNotNull().forEach { + if (it.fullversion != null && it.softwarename != null) { + val versions = m.getOrPut(it.softwarename) { TreeSet() } + versions.add(it.fullversion) + } + } + + val gson = GsonBuilder().setPrettyPrinting().create() + w.print(gson.toJson(m)) + w.close() + } +} + +fun main(args: Array) = App().main(args)