package main import ( "context" "fmt" "log" "os" "sort" "strings" "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 ServerPath struct { MaxRoomVersion string Server string } type VersionPath struct { MaxRoomVersion string Server string Version string } var unknownServerVersionInfo = fclient.Version{ Server: struct { Name string `json:"name"` Version string `json:"version"` }{ Name: "unknown", Version: "unknown", }, } // 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 " ()", 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-" 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 main() { userLocalpart := os.Getenv("MATRIX_USER_LOCALPART") homeserver := os.Getenv("MATRIX_HOMESERVER") token := os.Getenv("MATRIX_TOKEN") room := os.Getenv("MATRIX_ROOM") verbose := false client, err := mautrix.NewClient( homeserver, id.NewUserID(userLocalpart, homeserver), token, ) if err != nil { log.Fatal(err) } federationClient := fclient.NewClient( fclient.WithWellKnownSRVLookups(true), ) 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()]++ } // 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, err := federationClient.GetVersion(context.Background(), spec.ServerName(key)) if err != nil { serverVersionInfo = unknownServerVersionInfo } 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) } sort.Strings(maxRoomVersionKeys) 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) } sort.Strings(versionKeys) for _, versionKey := range versionKeys { versionValue := serverValue[versionKey] fmt.Printf(" %s -> %d\n", versionKey, membersByVersionPath[VersionPath{maxRoomVersionKey, serverKey, versionKey}]) if verbose { 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) } } } } } }