diff options
Diffstat (limited to '')
25 files changed, 381 insertions, 167 deletions
diff --git a/ArcaneLibs b/ArcaneLibs -Subproject b7685c786b29e7f8ae2db6ff0f79a52efc57020 +Subproject 952f5ef673862fdb7ff6f51d032eca4577dab9c diff --git a/LibMatrix.EventTypes/EventContent.cs b/LibMatrix.EventTypes/EventContent.cs index c582cf2..a837252 100644 --- a/LibMatrix.EventTypes/EventContent.cs +++ b/LibMatrix.EventTypes/EventContent.cs @@ -1,10 +1,20 @@ +using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace LibMatrix.EventTypes; -public abstract class EventContent; +public abstract class EventContent { + public static List<string> GetMatchingEventTypes<T>() where T : EventContent { + var type = typeof(T); + var eventTypes = new List<string>(); + foreach (var attr in type.GetCustomAttributes<MatrixEventAttribute>(true)) { + eventTypes.Add(attr.EventName); + } + return eventTypes; + } +} public class UnknownEventContent : TimelineEventContent; @@ -37,6 +47,10 @@ public abstract class TimelineEventContent : EventContent { [JsonPropertyName("rel_type")] public string? RelationType { get; set; } + // used for reactions + [JsonPropertyName("key")] + public string? Key { get; set; } + public class EventInReplyTo { [JsonPropertyName("event_id")] public string? EventId { get; set; } diff --git a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs index ae893f8..9602bf3 100644 --- a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs +++ b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs @@ -29,6 +29,7 @@ public class RoomMessageEventContent : TimelineEventContent { [JsonPropertyName("url")] public string? Url { get; set; } + [JsonPropertyName("filename")] public string? FileName { get; set; } [JsonPropertyName("info")] diff --git a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs index 6006048..5bfd77b 100644 --- a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs @@ -1,5 +1,7 @@ +using System.Security.Cryptography; using System.Text.Json.Serialization; using ArcaneLibs.Attributes; +using ArcaneLibs.Extensions; namespace LibMatrix.EventTypes.Spec.State.Policy; @@ -79,6 +81,7 @@ public abstract class PolicyRuleEventContent : EventContent { /// </summary> [JsonPropertyName("gay.rory.matrix_room_utils.readable_expiry_time_utc")] [FriendlyName(Name = "Expires at")] + [TableHide] public DateTime? ExpiryDateTime { get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime; set { @@ -86,6 +89,8 @@ public abstract class PolicyRuleEventContent : EventContent { Expiry = ((DateTimeOffset)value).ToUnixTimeMilliseconds(); } } + + public string GetDraupnir2StateKey() => Convert.ToBase64String(SHA256.HashData($"{Entity}{Recommendation}".AsBytes().ToArray())); } public static class PolicyRecommendationTypes { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs index c619d0e..f26b8e5 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs @@ -15,8 +15,8 @@ public class RoomCreateEventContent : EventContent { [JsonPropertyName("m.federate")] public bool? Federate { get; set; } - [JsonPropertyName("predecessor")] - public RoomCreatePredecessor? Predecessor { get; set; } + // [JsonPropertyName("predecessor")] + // public RoomCreatePredecessor? Predecessor { get; set; } [JsonPropertyName("type")] public string? Type { get; set; } diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs index 49a1b62..eb156b3 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs @@ -57,12 +57,13 @@ public class RoomPowerLevelEventContent : EventContent { return Users.TryGetValue(userId, out var level) && level >= Events.GetValueOrDefault(eventType, EventsDefault ?? 0); } - public bool UserHasStatePermission(string userId, string eventType) { + public bool UserHasStatePermission(string userId, string eventType, bool log = false) { ArgumentNullException.ThrowIfNull(userId); var userLevel = GetUserPowerLevel(userId); var eventLevel = GetStateEventPowerLevel(eventType); - - Console.WriteLine($"{userId}={userLevel} >= {eventType}={eventLevel} = {userLevel >= eventLevel}"); + + if (log) + Console.WriteLine($"{userId}={userLevel} >= {eventType}={eventLevel} = {userLevel >= eventLevel}"); return userLevel >= eventLevel; } @@ -78,7 +79,7 @@ public class RoomPowerLevelEventContent : EventContent { if (Events is null) return StateDefault ?? 0; return Events.TryGetValue(eventType, out var level) ? level : StateDefault ?? 0; } - + public long GetTimelineEventPowerLevel(string eventType) { ArgumentNullException.ThrowIfNull(eventType); if (Events is null) return EventsDefault ?? 0; diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs index c9cd260..4145a16 100644 --- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs +++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs @@ -2,6 +2,7 @@ // #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Net.Http.Headers; using System.Reflection; using System.Security.Cryptography.X509Certificates; @@ -26,7 +27,8 @@ public class MatrixHttpClient { EnableMultipleHttp2Connections = true }; Client = new HttpClient(handler) { - DefaultRequestVersion = new Version(3, 0) + DefaultRequestVersion = new Version(3, 0), + Timeout = TimeSpan.FromDays(1) }; } catch (PlatformNotSupportedException e) { @@ -72,12 +74,15 @@ public class MatrixHttpClient { await _rateLimitSemaphore.WaitAsync(cancellationToken); #endif - Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})"); + Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.GetContentLength())})"); if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri); foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value); - foreach (var (key, value) in DefaultRequestHeaders) request.Headers.Add(key, value); + foreach (var (key, value) in DefaultRequestHeaders) { + if (request.Headers.Contains(key)) continue; + request.Headers.Add(key, value); + } request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true); @@ -105,7 +110,13 @@ public class MatrixHttpClient { public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { var responseMessage = await SendUnhandledAsync(request, cancellationToken); if (responseMessage.IsSuccessStatusCode) return responseMessage; - + + //retry on gateway timeout + if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) { + request.ResetSendStatus(); + return await SendAsync(request, cancellationToken); + } + //error handling var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken); if (content.Length == 0) diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs index d897078..b639e1f 100644 --- a/LibMatrix/Helpers/MessageBuilder.cs +++ b/LibMatrix/Helpers/MessageBuilder.cs @@ -91,6 +91,18 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr return this; } + public MessageBuilder WithMention(string id, string? displayName = null) { + Content.Body += $"@{displayName ?? id}"; + Content.FormattedBody += $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>"; + return this; + } + + public MessageBuilder WithNewline() { + Content.Body += "\n"; + Content.FormattedBody += "<br>"; + return this; + } + public MessageBuilder WithTable(Action<TableBuilder> tableBuilder) { var tb = new TableBuilder(this); this.WithHtmlTag("table", msb => tableBuilder(tb)); diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs index 1833bd0..c9ca85d 100644 --- a/LibMatrix/Helpers/SyncHelper.cs +++ b/LibMatrix/Helpers/SyncHelper.cs @@ -4,6 +4,7 @@ using ArcaneLibs.Extensions; using LibMatrix.Filters; using LibMatrix.Homeservers; using LibMatrix.Responses; +using LibMatrix.Utilities; using Microsoft.Extensions.Logging; namespace LibMatrix.Helpers; @@ -42,6 +43,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg _filter = value; _filterIsDirty = true; _filterId = null; + _namedFilterName = null; } } @@ -81,16 +83,16 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg if (!string.IsNullOrWhiteSpace(Since)) url += $"&since={Since}"; if (_filterId is not null) url += $"&filter={_filterId}"; - logger?.LogInformation("SyncHelper: Calling: {}", url); + // logger?.LogInformation("SyncHelper: Calling: {}", url); try { var httpResp = await homeserver.ClientHttpClient.GetAsync(url, cancellationToken ?? CancellationToken.None); if (httpResp is null) throw new NullReferenceException("Failed to send HTTP request"); - logger?.LogInformation("Got sync response: {} bytes, {} elapsed", httpResp.Content.Headers.ContentLength ?? -1, sw.Elapsed); + logger?.LogTrace("Got sync response: {} bytes, {} elapsed", httpResp.GetContentLength(), sw.Elapsed); var deserializeSw = Stopwatch.StartNew(); var resp = await httpResp.Content.ReadFromJsonAsync<SyncResponse>(cancellationToken: cancellationToken ?? CancellationToken.None, jsonTypeInfo: SyncResponseSerializerContext.Default.SyncResponse); - logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.Content.Headers.ContentLength ?? -1, deserializeSw.Elapsed, sw.Elapsed); + logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.GetContentLength(), deserializeSw.Elapsed, sw.Elapsed); var timeToWait = MinimumDelay.Subtract(sw.Elapsed); if (timeToWait.TotalMilliseconds > 0) await Task.Delay(timeToWait); diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs index c729a44..6be49b9 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs @@ -406,4 +406,111 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { public NamedFilterCache FilterCache { get; init; } public NamedFileCache FileCache { get; init; } } + +#region Authenticated Media + + // TODO: implement /_matrix/client/v1/media/config when it's actually useful - https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediaconfig + + private (string ServerName, string MediaId) ParseMxcUri(string mxcUri) { + if (!mxcUri.StartsWith("mxc://")) throw new ArgumentException("Matrix Content URIs must start with 'mxc://'", nameof(mxcUri)); + var parts = mxcUri[6..].Split('/'); + if (parts.Length != 2) throw new ArgumentException($"Invalid Matrix Content URI '{mxcUri}' passed! Matrix Content URIs must exist of only 2 parts!", nameof(mxcUri)); + return (parts[0], parts[1]); + } + + public async Task<Stream> GetMediaStreamAsync(string mxcUri, string? filename = null, int? timeout = null) { + var (serverName, mediaId) = ParseMxcUri(mxcUri); + try { + var uri = $"/_matrix/client/v1/media/download/{serverName}/{mediaId}"; + if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}"; + if (timeout is not null) uri += $"?timeout_ms={timeout}"; + var res = await ClientHttpClient.GetAsync(uri); + return await res.Content.ReadAsStreamAsync(); + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_UNKNOWN" }) throw; + } + + //fallback to legacy media + try { + var uri = $"/_matrix/media/v1/download/{serverName}/{mediaId}"; + if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}"; + if (timeout is not null) uri += $"?timeout_ms={timeout}"; + var res = await ClientHttpClient.GetAsync(uri); + return await res.Content.ReadAsStreamAsync(); + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_UNKNOWN" }) throw; + } + + throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED, + Error = "Failed to download media" + }; + // return default; + } + + public async Task<Stream> GetThumbnailStreamAsync(string mxcUri, int width, int height, string? method = null, int? timeout = null) { + var (serverName, mediaId) = ParseMxcUri(mxcUri); + try { + var uri = new Uri($"/_matrix/client/v1/thumbnail/{serverName}/{mediaId}"); + uri = uri.AddQuery("width", width.ToString()); + uri = uri.AddQuery("height", height.ToString()); + if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method); + if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString()); + + var res = await ClientHttpClient.GetAsync(uri.ToString()); + return await res.Content.ReadAsStreamAsync(); + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_UNKNOWN" }) throw; + } + + //fallback to legacy media + try { + var uri = new Uri($"/_matrix/media/v1/thumbnail/{serverName}/{mediaId}"); + uri = uri.AddQuery("width", width.ToString()); + uri = uri.AddQuery("height", height.ToString()); + if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method); + if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString()); + + var res = await ClientHttpClient.GetAsync(uri.ToString()); + return await res.Content.ReadAsStreamAsync(); + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_UNKNOWN" }) throw; + } + + throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED, + Error = "Failed to download media" + }; + // return default; + } + + public async Task<Dictionary<string, JsonValue>?> GetUrlPreviewAsync(string url) { + try { + var res = await ClientHttpClient.GetAsync($"/_matrix/client/v1/media/preview_url?url={HttpUtility.UrlEncode(url)}"); + return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>(); + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw; + } + + //fallback to legacy media + try { + var res = await ClientHttpClient.GetAsync($"/_matrix/media/v1/preview_url?url={HttpUtility.UrlEncode(url)}"); + return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>(); + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw; + } + + throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED, + Error = "Failed to download URL preview" + }; + } + +#endregion } \ No newline at end of file diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs index ecf3e3a..f9e3d04 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs @@ -107,6 +107,7 @@ public class RemoteHomeserver { #endregion + [Obsolete("This call uses the deprecated unauthenticated media endpoints, please switch to the relevant AuthenticatedHomeserver methods instead.", true)] public string? ResolveMediaUri(string? mxcUri) { if (mxcUri is null) return null; if (mxcUri.StartsWith("https://")) return mxcUri; diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj index e037672..6158ff8 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj @@ -23,12 +23,14 @@ Using the NuGet version in development is annoying due to delays between pushing and being able to consume. If you want to use a time-appropriate version of the library, recursively clone https://cgit.rory.gay/matrix/MatrixUtils.git instead, since this will be locked by the MatrixUtils project, which contains both LibMatrix and ArcaneLibs as a submodule. --> - <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview*"/> + <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview.202*"/> <ProjectReference Include="..\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj"/> </ItemGroup> + <!-- <Target Name="ArcaneLibsNugetWarning" AfterTargets="AfterBuild"> <Warning Text="ArcaneLibs is being referenced from NuGet, which is dangerous. Please read the warning in LibMatrix.csproj!" Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')"/> </Target> + --> </Project> diff --git a/LibMatrix/Responses/SyncResponse.cs b/LibMatrix/Responses/SyncResponse.cs index e4addb6..b2308c5 100644 --- a/LibMatrix/Responses/SyncResponse.cs +++ b/LibMatrix/Responses/SyncResponse.cs @@ -39,7 +39,7 @@ public class SyncResponse { // supporting classes public class PresenceDataStructure { [JsonPropertyName("events")] - public List<StateEventResponse> Events { get; set; } = new(); + public List<StateEventResponse>? Events { get; set; } } public class RoomsDataStructure { diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index b906f08..8398ab9 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -106,7 +106,7 @@ public class GenericRoom { Console.WriteLine("WARNING: Homeserver does not support getting event ID from state events, falling back to sync"); var sh = new SyncHelper(Homeserver); var emptyFilter = new SyncFilter.EventFilter(types: [], limit: 1, senders: [], notTypes: ["*"]); - var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms:[]); + var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms: []); sh.Filter = new() { Presence = emptyFilter, AccountData = emptyFilter, @@ -121,10 +121,11 @@ public class GenericRoom { var sync = await sh.SyncAsync(); var state = sync.Rooms.Join[RoomId].State.Events; var stateEvent = state.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey); - if (stateEvent is null) throw new LibMatrixException() { - ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND, - Error = "State event not found in sync response" - }; + if (stateEvent is null) + throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND, + Error = "State event not found in sync response" + }; return stateEvent.EventId; } @@ -210,10 +211,11 @@ public class GenericRoom { public async Task<RoomIdResponse> JoinAsync(string[]? homeservers = null, string? reason = null, bool checkIfAlreadyMember = true) { if (checkIfAlreadyMember) try { - _ = await GetCreateEventAsync(); - return new RoomIdResponse { - RoomId = RoomId - }; + var ser = await GetStateEventOrNullAsync(RoomMemberEventContent.EventId, Homeserver.UserId); + if (ser?.TypedContent is RoomMemberEventContent { Membership: "join" }) + return new RoomIdResponse { + RoomId = RoomId + }; } catch { } //ignore @@ -231,7 +233,7 @@ public class GenericRoom { // var sw = Stopwatch.StartNew(); var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members"); // if (sw.ElapsedMilliseconds > 1000) - // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); // var resText = await res.Content.ReadAsStringAsync(); // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}"); @@ -239,7 +241,7 @@ public class GenericRoom { TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default }); // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); foreach (var resp in result.Chunk) { if (resp?.Type != "m.room.member") continue; @@ -248,14 +250,14 @@ public class GenericRoom { } // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); } public async Task<FrozenSet<StateEventResponse>> GetMembersListAsync(bool joinedOnly = true) { // var sw = Stopwatch.StartNew(); var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members"); // if (sw.ElapsedMilliseconds > 1000) - // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); // var resText = await res.Content.ReadAsStringAsync(); // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}"); @@ -263,7 +265,7 @@ public class GenericRoom { TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default }); // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); var members = new List<StateEventResponse>(); foreach (var resp in result.Chunk) { @@ -273,7 +275,7 @@ public class GenericRoom { } // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); return members.ToFrozenSet(); } @@ -316,9 +318,12 @@ public class GenericRoom { public Task<RoomPowerLevelEventContent?> GetPowerLevelsAsync() => GetStateAsync<RoomPowerLevelEventContent>("m.room.power_levels"); + [Obsolete("This method will be merged into GetNameAsync() in the future.")] public async Task<string> GetNameOrFallbackAsync(int maxMemberNames = 2) { try { - return await GetNameAsync(); + var name = await GetNameAsync(); + if (!string.IsNullOrEmpty(name)) return name; + throw new(); } catch { try { @@ -352,22 +357,6 @@ public class GenericRoom { return Task.WhenAll(tasks); } - public async Task<string?> GetResolvedRoomAvatarUrlAsync(bool useOriginHomeserver = false) { - var avatar = await GetAvatarUrlAsync(); - if (avatar?.Url is null) return null; - if (!avatar.Url.StartsWith("mxc://")) return avatar.Url; - if (useOriginHomeserver) - try { - var hs = avatar.Url.Split('/', 3)[1]; - return await new HomeserverResolverService(NullLogger<HomeserverResolverService>.Instance).ResolveMediaUri(hs, avatar.Url); - } - catch (Exception e) { - Console.WriteLine(e); - } - - return Homeserver.ResolveMediaUri(avatar.Url); - } - #endregion #region Simple calls @@ -390,7 +379,7 @@ public class GenericRoom { public async Task UnbanAsync(string userId, string? reason = null) => await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", - new UserIdAndReason { UserId = userId, Reason = reason}); + new UserIdAndReason { UserId = userId, Reason = reason }); public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) { if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not null) @@ -407,7 +396,7 @@ public class GenericRoom { .Content.ReadFromJsonAsync<EventIdResponse>(); public async Task<EventIdResponse?> SendStateEventAsync(string eventType, string stateKey, object content) => - await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}/{stateKey}", content)) + await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType.UrlEncode()}/{stateKey.UrlEncode()}", content)) .Content.ReadFromJsonAsync<EventIdResponse>(); public async Task<EventIdResponse> SendTimelineEventAsync(string eventType, TimelineEventContent content) { @@ -442,6 +431,16 @@ public class GenericRoom { return await res.Content.ReadFromJsonAsync<T>(); } + + public async Task<T?> GetRoomAccountDataOrNullAsync<T>(string key) { + try { + return await GetRoomAccountDataAsync<T>(key); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_NOT_FOUND") return default; + throw; + } + } public async Task SetRoomAccountDataAsync(string key, object data) { var res = await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}", data); @@ -454,10 +453,17 @@ public class GenericRoom { public Task<StateEventResponse> GetEventAsync(string eventId) => Homeserver.ClientHttpClient.GetFromJsonAsync<StateEventResponse>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}"); - public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string reason) { + public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string? reason = null) { var data = new { reason }; - return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync( - $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!; + var url = $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid().ToString()}"; + while (true) { + try { + return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync(url, data)).Content.ReadFromJsonAsync<EventIdResponse>())!; + } catch (MatrixException e) { + if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) throw; + throw; + } + } } #endregion @@ -524,7 +530,7 @@ public class GenericRoom { var uri = new Uri(path, UriKind.Relative); if (dir == "b" || dir == "f") uri = uri.AddQuery("dir", dir); - else if(!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir)); + else if (!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir)); if (!string.IsNullOrEmpty(from)) uri = uri.AddQuery("from", from); if (chunkLimit is not null) uri = uri.AddQuery("limit", chunkLimit.Value.ToString()); if (recurse is not null) uri = uri.AddQuery("recurse", recurse.Value.ToString()); diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs index 06ea9de..8b7e54b 100644 --- a/LibMatrix/Services/ServiceInstaller.cs +++ b/LibMatrix/Services/ServiceInstaller.cs @@ -5,23 +5,13 @@ namespace LibMatrix.Services; public static class ServiceInstaller { public static IServiceCollection AddRoryLibMatrixServices(this IServiceCollection services, RoryLibMatrixConfiguration? config = null) { - //Check required services - // if (!services.Any(x => x.ServiceType == typeof(TieredStorageService))) - // throw new Exception("[RMUCore/DI] No TieredStorageService has been registered!"); //Add config services.AddSingleton(config ?? new RoryLibMatrixConfiguration()); //Add services services.AddSingleton<HomeserverResolverService>(sp => new HomeserverResolverService(sp.GetRequiredService<ILogger<HomeserverResolverService>>())); - - // if (services.First(x => x.ServiceType == typeof(TieredStorageService)).Lifetime == ServiceLifetime.Singleton) { services.AddSingleton<HomeserverProviderService>(); - // } - // else { - // services.AddScoped<HomeserverProviderService>(); - // } - // services.AddScoped<MatrixHttpClient>(); return services; } } diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs index 073d26d..cc870e4 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs @@ -44,6 +44,7 @@ public class StateEvent { public string FriendlyTypeNamePlural => MappedType.GetFriendlyNamePluralOrNull() ?? Type; private static readonly JsonSerializerOptions TypedContentSerializerOptions = new() { + // We need these, NumberHandling covers other number types that we don't want to convert Converters = { new JsonFloatStringConverter(), new JsonDoubleStringConverter(), @@ -55,9 +56,6 @@ public class StateEvent { [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] public EventContent? TypedContent { get { - // if (Type == "m.receipt") { - // return null; - // } try { var mappedType = GetStateEventType(Type); if (mappedType == typeof(UnknownEventContent)) @@ -81,6 +79,18 @@ public class StateEvent { } } + public T? ContentAs<T>() { + try { + return RawContent.Deserialize<T>(TypedContentSerializerOptions)!; + } + catch (JsonException e) { + Console.WriteLine(e); + Console.WriteLine("Content:\n" + (RawContent?.ToJson() ?? "null")); + } + + return default; + } + [JsonPropertyName("state_key")] public string? StateKey { get; set; } @@ -156,7 +166,7 @@ public class StateEventResponse : StateEvent { public string? Sender { get; set; } [JsonPropertyName("unsigned")] - public UnsignedData? Unsigned { get; set; } + public JsonObject? Unsigned { get; set; } [JsonPropertyName("event_id")] public string? EventId { get; set; } @@ -254,4 +264,23 @@ public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoR } */ -#endregion \ No newline at end of file +#endregion + +/* +public class ForgivingObjectConverter<T> : JsonConverter<T> where T : new() { + public override T? Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { + try { + var text = JsonDocument.ParseValue(ref reader).RootElement.GetRawText(); + return JsonSerializer.Deserialize<T>(text, options); + } + catch (JsonException ex) { + Console.WriteLine(ex); + return null; + } + } + + public override bool CanConvert(Type typeToConvert) => true; + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + => JsonSerializer.Serialize<T>(writer, value, options); +}*/ \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs index 401223c..2819f80 100644 --- a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs +++ b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs @@ -78,19 +78,7 @@ public class HomeserverAbstraction(HomeserverProviderService _hsProvider, Config var username = _config.TestUsername; var password = _config.TestPassword; - - LoginResponse reg; - try { - reg = await rhs.LoginAsync(username, password); - } - catch (MatrixException e) { - if (e.ErrorCode == "M_FORBIDDEN") { - await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!"); - reg = await rhs.RegisterAsync(username, password, "Unit tests!"); - } - else throw new Exception("Failed to log in", e); - } - - return (username, password, reg.AccessToken); + var reg = await rhs.RegisterAsync(username, password, "Unit tests!"); + return ("", "", ""); } } \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs new file mode 100644 index 0000000..712e45a --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs @@ -0,0 +1,56 @@ +using ArcaneLibs.Extensions; +using ArcaneLibs.Extensions.Streams; +using LibMatrix.Homeservers; +using LibMatrix.Services; +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests; + +public class AuthMediaTests : TestBed<TestFixture> { + private readonly TestFixture _fixture; + private readonly HomeserverResolverService _resolver; + private readonly Config _config; + private readonly HomeserverProviderService _provider; + private readonly HomeserverAbstraction _hsAbstraction; + + public AuthMediaTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _fixture = fixture; + _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); + _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task UploadFileAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + + var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes()); + Assert.NotNull(mxcUri); + } + + [Fact] + public async Task DownloadFileAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + + var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes()); + Assert.NotNull(mxcUri); + + var file = await hs.GetMediaStreamAsync(mxcUri); + Assert.NotNull(file); + + var data = file!.ReadToEnd().AsString(); + Assert.Equal("LibMatrix test file", data); + } + + [SkippableFact(typeof(LibMatrixException))] // This test will fail if the homeserver does not support URL previews + public async Task GetUrlPreviewAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var preview = await hs.GetUrlPreviewAsync("https://matrix.org"); + + Assert.NotNull(preview); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/AuthTests.cs b/Tests/LibMatrix.Tests/Tests/AuthTests.cs index 633c842..69e6231 100644 --- a/Tests/LibMatrix.Tests/Tests/AuthTests.cs +++ b/Tests/LibMatrix.Tests/Tests/AuthTests.cs @@ -20,17 +20,36 @@ public class AuthTests : TestBed<TestFixture> { [Fact] public async Task LoginWithPassword() { - var credentials = await _hsAbstraction.GetKnownCredentials(); + Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!"); + Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!"); + Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!"); + + // var server = await _resolver.ResolveHomeserverFromWellKnown(_config.TestHomeserver!); + var rhs = await _provider.GetRemoteHomeserver(_config.TestHomeserver); + var username = Guid.NewGuid().ToString(); + var password = Guid.NewGuid().ToString(); - var login = await _provider.Login(_config.TestHomeserver!, credentials.username, credentials.password); + var reg = await rhs.RegisterAsync(username, password, "Unit tests!"); + + var login = await _provider.Login(_config.TestHomeserver!, username, password); Assert.NotNull(login); Assert.NotNull(login.AccessToken); } [Fact] public async Task LoginWithToken() { - var credentials = await _hsAbstraction.GetKnownCredentials(); - var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, credentials.token); + Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!"); + Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!"); + Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!"); + + // var server = await _resolver.ResolveHomeserverFromWellKnown(_config.TestHomeserver!); + var rhs = await _provider.GetRemoteHomeserver(_config.TestHomeserver); + var username = Guid.NewGuid().ToString(); + var password = Guid.NewGuid().ToString(); + + var reg = await rhs.RegisterAsync(username, password, "Unit tests!"); + + var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, reg.AccessToken); Assert.NotNull(hs); Assert.NotNull(hs.WhoAmI); hs.WhoAmI.VerifyRequiredFields(); diff --git a/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs index 03f3c24..20f975e 100644 --- a/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs +++ b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs @@ -18,13 +18,13 @@ public class RemoteHomeserverTests : TestBed<TestFixture> { _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); } - [Fact] - public async Task ResolveMedia() { - var hs = await _provider.GetRemoteHomeserver("matrix.org"); - var media = hs.ResolveMediaUri("mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo"); - - Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media); - } + // [Fact] + // public async Task ResolveMedia() { + // var hs = await _provider.GetRemoteHomeserver("matrix.org"); + // var media = hs.ResolveMediaUri("mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo"); + // + // Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media); + // } [Fact] public async Task ResolveRoomAliasAsync() { diff --git a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs deleted file mode 100644 index 1c5747c..0000000 --- a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs +++ /dev/null @@ -1,69 +0,0 @@ -// using System.Diagnostics; -// using LibMatrix.Helpers; -// using LibMatrix.Services; -// using LibMatrix.Tests.Abstractions; -// using LibMatrix.Tests.Fixtures; -// using Microsoft.Extensions.Logging; -// using Xunit.Abstractions; -// using Xunit.Microsoft.DependencyInjection.Abstracts; -// -// namespace LibMatrix.Tests.Tests; -// -// public class TestCleanup : TestBed<TestFixture> { -// private readonly HomeserverAbstraction _hsAbstraction; -// private readonly ILogger<TestCleanup> _logger; -// -// public TestCleanup(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { -// // _fixture = fixture; -// _logger = _fixture.GetService<ILogger<TestCleanup>>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(ILogger<TestCleanup>)}"); -// _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); -// } -// -// [SkippableFact(typeof(MatrixException))] -// public async Task Cleanup() { -// // Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!"); -// // Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!"); -// // Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!"); -// -// var hs = await _hsAbstraction.GetConfiguredHomeserver(); -// Assert.NotNull(hs); -// -// var syncHelper = new SyncHelper(hs, _logger) { -// Timeout = 3000 -// }; -// _testOutputHelper.WriteLine("Starting sync loop"); -// var cancellationTokenSource = new CancellationTokenSource(); -// var sw = Stopwatch.StartNew(); -// syncHelper.SyncReceivedHandlers.Add(async response => { -// // if (sw.ElapsedMilliseconds >= 3000) { -// // _testOutputHelper.WriteLine("Cancelling sync loop"); -// -// var tasks = (await hs.GetJoinedRooms()).Select(async room => { -// _logger.LogInformation("Leaving room: {}", room.RoomId); -// await room.LeaveAsync(); -// await room.ForgetAsync(); -// return room; -// }).ToList(); -// await Task.WhenAll(tasks); -// -// // cancellationTokenSource.Cancel(); -// // } -// -// sw.Restart(); -// if (response.Rooms?.Leave is { Count: > 0 }) { -// // foreach (var room in response.Rooms.Leave) { -// // await hs.GetRoom(room.Key).ForgetAsync(); -// // } -// var tasks2 = response.Rooms.Leave.Select(async room => { -// await hs.GetRoom(room.Key).ForgetAsync(); -// return room; -// }).ToList(); -// await Task.WhenAll(tasks2); -// } -// }); -// await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationTokenSource.Token); -// -// Assert.NotNull(hs); -// await hs.Logout(); -// } -// } \ No newline at end of file diff --git a/Utilities/LibMatrix.TestDataGenerator/Program.cs b/Utilities/LibMatrix.TestDataGenerator/Program.cs index 2583817..f3750a8 100644 --- a/Utilities/LibMatrix.TestDataGenerator/Program.cs +++ b/Utilities/LibMatrix.TestDataGenerator/Program.cs @@ -2,6 +2,7 @@ using LibMatrix.Services; using LibMatrix.Utilities.Bot; +using LibMatrix.Utilities.Bot.AppServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using TestDataGenerator.Bot; diff --git a/Utilities/LibMatrix.Utilities.Bot/AppServiceConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/AppServices/AppServiceConfiguration.cs index afda89e..2cfcf32 100644 --- a/Utilities/LibMatrix.Utilities.Bot/AppServiceConfiguration.cs +++ b/Utilities/LibMatrix.Utilities.Bot/AppServices/AppServiceConfiguration.cs @@ -1,23 +1,47 @@ -namespace LibMatrix.Utilities.Bot; +using System.Text.Json.Serialization; + +namespace LibMatrix.Utilities.Bot.AppServices; public class AppServiceConfiguration { + [JsonPropertyName("id")] public string Id { get; set; } = null!; + + [JsonPropertyName("url")] public string? Url { get; set; } = null!; + + [JsonPropertyName("sender_localpart")] public string SenderLocalpart { get; set; } = null!; + + [JsonPropertyName("as_token")] public string AppserviceToken { get; set; } = null!; + + [JsonPropertyName("hs_token")] public string HomeserverToken { get; set; } = null!; + + [JsonPropertyName("protocols")] public List<string>? Protocols { get; set; } = null!; + + [JsonPropertyName("rate_limited")] public bool? RateLimited { get; set; } = null!; + [JsonPropertyName("namespaces")] public AppserviceNamespaces Namespaces { get; set; } = null!; public class AppserviceNamespaces { + [JsonPropertyName("users")] public List<AppserviceNamespace>? Users { get; set; } = null; + + [JsonPropertyName("aliases")] public List<AppserviceNamespace>? Aliases { get; set; } = null; + + [JsonPropertyName("rooms")] public List<AppserviceNamespace>? Rooms { get; set; } = null; public class AppserviceNamespace { + [JsonPropertyName("exclusive")] public bool Exclusive { get; set; } + + [JsonPropertyName("regex")] public string Regex { get; set; } = null!; } } diff --git a/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs b/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs index 621c1ee..ca6a4d8 100644 --- a/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs +++ b/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs @@ -1,8 +1,7 @@ using ArcaneLibs; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.Homeservers; -using LibMatrix.Responses; using LibMatrix.Services; +using LibMatrix.Utilities.Bot.AppServices; using LibMatrix.Utilities.Bot.Interfaces; using LibMatrix.Utilities.Bot.Services; using Microsoft.Extensions.DependencyInjection; @@ -22,6 +21,20 @@ public class BotInstaller(IServiceCollection services) { services.AddScoped<AuthenticatedHomeserverGeneric>(x => { var config = x.GetService<LibMatrixBotConfiguration>() ?? throw new Exception("No configuration found!"); var hsProvider = x.GetService<HomeserverProviderService>() ?? throw new Exception("No homeserver provider found!"); + + if (x.GetService<AppServiceConfiguration>() is AppServiceConfiguration appsvcConfig) + config.AccessToken = appsvcConfig.AppserviceToken; + else if (Environment.GetEnvironmentVariable("LIBMATRIX_ACCESS_TOKEN_PATH") is string path) + config.AccessTokenPath = path; + + if(string.IsNullOrWhiteSpace(config.AccessToken) && string.IsNullOrWhiteSpace(config.AccessTokenPath)) + throw new Exception("Unable to add bot service without an access token or access token path!"); + + if(!string.IsNullOrWhiteSpace(config.AccessTokenPath)) { + var token = File.ReadAllText(config.AccessTokenPath); + config.AccessToken = token; + } + var hs = hsProvider.GetAuthenticatedWithToken(config.Homeserver, config.AccessToken).Result; return hs; diff --git a/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs index 245442f..728b169 100644 --- a/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs +++ b/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs @@ -6,7 +6,8 @@ namespace LibMatrix.Utilities.Bot; public class LibMatrixBotConfiguration { public LibMatrixBotConfiguration(IConfiguration config) => config.GetRequiredSection("LibMatrixBot").Bind(this); public string Homeserver { get; set; } - public string AccessToken { get; set; } + public string? AccessToken { get; set; } + public string? AccessTokenPath { get; set; } public List<string> Prefixes { get; set; } public bool MentionPrefix { get; set; } public string? LogRoom { get; set; } |