1
0
Fork 0
mirror of https://codeberg.org/june64/mrvc.git synced 2026-01-10 16:06:33 +01:00
mrvc/main.go
June f939294723
improve and streamline config options and introduce command-line flags
Introduce command-line flags for config options with the added benefit
of getting a help output, expose more config options to be directly
configurable, prefix all environment variables with "MRVC", adjust
config option naming to better reflect their purpose and add validation
to ensure some config options got explicitly set.
2025-08-16 06:01:30 +02:00

476 lines
19 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"slices"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/hashicorp/go-version"
"github.com/matrix-org/gomatrixserverlib/fclient"
"github.com/matrix-org/gomatrixserverlib/spec"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
)
type HomeserverServerVersionInfo struct {
Homeserver string
ServerVersionInfo fclient.Version
}
type ServerPath struct {
MaxRoomVersion string
Server string
}
type VersionPath struct {
MaxRoomVersion string
Server string
Version string
}
var userIdFlag = flag.String("user-id", "", "The Matrix user ID to use. (EnvVar: MRVC_USER_ID)")
var tokenFlag = flag.String("token", "", "The Matrix access token to use. (EnvVar: MRVC_TOKEN)")
var roomFlag = flag.String("room", "", "The Matrix room to check. (EnvVar: MRVC_ROOM)")
var printHomeserverMemberCountFlag = flag.Bool("print-homeserver-member-count", false, "Print the member count for each homeserver. (EnvVar: MRVC_PRINT_HOMESERVER_MEMBER_COUNT)")
var homeserverVersionInfoTimeoutFlag = flag.Duration("homeserver-version-info-timeout", time.Second*5, "Timeout for getting the homeservers version information per homeserver. (EnvVar: MRVC_HOMESERVER_VERSION_INFO_TIMEOUT)")
var unknownServerVersionInfo = fclient.Version{
Server: struct {
Name string `json:"name"`
Version string `json:"version"`
}{
Name: "unknown",
Version: "unknown",
},
}
func getServerVersionInfoByHomeserver(homeservers []string, federationClient *fclient.Client) map[string](fclient.Version) {
homeserverChannel := make(chan string)
go func() {
for _, hs := range homeservers {
homeserverChannel <- hs
}
close(homeserverChannel)
}()
homeserverSVInfoChannel := make(chan HomeserverServerVersionInfo)
var wg sync.WaitGroup
const numHomeserverSVInfoReceivers = 100
wg.Add(numHomeserverSVInfoReceivers)
for i := 0; i < numHomeserverSVInfoReceivers; i++ {
go func() {
for hs := range homeserverChannel {
serverVersionInfo, err := federationClient.GetVersion(context.Background(), spec.ServerName(hs))
if err != nil {
serverVersionInfo = unknownServerVersionInfo
}
homeserverSVInfoChannel <- HomeserverServerVersionInfo{hs, serverVersionInfo}
}
wg.Done()
}()
}
go func() {
wg.Wait()
close(homeserverSVInfoChannel)
}()
serverVersionInfoByHomeserver := make(map[string](fclient.Version))
for homeserverSVInfo := range homeserverSVInfoChannel {
serverVersionInfoByHomeserver[homeserverSVInfo.Homeserver] = homeserverSVInfo.ServerVersionInfo
}
return serverVersionInfoByHomeserver
}
// Gets the maximum supported room version for the given server-version-pair.
func getMaxRoomVersion(serverVersionInfo fclient.Version) string {
// Use only the first part of the version string as sometimes there are suffixes after a space, like " (<commit>)", which then can't be parsed.
serverVersion, err := version.NewVersion(strings.Split(serverVersionInfo.Server.Version, " ")[0])
if err != nil {
return "unkown"
}
switch serverVersionInfo.Server.Name {
case "Conduit":
// https://conduit.rs/
// https://conduit.rs/changelog/
// https://gitlab.com/famedly/conduit
switch {
// https://conduit.rs/changelog/#v0-10-8-2025-08-11
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.10.8"))):
return "v12"
// https://conduit.rs/changelog/#v0-7-0-2024-04-25
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.7.0"))):
return "v11"
// https://conduit.rs/changelog/#v0-5-0-2022-12-21
// Includes MR 400:
// https://gitlab.com/famedly/conduit/-/merge_requests/400/commits
// MR 400 includes a commit, which specifies v10 as stable.
// https://gitlab.com/famedly/conduit/-/commit/1e1a144dfa98429ef9f02d16045796b73013830d
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.5.0"))):
return "v10"
// https://conduit.rs/changelog/#v0-4-0-2022-06-23
// Includes MR 257:
// https://gitlab.com/famedly/conduit/-/merge_requests/257/commits
// MR 257 includes a commit, which specifies added support v7 to v9.
// https://gitlab.com/famedly/conduit/-/commit/0ae39807a478370a769217d01fa33514299a2b35
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.4.0"))):
return "v9"
// https://conduit.rs/changelog/#v0-2-0-2021-09-01
default:
return "v6"
}
case "conduwuit", "Conduwuit":
// Using continuwuitys source (history) as a reference:
// https://forgejo.ellis.link/continuwuation/continuwuity
// History from first commit on "continuwuity":
// https://forgejo.ellis.link/continuwuation/continuwuity/commits/commit/e054a56b3286a6fb3091bedd5261089435ed26d1
// Last commit on "conduwuit"/"Conduwuit":
// https://forgejo.ellis.link/continuwuation/continuwuity/src/commit/d8311a5ff672fdc4729d956af5e3af8646b0670d
// Last version was "0.5.0":
// https://forgejo.ellis.link/continuwuation/continuwuity/src/commit/d8311a5ff672fdc4729d956af5e3af8646b0670d/Cargo.toml
// Matching both "conduwuit" and "Conduwuit" as the capitalization got changed.
// https://forgejo.ellis.link/continuwuation/continuwuity/commit/5b5ccba64e3d36a9235f4e0d449f40d859046dad
switch {
// Since the version went from "0.7.0-<something>" to "0.3.0" and the last version was "0.5.0", give all versions greater than "0.7.0" a lower max room version.
// https://forgejo.ellis.link/continuwuation/continuwuity/commit/af0b81f5fb737d3550cf22fbcf8d8b8eb1947408
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.7.0"))):
return "v10"
// v11 support (shows there as included in tag "v0.3.0"):
// https://forgejo.ellis.link/continuwuation/continuwuity/commit/223f05c922ea6f3085d386f0758df550459a25c3
// Tag "v0.3.0":
// https://forgejo.ellis.link/continuwuation/continuwuity/src/tag/v0.3.0/src/service/globals/mod.rs#L130
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.3.0"))):
return "v11"
// First version named "Conduwuit"/"conduwuit":
// https://forgejo.ellis.link/continuwuation/continuwuity/commit/e71855cd0b59d9274e8528a4c1ee480f99006c3c
// v10 support at first version named "Conduwuit"/"conduwuit":
// https://forgejo.ellis.link/continuwuation/continuwuity/src/commit/e71855cd0b59d9274e8528a4c1ee480f99006c3c/src/service/globals/mod.rs#L187
// And shows first version as "0.7.0-alpha+conduwuit-0.1.3":
// https://forgejo.ellis.link/continuwuation/continuwuity/src/commit/e71855cd0b59d9274e8528a4c1ee480f99006c3c/Cargo.toml
// Because of this versioning and the match on "0.7.0" before, this default should never match, but it is included for clarity.
default:
return "v10"
}
case "continuwuity":
// https://continuwuity.org/
// https://forgejo.ellis.link/continuwuation/continuwuity
// First version named "continuwuity":
// https://forgejo.ellis.link/continuwuation/continuwuity/commit/dc599db19c48ad3cbae15fc419c4a531d217ed05
// v11 support at first version named "continuwuity":
// https://forgejo.ellis.link/continuwuation/continuwuity/src/commit/dc599db19c48ad3cbae15fc419c4a531d217ed05/src/core/info/room_version.rs
return "v11"
case "Dendrite":
// https://github.com/element-hq/dendrite
// https://element-hq.github.io/dendrite/
// https://github.com/element-hq/dendrite/blob/main/CHANGES.md
switch {
// https://github.com/element-hq/dendrite/blob/v0.15.0/CHANGES.md#dendrite-0150-2025-08-12
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.15.0"))):
return "v12"
// https://github.com/matrix-org/dendrite/blob/v0.13.3/CHANGES.md#dendrite-0133-2023-09-28
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.13.3"))):
return "v11"
// https://github.com/matrix-org/dendrite/blob/v0.8.7/CHANGES.md#dendrite-087-2022-06-01
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.8.7"))):
return "v10"
// https://github.com/matrix-org/dendrite/blob/v0.8.6/CHANGES.md#dendrite-086-2022-05-26
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.8.6"))):
return "v9"
// https://github.com/matrix-org/dendrite/blob/v0.8.6/CHANGES.md#dendrite-086-2022-05-26
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.8.6"))):
return "v8"
// https://github.com/matrix-org/dendrite/blob/v0.4.1/CHANGES.md#dendrite-041-2021-07-26
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.4.1"))):
return "v7"
// https://github.com/matrix-org/dendrite/blob/v0.1.0/CHANGES.md#dendrite-010-2020-10-08
default:
return "v6"
}
case "Synapse":
// https://github.com/element-hq/synapse/
// https://element-hq.github.io/synapse/latest/
// https://github.com/element-hq/synapse/blob/develop/CHANGES.md
// https://github.com/element-hq/synapse/blob/develop/docs/changelogs/
switch {
// https://github.com/element-hq/synapse/blob/v1.135.1/CHANGES.md#synapse-11351-2025-08-11
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("1.135.1"))):
return "v12"
// https://github.com/element-hq/synapse/blob/v1.89.0rc1/CHANGES.md#synapse-1890rc1-2023-07-25
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("1.89.0rc1"))):
return "v11"
// https://github.com/element-hq/synapse/blob/v1.64.0rc1/CHANGES.md#synapse-1640rc1-2022-07-26
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("1.64.0rc1"))):
return "v10"
// https://github.com/element-hq/synapse/blob/v1.42.0rc2/CHANGES.md#synapse-1420rc2-2021-09-06
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("1.42.0rc2"))):
return "v9"
// https://github.com/element-hq/synapse/blob/v1.40.0rc3/CHANGES.md#synapse-1400rc3-2021-08-09
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("1.40.0rc3"))):
return "v8"
// https://github.com/element-hq/synapse/blob/v1.37.0rc1/CHANGES.md#synapse-1370rc1-2021-06-24
// https://github.com/matrix-org/synapse/pull/10167
// https://github.com/matrix-org/synapse/pull/10167/files#diff-8e36e2fc4dec5d84357ee3d140ad223b66fb36e630c25bbb45382ca59cdb0f40
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("1.37.0rc1"))):
return "v7"
// https://github.com/element-hq/synapse/blob/v1.14.0rc1/CHANGES.md#synapse-1140rc1-2020-05-26
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("1.14.0rc1"))):
return "v6"
// https://github.com/element-hq/synapse/blob/v1.0.0rc1/CHANGES.md#synapse-100rc1-2019-06-07
// https://github.com/matrix-org/synapse/pull/5354
// https://github.com/matrix-org/synapse/pull/5354/files#diff-8e36e2fc4dec5d84357ee3d140ad223b66fb36e630c25bbb45382ca59cdb0f40
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("1.0.0rc1"))):
return "v5"
// https://github.com/element-hq/synapse/blob/v0.99.5rc1/CHANGES.md#synapse-0995rc1-2019-05-21
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.99.5rc1"))):
return "v4"
// https://github.com/element-hq/synapse/blob/v0.99.0rc1/CHANGES.md#synapse-0990rc1-2019-01-30
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.99.0rc1"))):
return "v3"
// https://github.com/element-hq/synapse/blob/v0.34.1rc1/CHANGES.md#synapse-0341rc1-2019-01-08
// https://github.com/matrix-org/synapse/pull/4307
// https://github.com/matrix-org/synapse/pull/4307/files#diff-32e0f096397b0e3545054c51c42b5be4f0db953083b60d76a047cb1618f6b877
case serverVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.34.1rc1"))):
return "v2"
default:
return "v1"
}
case "Tuwunel":
// https://github.com/matrix-construct/tuwunel
// First version named "Tuwunel":
// https://github.com/matrix-construct/tuwunel/commit/9b658d86b2f49d01ba8d2f5ae7f263a104e8f282#diff-d58f31e506570d0bcb7629251ec1b350c5dde4a205a338349f154f3fd3187e93
// v11 support at first version named "Tuwunel":
// https://github.com/matrix-construct/tuwunel/blob/9b658d86b2f49d01ba8d2f5ae7f263a104e8f282/src/core/info/room_version.rs
return "v11"
default:
return "unkown"
}
}
func compareVersionStrings(a, b string) int {
// Try to parse a and b as versions.
// Use only the first part of the version string as sometimes there are suffixes after a space, like " (<commit>)", which then can't be parsed.
aStart := strings.Split(a, " ")[0]
bStart := strings.Split(b, " ")[0]
aVersion, aErr := version.NewVersion(aStart)
bVersion, bErr := version.NewVersion(bStart)
// An input, which can't be parsed as a version, should be interpreted as being smaller than an input, which can be parsed as a version.
switch {
case aErr != nil && bErr == nil:
return -1
case aErr == nil && bErr != nil:
return 1
case aErr != nil && bErr != nil:
return strings.Compare(a, b)
}
if cmpResult := aVersion.Compare(bVersion); cmpResult != 0 {
return cmpResult
} else {
// When the versions are equal, look at the potential suffixes in the version string.
aEnd := strings.TrimPrefix(a, aStart)
bEnd := strings.TrimPrefix(b, bStart)
return strings.Compare(aEnd, bEnd)
}
}
func main() {
flag.Parse()
// Configuration variables.
var userIdString, token, room string
var printHomeserverMemberCount bool
var homeserverVersionInfoTimeout time.Duration
visitedFlags := make(map[string]bool)
flag.Visit(func(visitedFlag *flag.Flag) {
visitedFlags[visitedFlag.Name] = true
})
// Assign flag and environment variable values to configuration variables.
// Flags take precedence over environment variables.
// This also ensures some configuration options got explicitly set.
if visitedFlags["user-id"] {
userIdString = *userIdFlag
} else if userIdEnvVar, ok := os.LookupEnv("MRVC_USER_ID"); ok {
userIdString = userIdEnvVar
} else {
log.Fatal("A Matrix user ID must be provided.")
}
if visitedFlags["token"] {
token = *tokenFlag
} else if tokenEnvVar, ok := os.LookupEnv("MRVC_TOKEN"); ok {
token = tokenEnvVar
} else {
log.Fatal("A Matrix access token must be provided.")
}
if visitedFlags["room"] {
room = *roomFlag
} else if roomEnvVar, ok := os.LookupEnv("MRVC_ROOM"); ok {
room = roomEnvVar
} else {
log.Fatal("A Matrix room must be provided.")
}
if visitedFlags["print-homeserver-member-count"] {
printHomeserverMemberCount = *printHomeserverMemberCountFlag
} else if printHomeserverMemberCountEnvVar, ok := os.LookupEnv("MRVC_PRINT_HOMESERVER_MEMBER_COUNT"); ok {
parsedPrintHomeserverMemberCountEnvVar, err := strconv.ParseBool(printHomeserverMemberCountEnvVar)
if err != nil {
log.Println("Error parsing MRVC_PRINT_HOMESERVER_MEMBER_COUNT:")
log.Fatal(err)
}
printHomeserverMemberCount = parsedPrintHomeserverMemberCountEnvVar
} else {
// The flag holds the default value.
printHomeserverMemberCount = *printHomeserverMemberCountFlag
}
if visitedFlags["homeserver-version-info-timeout"] {
homeserverVersionInfoTimeout = *homeserverVersionInfoTimeoutFlag
} else if homeserverVersionInfoTimeoutEnvVar, ok := os.LookupEnv("MRVC_HOMESERVER_VERSION_INFO_TIMEOUT"); ok {
parsedHomeserverVersionInfoTimeoutEnvVar, err := time.ParseDuration(homeserverVersionInfoTimeoutEnvVar)
if err != nil {
log.Println("Error parsing MRVC_HOMESERVER_VERSION_INFO_TIMEOUT:")
log.Fatal(err)
}
homeserverVersionInfoTimeout = parsedHomeserverVersionInfoTimeoutEnvVar
} else {
// The flag holds the default value.
homeserverVersionInfoTimeout = *homeserverVersionInfoTimeoutFlag
}
userId := id.UserID(userIdString)
_, homeserver, err := userId.ParseAndValidate()
if err != nil {
log.Fatal(err)
}
client, err := mautrix.NewClient(
homeserver,
userId,
token,
)
if err != nil {
log.Fatal(err)
}
federationClient := fclient.NewClient(
fclient.WithWellKnownSRVLookups(true),
fclient.WithTimeout(homeserverVersionInfoTimeout),
)
joinedMembers, err := client.JoinedMembers(context.Background(), id.RoomID(room))
if err != nil {
log.Fatal(err)
}
membersByHomeservers := make(map[string]uint)
for key := range joinedMembers.Joined {
membersByHomeservers[key.Homeserver()]++
}
homeservers := make([]string, 0, len(membersByHomeservers))
for hs := range membersByHomeservers {
homeservers = append(homeservers, hs)
}
serverVersionInfoByHomeserver := getServerVersionInfoByHomeserver(homeservers, federationClient)
// Info map tree.
maxRoomVersionInfo := make(map[string](map[string](map[string](map[string]uint))))
// Member counters.
membersByMaxRoomVersion := make(map[string]uint)
membersByServerPath := make(map[ServerPath]uint)
membersByVersionPath := make(map[VersionPath]uint)
for key, value := range membersByHomeservers {
serverVersionInfo := serverVersionInfoByHomeserver[key]
maxRoomVersion := getMaxRoomVersion(serverVersionInfo)
// Add to counters.
membersByMaxRoomVersion[maxRoomVersion] += value
membersByServerPath[ServerPath{maxRoomVersion, serverVersionInfo.Server.Name}] += value
membersByVersionPath[VersionPath{maxRoomVersion, serverVersionInfo.Server.Name, serverVersionInfo.Server.Version}] += value
// Sort into maxRoomVersionInfo map tree.
serverMap, ok := maxRoomVersionInfo[maxRoomVersion]
if !ok {
serverMap = make(map[string](map[string](map[string]uint)))
maxRoomVersionInfo[maxRoomVersion] = serverMap
}
versionMap, ok := serverMap[serverVersionInfo.Server.Name]
if !ok {
versionMap = make(map[string](map[string]uint))
serverMap[serverVersionInfo.Server.Name] = versionMap
}
homeserverMap, ok := versionMap[serverVersionInfo.Server.Version]
if !ok {
homeserverMap = make(map[string]uint)
versionMap[serverVersionInfo.Server.Version] = homeserverMap
}
homeserverMap[key] = value
}
fmt.Println("Room:")
fmt.Printf("%s -> %d\n\n", room, len(joinedMembers.Joined))
fmt.Println("Version Support:")
maxRoomVersionKeys := make([]string, 0, len(maxRoomVersionInfo))
for key := range maxRoomVersionInfo {
maxRoomVersionKeys = append(maxRoomVersionKeys, key)
}
slices.SortFunc(maxRoomVersionKeys, compareVersionStrings)
for _, maxRoomVersionKey := range maxRoomVersionKeys {
maxRoomVersionValue := maxRoomVersionInfo[maxRoomVersionKey]
fmt.Printf("%s -> %d\n", maxRoomVersionKey, membersByMaxRoomVersion[maxRoomVersionKey])
serverKeys := make([]string, 0, len(maxRoomVersionValue))
for key := range maxRoomVersionValue {
serverKeys = append(serverKeys, key)
}
sort.Strings(serverKeys)
for _, serverKey := range serverKeys {
serverValue := maxRoomVersionValue[serverKey]
fmt.Printf(" %s -> %d\n", serverKey, membersByServerPath[ServerPath{maxRoomVersionKey, serverKey}])
versionKeys := make([]string, 0, len(serverValue))
for key := range serverValue {
versionKeys = append(versionKeys, key)
}
slices.SortFunc(versionKeys, compareVersionStrings)
for _, versionKey := range versionKeys {
versionValue := serverValue[versionKey]
fmt.Printf(" %s -> %d\n", versionKey, membersByVersionPath[VersionPath{maxRoomVersionKey, serverKey, versionKey}])
if printHomeserverMemberCount {
homeserverKeys := make([]string, 0, len(versionValue))
for key := range versionValue {
homeserverKeys = append(homeserverKeys, key)
}
sort.Strings(homeserverKeys)
for _, homeserverKey := range homeserverKeys {
homeserverValue := versionValue[homeserverKey]
fmt.Printf(" %s -> %d\n", homeserverKey, homeserverValue)
}
}
}
}
}
}