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
This commit is contained in:
parent
7e3cde4c65
commit
5cfe6d055b
|
@ -25,3 +25,8 @@ max_line_length = off
|
||||||
|
|
||||||
[*.{yml,yaml}]
|
[*.{yml,yaml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
# Disable ktlint on generated source code, see
|
||||||
|
# https://github.com/JLLeitschuh/ktlint-gradle/issues/746
|
||||||
|
[**/build/generated/source/**]
|
||||||
|
ktlint = disabled
|
||||||
|
|
|
@ -3534,7 +3534,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/app/pachli/fragment/SFragment.kt"
|
file="src/main/java/app/pachli/fragment/SFragment.kt"
|
||||||
line="239"
|
line="240"
|
||||||
column="48"/>
|
column="48"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,7 @@ abstract class SFragment : Fragment() {
|
||||||
result.onFailure {
|
result.onFailure {
|
||||||
val msg = getString(
|
val msg = getString(
|
||||||
R.string.server_repository_error,
|
R.string.server_repository_error,
|
||||||
|
accountManager.activeAccount!!.domain,
|
||||||
it.msg(requireContext()),
|
it.msg(requireContext()),
|
||||||
)
|
)
|
||||||
Timber.e(msg)
|
Timber.e(msg)
|
||||||
|
|
|
@ -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.MastodonApi
|
||||||
import app.pachli.core.network.retrofit.NodeInfoApi
|
import app.pachli.core.network.retrofit.NodeInfoApi
|
||||||
import app.pachli.network.ServerRepository.Error.Capabilities
|
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.GetNodeInfo
|
||||||
import app.pachli.network.ServerRepository.Error.GetWellKnownNodeInfo
|
import app.pachli.network.ServerRepository.Error.GetWellKnownNodeInfo
|
||||||
import app.pachli.network.ServerRepository.Error.UnsupportedSchema
|
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
|
* @return the server info or a [Server.Error] if the server info can not
|
||||||
* server can not be determined.
|
* be determined.
|
||||||
*/
|
*/
|
||||||
private suspend fun getServer(): Result<Server, Error> = binding {
|
private suspend fun getServer(): Result<Server, Error> = binding {
|
||||||
// Fetch the /.well-known/nodeinfo document
|
// Fetch the /.well-known/nodeinfo document
|
||||||
|
@ -107,7 +107,7 @@ class ServerRepository @Inject constructor(
|
||||||
{
|
{
|
||||||
mastodonApi.getInstanceV1().fold(
|
mastodonApi.getInstanceV1().fold(
|
||||||
{ Server.from(nodeInfo.software, it).mapError(::Capabilities) },
|
{ Server.from(nodeInfo.software, it).mapError(::Capabilities) },
|
||||||
{ Err(GetInstanceInfo(it)) },
|
{ Err(GetInstanceInfoV1(it)) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
).bind()
|
).bind()
|
||||||
|
@ -139,7 +139,7 @@ class ServerRepository @Inject constructor(
|
||||||
source = error,
|
source = error,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GetInstanceInfo(val throwable: Throwable) : Error(
|
data class GetInstanceInfoV1(val throwable: Throwable) : Error(
|
||||||
R.string.server_repository_error_get_instance_info,
|
R.string.server_repository_error_get_instance_info,
|
||||||
throwable.localizedMessage,
|
throwable.localizedMessage,
|
||||||
)
|
)
|
||||||
|
|
|
@ -766,7 +766,7 @@ Your description here:\n\n
|
||||||
----\n
|
----\n
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="server_repository_error">Could not fetch server info: %1$s</string>
|
<string name="server_repository_error">Could not fetch server info for %1$s: %2$s</string>
|
||||||
<string name="server_repository_error_get_well_known_node_info">fetching /.well-known/nodeinfo failed: %1$s</string>
|
<string name="server_repository_error_get_well_known_node_info">fetching /.well-known/nodeinfo failed: %1$s</string>
|
||||||
<string name="server_repository_error_unsupported_schema">/.well-known/nodeinfo did not contain understandable schemas</string>
|
<string name="server_repository_error_unsupported_schema">/.well-known/nodeinfo did not contain understandable schemas</string>
|
||||||
<string name="server_repository_error_get_node_info">fetching nodeinfo %1$s failed: %2$s</string>
|
<string name="server_repository_error_get_node_info">fetching nodeinfo %1$s failed: %2$s</string>
|
||||||
|
|
|
@ -19,12 +19,6 @@ package app.pachli.core.common
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.StringRes
|
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.
|
* Base class for errors throughout the app.
|
||||||
|
@ -83,46 +77,3 @@ open class PachliError(
|
||||||
return context.getString(resourceId, *args.toTypedArray())
|
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 <R> resultOf(block: () -> R): Result<R, Exception> {
|
|
||||||
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, R> T.resultOf(block: T.() -> R): Result<R, Exception> {
|
|
||||||
return try {
|
|
||||||
Ok(block())
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like [mapCatching], but uses [resultOf] instead of [runCatching].
|
|
||||||
*/
|
|
||||||
inline fun <R, T> Result<T, Exception>.mapResult(transform: (value: T) -> R): Result<R, Exception> {
|
|
||||||
val successResult = getOr { null } // getOrNull()
|
|
||||||
return when {
|
|
||||||
successResult != null -> resultOf { transform(successResult) }
|
|
||||||
else -> Err(getError() ?: error("Unreachable state"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,13 +18,21 @@
|
||||||
package app.pachli.core.network
|
package app.pachli.core.network
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
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.PachliError
|
||||||
import app.pachli.core.common.resultOf
|
|
||||||
import app.pachli.core.network.Server.Error.UnparseableVersion
|
import app.pachli.core.network.Server.Error.UnparseableVersion
|
||||||
import app.pachli.core.network.ServerKind.AKKOMA
|
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.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.MASTODON
|
||||||
|
import app.pachli.core.network.ServerKind.PIXELFED
|
||||||
import app.pachli.core.network.ServerKind.PLEROMA
|
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.ServerKind.UNKNOWN
|
||||||
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_CLIENT
|
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_CLIENT
|
||||||
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_SERVER
|
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.InstanceV1
|
||||||
import app.pachli.core.network.model.InstanceV2
|
import app.pachli.core.network.model.InstanceV2
|
||||||
import app.pachli.core.network.model.nodeinfo.NodeInfo
|
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.Result
|
||||||
|
import com.github.michaelbull.result.andThen
|
||||||
import com.github.michaelbull.result.binding
|
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.mapError
|
||||||
|
import com.github.michaelbull.result.recover
|
||||||
|
import com.github.michaelbull.result.toResultOr
|
||||||
import io.github.z4kn4fein.semver.Version
|
import io.github.z4kn4fein.semver.Version
|
||||||
import io.github.z4kn4fein.semver.constraints.Constraint
|
import io.github.z4kn4fein.semver.constraints.Constraint
|
||||||
import io.github.z4kn4fein.semver.satisfies
|
import io.github.z4kn4fein.semver.satisfies
|
||||||
import io.github.z4kn4fein.semver.toVersion
|
import io.github.z4kn4fein.semver.toVersion
|
||||||
|
import java.text.ParseException
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -97,7 +110,7 @@ data class Server(
|
||||||
* Constructs a server from its [NodeInfo] and [InstanceV2] details.
|
* Constructs a server from its [NodeInfo] and [InstanceV2] details.
|
||||||
*/
|
*/
|
||||||
fun from(software: NodeInfo.Software, instanceV2: InstanceV2): Result<Server, Error> = binding {
|
fun from(software: NodeInfo.Software, instanceV2: InstanceV2): Result<Server, Error> = binding {
|
||||||
val serverKind = ServerKind.from(software.name)
|
val serverKind = ServerKind.from(software)
|
||||||
val version = parseVersionString(serverKind, software.version).bind()
|
val version = parseVersionString(serverKind, software.version).bind()
|
||||||
val capabilities = capabilitiesFromServerVersion(serverKind, version)
|
val capabilities = capabilitiesFromServerVersion(serverKind, version)
|
||||||
|
|
||||||
|
@ -120,7 +133,7 @@ data class Server(
|
||||||
* Constructs a server from its [NodeInfo] and [InstanceV1] details.
|
* Constructs a server from its [NodeInfo] and [InstanceV1] details.
|
||||||
*/
|
*/
|
||||||
fun from(software: NodeInfo.Software, instanceV1: InstanceV1): Result<Server, Error> = binding {
|
fun from(software: NodeInfo.Software, instanceV1: InstanceV1): Result<Server, Error> = binding {
|
||||||
val serverKind = ServerKind.from(software.name)
|
val serverKind = ServerKind.from(software)
|
||||||
val version = parseVersionString(serverKind, software.version).bind()
|
val version = parseVersionString(serverKind, software.version).bind()
|
||||||
val capabilities = capabilitiesFromServerVersion(serverKind, version)
|
val capabilities = capabilitiesFromServerVersion(serverKind, version)
|
||||||
|
|
||||||
|
@ -130,37 +143,108 @@ data class Server(
|
||||||
/**
|
/**
|
||||||
* Parse a [version] string from the given [serverKind] in to a [Version].
|
* Parse a [version] string from the given [serverKind] in to a [Version].
|
||||||
*/
|
*/
|
||||||
private fun parseVersionString(serverKind: ServerKind, version: String): Result<Version, UnparseableVersion> = binding {
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
// Real world examples of version strings from nodeinfo
|
fun parseVersionString(serverKind: ServerKind, version: String): Result<Version, Error> {
|
||||||
// pleroma - 2.6.50-875-g2eb5c453.service-origin+soapbox
|
val result = runSuspendCatching {
|
||||||
// akkoma - 3.9.3-0-gd83f5f66f-blob
|
Version.parse(version, strict = false)
|
||||||
// firefish - 1.1.0-dev29-hf1
|
}.mapError { UnparseableVersion(version, it) }
|
||||||
// hometown - 4.0.10+hometown-1.1.1
|
|
||||||
// cherrypick - 4.6.0+cs-8f0ba0f
|
|
||||||
// gotosocial - 0.13.1-SNAPSHOT git-dfc7656
|
|
||||||
|
|
||||||
val semver = when (serverKind) {
|
if (result is Ok) return result
|
||||||
// These servers have semver compatible versions
|
|
||||||
AKKOMA, MASTODON, PLEROMA, UNKNOWN -> {
|
return when (serverKind) {
|
||||||
resultOf { Version.parse(version, strict = false) }
|
// These servers should have semver compatible versions, but perhaps
|
||||||
.mapError { UnparseableVersion(version, it) }.bind()
|
// the server operator has changed them. Try looking for a matching
|
||||||
|
// <major>.<minor>.<patch> somewhere in the version string and hope
|
||||||
|
// it's correct
|
||||||
|
AKKOMA, FEDIBIRD, GLITCH, HOMETOWN, MASTODON, PIXELFED, UNKNOWN -> {
|
||||||
|
val rx = """(?<major>\d+)\.(?<minor>\d+).(?<patch>\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*(?<major>\d+)\.0*(?<minor>\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
|
// 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 -> {
|
GOTOSOCIAL -> {
|
||||||
// Try and parse as semver, just in case
|
// Failed, split on spaces and parse the first component
|
||||||
resultOf { Version.parse(version, strict = false) }
|
val components = version.split(" ", "_")
|
||||||
.getOrElse {
|
runSuspendCatching { Version.parse(components[0], strict = false) }
|
||||||
// Didn't parse, use the first component, fall back to 0.0.0
|
.recover { "0.0.0".toVersion() }
|
||||||
val components = version.split(" ")
|
}
|
||||||
resultOf { Version.parse(components[0], strict = false) }
|
|
||||||
.getOrElse { "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*(?<major>\d+)\.0*(?<minor>\d+)\.0*(?<patch>\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 (?<major>\d+)\.(?<minor>\d+)\.(?<patch>\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 = """^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\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
|
// Everything else. Assume server side filtering and no translation. This may be an
|
||||||
// incorrect assumption.
|
// incorrect assumption.
|
||||||
AKKOMA, PLEROMA, UNKNOWN -> {
|
AKKOMA, FEDIBIRD, FRIENDICA, GLITCH, HOMETOWN, ICESHRIMP, PIXELFED, PLEROMA, SHARKEY, UNKNOWN -> {
|
||||||
c[ORG_JOINMASTODON_FILTERS_SERVER] = "1.0.0".toVersion()
|
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 {
|
enum class ServerKind {
|
||||||
AKKOMA,
|
AKKOMA,
|
||||||
|
FEDIBIRD,
|
||||||
|
FRIENDICA,
|
||||||
|
GLITCH,
|
||||||
GOTOSOCIAL,
|
GOTOSOCIAL,
|
||||||
|
HOMETOWN,
|
||||||
|
ICESHRIMP,
|
||||||
MASTODON,
|
MASTODON,
|
||||||
PLEROMA,
|
PLEROMA,
|
||||||
|
PIXELFED,
|
||||||
|
SHARKEY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catch-all for servers we don't recognise but that responded to either
|
||||||
|
* /api/v1/instance or /api/v2/instance
|
||||||
|
*/
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(s: String) = when (s.lowercase()) {
|
fun from(s: NodeInfo.Software) = when (s.name.lowercase()) {
|
||||||
"akkoma" -> AKKOMA
|
"akkoma" -> AKKOMA
|
||||||
|
"fedibird" -> FEDIBIRD
|
||||||
|
"friendica" -> FRIENDICA
|
||||||
"gotosocial" -> GOTOSOCIAL
|
"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
|
"pleroma" -> PLEROMA
|
||||||
|
"sharkey" -> SHARKEY
|
||||||
else -> UNKNOWN
|
else -> UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package app.pachli.core.network
|
package app.pachli.core.network
|
||||||
|
|
||||||
import app.pachli.core.network.ServerKind.AKKOMA
|
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.GOTOSOCIAL
|
||||||
import app.pachli.core.network.ServerKind.MASTODON
|
import app.pachli.core.network.ServerKind.MASTODON
|
||||||
import app.pachli.core.network.ServerKind.PLEROMA
|
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.Ok
|
||||||
import com.github.michaelbull.result.Result
|
import com.github.michaelbull.result.Result
|
||||||
import com.google.common.truth.Truth.assertWithMessage
|
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 io.github.z4kn4fein.semver.toVersion
|
||||||
|
import java.io.BufferedReader
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.Parameterized
|
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)
|
.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<Map<String, Set<String>>> =
|
||||||
|
object : TypeToken<Map<String, Set<String>>>() {}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -47,4 +47,5 @@ include(":core:navigation")
|
||||||
include(":core:network")
|
include(":core:network")
|
||||||
include(":core:testing")
|
include(":core:testing")
|
||||||
include(":tools:mklanguages")
|
include(":tools:mklanguages")
|
||||||
|
include(":tools:mkserverversions")
|
||||||
include(":checks")
|
include(":checks")
|
||||||
|
|
|
@ -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.
|
|
@ -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 <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
versionCatalogs {
|
||||||
|
create("libs") {
|
||||||
|
from(files("../../gradle/libs.versions.toml"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "mkserverversions"
|
|
@ -0,0 +1,6 @@
|
||||||
|
query ServerVersions {
|
||||||
|
nodes(status: "UP") {
|
||||||
|
softwarename
|
||||||
|
fullversion
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String, MutableSet<String>>()
|
||||||
|
|
||||||
|
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<String>) = App().main(args)
|
Loading…
Reference in New Issue