diff --git a/LibMatrix/Helpers/RoomBuilder.cs b/LibMatrix/Helpers/RoomBuilder.cs
index 601f001..6dfb056 100644
--- a/LibMatrix/Helpers/RoomBuilder.cs
+++ b/LibMatrix/Helpers/RoomBuilder.cs
@@ -4,11 +4,13 @@ using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Homeservers;
using LibMatrix.Responses;
using LibMatrix.RoomTypes;
+using LibMatrix.StructuredData;
namespace LibMatrix.Helpers;
public class RoomBuilder {
private static readonly string[] V12PlusRoomVersions = ["org.matrix.hydra.11", "12"];
+ public bool SynapseAdminAutoAcceptLocalInvites { get; set; }
public string? Type { get; set; }
public string Version { get; set; } = "11";
public RoomNameEventContent Name { get; set; } = new();
@@ -130,6 +132,8 @@ public class RoomBuilder {
AdditionalCreators.RemoveAll(string.IsNullOrWhiteSpace);
if (V12PlusRoomVersions.Contains(Version) && AdditionalCreators is { Count: > 0 }) {
crq.CreationContent.Add("additional_creators", AdditionalCreators);
+ foreach (var user in AdditionalCreators)
+ PowerLevels.Users?.Remove(user);
}
foreach (var kvp in AdditionalCreationContent) {
@@ -150,6 +154,23 @@ public class RoomBuilder {
private async Task SendInvites(GenericRoom room) {
if (Invites.Count == 0) return;
+ if (SynapseAdminAutoAcceptLocalInvites && room.Homeserver is AuthenticatedHomeserverSynapse synapse) {
+ var localJoinTasks = Invites.Where(u => UserId.Parse(u.Key).ServerName == synapse.ServerName).Select(async entry => {
+ var user = entry.Key;
+ var reason = entry.Value;
+ try {
+ var uhs = await synapse.Admin.GetHomeserverForUserAsync(user, TimeSpan.FromHours(1));
+ var userRoom = uhs.GetRoom(room.RoomId);
+ await userRoom.JoinAsync([uhs.ServerName], reason);
+ await uhs.Logout();
+ }
+ catch (MatrixException e) {
+ Console.WriteLine("Failed to auto-accept invite for {0} in {1}: {2}", user, room.RoomId, e.Message);
+ }
+ }).ToList();
+ await Task.WhenAll(localJoinTasks);
+ }
+
var inviteTasks = Invites.Select(async kvp => {
try {
await room.InviteUserAsync(kvp.Key, kvp.Value);
@@ -163,11 +184,17 @@ public class RoomBuilder {
}
private async Task SetStatesAsync(GenericRoom room, List<StateEvent> state) {
- foreach (var ev in state) {
- await (string.IsNullOrWhiteSpace(ev.StateKey)
- ? room.SendStateEventAsync(ev.Type, ev.RawContent)
- : room.SendStateEventAsync(ev.Type, ev.StateKey, ev.RawContent));
- }
+ // foreach (var ev in state) {
+ // await (string.IsNullOrWhiteSpace(ev.StateKey)
+ // ? room.SendStateEventAsync(ev.Type, ev.RawContent)
+ // : room.SendStateEventAsync(ev.Type, ev.StateKey, ev.RawContent));
+ // }
+
+ foreach (var group in state.Chunk(100))
+ await room.BulkSendEventsAsync(group);
+
+ // var tasks = state.Chunk(50).Select(room.BulkSendEventsAsync).ToList();
+ // await Task.WhenAll(tasks);
}
private async Task SetBasicRoomInfoAsync(GenericRoom room) {
diff --git a/LibMatrix/Helpers/RoomUpgradeBuilder.cs b/LibMatrix/Helpers/RoomUpgradeBuilder.cs
index f9ce62b..8eaf9b8 100644
--- a/LibMatrix/Helpers/RoomUpgradeBuilder.cs
+++ b/LibMatrix/Helpers/RoomUpgradeBuilder.cs
@@ -1,11 +1,14 @@
using System.Diagnostics;
+using System.Reflection;
using System.Text.Json.Serialization;
using ArcaneLibs;
+using LibMatrix.EventTypes;
using LibMatrix.EventTypes.Spec;
using LibMatrix.EventTypes.Spec.State.Policy;
using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Homeservers;
using LibMatrix.RoomTypes;
+using LibMatrix.StructuredData;
namespace LibMatrix.Helpers;
@@ -15,11 +18,13 @@ public class RoomUpgradeBuilder : RoomBuilder {
public bool CanUpgrade { get; private set; }
public Dictionary<string, object> AdditionalTombstoneContent { get; set; } = new();
+ private List<Type> basePolicyTypes = [];
+
public async Task ImportAsync(GenericRoom OldRoom) {
var sw = Stopwatch.StartNew();
var total = 0;
- var basePolicyTypes = ClassCollector<PolicyRuleEventContent>.ResolveFromAllAccessibleAssemblies().ToList();
+ basePolicyTypes = ClassCollector<PolicyRuleEventContent>.ResolveFromAllAccessibleAssemblies().ToList();
Console.WriteLine($"Found {basePolicyTypes.Count} policy types in {sw.ElapsedMilliseconds}ms");
CanUpgrade = (
(await OldRoom.GetPowerLevelsAsync())?.UserHasStatePermission(OldRoom.Homeserver.UserId, RoomTombstoneEventContent.EventId)
@@ -39,7 +44,7 @@ public class RoomUpgradeBuilder : RoomBuilder {
if (evt.StateKey == "") {
if (evt.Type == RoomCreateEventContent.EventId)
foreach (var (key, value) in evt.RawContent) {
- if (key == "version") continue;
+ if (key is "room_version" or "creator") continue;
if (key == "type")
Type = value!.GetValue<string>();
else AdditionalCreationContent[key] = value;
@@ -80,8 +85,11 @@ public class RoomUpgradeBuilder : RoomBuilder {
});
}
else if (evt.Type == RoomMemberEventContent.EventId) {
- if (UpgradeOptions.InviteMembers && evt.TypedContent is RoomMemberEventContent { Membership: "join" or "invite" } invitedMember) {
- Invites.TryAdd(evt.StateKey!, invitedMember.Reason ?? "Room upgrade");
+ if (evt.TypedContent is RoomMemberEventContent { Membership: "join" or "invite" } invitedMember) {
+ if (UpgradeOptions.InviteMembers)
+ Invites.TryAdd(evt.StateKey!, invitedMember.Reason ?? "Room upgrade");
+ else if (UpgradeOptions.InviteLocalMembers && UserId.Parse(evt.StateKey!).ServerName == OldRoom.Homeserver.ServerName)
+ Invites.TryAdd(evt.StateKey!, invitedMember.Reason ?? "Room upgrade (local user)");
}
else if (UpgradeOptions.MigrateBans && evt.TypedContent is RoomMemberEventContent { Membership: "ban" } bannedMember)
Bans.TryAdd(evt.StateKey!, bannedMember.Reason);
@@ -100,6 +108,19 @@ public class RoomUpgradeBuilder : RoomBuilder {
}
private StateEventResponse UpgradeUnstableValues(StateEventResponse evt) {
+ if (evt.IsLegacyType) {
+ var oldType = evt.Type;
+ evt.Type = evt.MappedType.GetCustomAttributes<MatrixEventAttribute>().FirstOrDefault(x => !x.Legacy)!.EventName;
+ Console.WriteLine($"Upgraded event type from {oldType} to {evt.Type} for event {evt.EventId}");
+ }
+
+ if (evt.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) {
+ if (evt.RawContent["recommendation"]?.GetValue<string>() == "org.matrix.mjolnir.ban") {
+ evt.RawContent["recommendation"] = "m.ban";
+ Console.WriteLine($"Upgraded recommendation from 'org.matrix.mjolnir.ban' to 'm.ban' for event {evt.EventId}");
+ }
+ }
+
return evt;
}
@@ -158,6 +179,7 @@ public class RoomUpgradeBuilder : RoomBuilder {
public class RoomUpgradeOptions {
public bool InviteMembers { get; set; }
+ public bool InviteLocalMembers { get; set; }
public bool InvitePowerlevelUsers { get; set; }
public bool MigrateBans { get; set; }
public bool MigrateEmptyStateEvents { get; set; }
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index e2f52cd..47e7039 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -14,6 +14,7 @@ using LibMatrix.Homeservers.Extensions.NamedCaches;
using LibMatrix.Responses;
using LibMatrix.RoomTypes;
using LibMatrix.Services;
+using LibMatrix.StructuredData;
using LibMatrix.Utilities;
namespace LibMatrix.Homeservers;
diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
index cee3d8d..4da0013 100644
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
@@ -16,6 +16,7 @@ using LibMatrix.Filters;
using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters;
using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses;
using LibMatrix.Responses;
+using LibMatrix.StructuredData;
namespace LibMatrix.Homeservers.ImplementationDetails.Synapse;
diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index f0b35f9..3e41075 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -3,6 +3,7 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Web;
+using ArcaneLibs.Collections;
using ArcaneLibs.Extensions;
using LibMatrix.Extensions;
using LibMatrix.Responses;
@@ -28,7 +29,8 @@ public class RemoteHomeserver {
Auth = new(this);
}
- private Dictionary<string, object> _profileCache { get; set; } = new();
+ // private Dictionary<string, object> _profileCache { get; set; } = new();
+ private SemaphoreCache<UserProfileResponse> _profileCache { get; set; } = new();
public string ServerNameOrUrl { get; }
public string? Proxy { get; }
@@ -40,27 +42,12 @@ public class RemoteHomeserver {
public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; }
- public async Task<UserProfileResponse> GetProfileAsync(string mxid, bool useCache = false) {
- if (mxid is null) throw new ArgumentNullException(nameof(mxid));
- if (useCache && _profileCache.TryGetValue(mxid, out var value)) {
- if (value is SemaphoreSlim s) await s.WaitAsync();
- if (value is UserProfileResponse p) return p;
- }
-
- _profileCache[mxid] = new SemaphoreSlim(1);
-
- var resp = await ClientHttpClient.GetAsync($"/_matrix/client/v3/profile/{HttpUtility.UrlEncode(mxid)}");
- var data = await resp.Content.ReadFromJsonAsync<UserProfileResponse>();
- if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data);
- _profileCache[mxid] = data ?? throw new InvalidOperationException($"Could not get profile for {mxid}");
-
- return data;
- }
-
// TODO: Do we need to support retrieving individual profile properties? Is there any use for that besides just getting the full profile?
+ public async Task<UserProfileResponse> GetProfileAsync(string mxid) =>
+ await ClientHttpClient.GetFromJsonAsync<UserProfileResponse>($"/_matrix/client/v3/profile/{HttpUtility.UrlEncode(mxid)}");
public async Task<ClientVersionsResponse> GetClientVersionsAsync() {
- var resp = await ClientHttpClient.GetAsync($"/_matrix/client/versions");
+ var resp = await ClientHttpClient.GetAsync("/_matrix/client/versions");
var data = await resp.Content.ReadFromJsonAsync<ClientVersionsResponse>();
if (!resp.IsSuccessStatusCode) Console.WriteLine("ClientVersions: " + data);
return data ?? throw new InvalidOperationException("ClientVersionsResponse is null");
@@ -74,13 +61,27 @@ public class RemoteHomeserver {
return data ?? throw new InvalidOperationException($"Could not resolve alias {alias}");
}
- public Task<PublicRoomDirectoryResult> GetPublicRoomsAsync(int limit = 100, string? server = null, string? since = null) =>
- ClientHttpClient.GetFromJsonAsync<PublicRoomDirectoryResult>(buildUriWithParams("/_matrix/client/v3/publicRooms", (nameof(limit), true, limit),
- (nameof(server), !string.IsNullOrWhiteSpace(server), server), (nameof(since), !string.IsNullOrWhiteSpace(since), since)));
+ public Task<PublicRoomDirectoryResult> GetPublicRoomsAsync(int limit = 100, string? server = null, string? since = null) {
+ var url = $"/_matrix/client/v3/publicRooms?limit={limit}";
+ if (!string.IsNullOrWhiteSpace(server)) {
+ url += $"&server={server}";
+ }
+
+ if (!string.IsNullOrWhiteSpace(since)) {
+ url += $"&since={since}";
+ }
+
+ return ClientHttpClient.GetFromJsonAsync<PublicRoomDirectoryResult>(url);
+ }
- // TODO: move this somewhere else
- private string buildUriWithParams(string url, params (string name, bool include, object? value)[] values) {
- return url + "?" + string.Join("&", values.Where(x => x.include));
+ public async IAsyncEnumerable<PublicRoomDirectoryResult> EnumeratePublicRoomsAsync(int limit = int.MaxValue, string? server = null, string? since = null, int chunkSize = 100) {
+ PublicRoomDirectoryResult res;
+ do {
+ res = await GetPublicRoomsAsync(chunkSize, server, since);
+ yield return res;
+ if (res.NextBatch is null || res.NextBatch == since || res.Chunk.Count == 0) break;
+ since = res.NextBatch;
+ } while (limit > 0 && limit-- > 0);
}
#region Authentication
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index 7d21d68..9753176 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -623,7 +623,7 @@ public class GenericRoom {
}
}
- public async Task BulkSendEventsAsync(IEnumerable<StateEventResponse> events) {
+ public async Task BulkSendEventsAsync(IEnumerable<StateEvent> events) {
if ((await Homeserver.GetCapabilitiesAsync()).Capabilities.BulkSendEvents?.Enabled == true)
await Homeserver.ClientHttpClient.PostAsJsonAsync(
$"/_matrix/client/unstable/gay.rory.bulk_send_events/rooms/{RoomId}/bulk_send_events?_libmatrix_txn_id={Guid.NewGuid()}", events);
@@ -638,7 +638,7 @@ public class GenericRoom {
}
}
- public async Task BulkSendEventsAsync(IAsyncEnumerable<StateEventResponse> events) {
+ public async Task BulkSendEventsAsync(IAsyncEnumerable<StateEvent> events) {
if ((await Homeserver.GetCapabilitiesAsync()).Capabilities.BulkSendEvents?.Enabled == true)
await Homeserver.ClientHttpClient.PostAsJsonAsync(
$"/_matrix/client/unstable/gay.rory.bulk_send_events/rooms/{RoomId}/bulk_send_events?_libmatrix_txn_id={Guid.NewGuid()}", events);
diff --git a/LibMatrix/MxcUri.cs b/LibMatrix/StructuredData/MxcUri.cs
index 875ae53..82a9677 100644
--- a/LibMatrix/MxcUri.cs
+++ b/LibMatrix/StructuredData/MxcUri.cs
@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
-namespace LibMatrix;
+namespace LibMatrix.StructuredData;
public class MxcUri {
public required string ServerName { get; set; }
diff --git a/LibMatrix/StructuredData/UserId.cs b/LibMatrix/StructuredData/UserId.cs
new file mode 100644
index 0000000..02b2e91
--- /dev/null
+++ b/LibMatrix/StructuredData/UserId.cs
@@ -0,0 +1,27 @@
+namespace LibMatrix.StructuredData;
+
+public class UserId {
+ public required string ServerName { get; set; }
+ public required string LocalPart { get; set; }
+
+ public static UserId Parse(string mxid) {
+ if (!mxid.StartsWith('@')) throw new ArgumentException("Matrix User IDs must start with '@'", nameof(mxid));
+ var parts = mxid.Split(':', 2);
+ if (parts.Length != 2) throw new ArgumentException($"Invalid MXID '{mxid}' passed! MXIDs must exist of only 2 parts!", nameof(mxid));
+ return new UserId {
+ LocalPart = parts[0][1..],
+ ServerName = parts[1]
+ };
+ }
+
+ public static implicit operator UserId(string mxid) => Parse(mxid);
+ public static implicit operator string(UserId mxid) => $"@{mxid.LocalPart}:{mxid.ServerName}";
+ public static implicit operator (string, string)(UserId mxid) => (mxid.LocalPart, mxid.ServerName);
+ public static implicit operator UserId((string localPart, string serverName) mxid) => (mxid.localPart, mxid.serverName);
+ // public override string ToString() => $"mxc://{ServerName}/{MediaId}";
+
+ public void Deconstruct(out string serverName, out string localPart) {
+ serverName = ServerName;
+ localPart = LocalPart;
+ }
+}
\ No newline at end of file
|