diff --git a/MatrixRoomUtils.Core/Authentication/MatrixAccount.cs b/MatrixRoomUtils.Core/Authentication/MatrixAccount.cs
new file mode 100644
index 0000000..4180df5
--- /dev/null
+++ b/MatrixRoomUtils.Core/Authentication/MatrixAccount.cs
@@ -0,0 +1,93 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using MatrixRoomUtils.Responses;
+
+namespace MatrixRoomUtils.Authentication;
+
+public class MatrixAccount
+{
+ public static async Task<LoginResponse> Login(string homeserver, string username, string password)
+ {
+ Console.WriteLine($"Logging in to {homeserver} as {username}...");
+ homeserver = await ResolveHomeserverFromWellKnown(homeserver);
+ var hc = new HttpClient();
+ var payload = new
+ {
+ type = "m.login.password",
+ identifier = new
+ {
+ type = "m.id.user",
+ user = username
+ },
+ password = password,
+ initial_device_display_name = "Rory&::MatrixRoomUtils"
+ };
+ Console.WriteLine($"Sending login request to {homeserver}...");
+ var resp = await hc.PostAsJsonAsync($"{homeserver}/_matrix/client/r0/login", payload);
+ Console.WriteLine($"Login: {resp.StatusCode}");
+ var data = await resp.Content.ReadFromJsonAsync<JsonElement>();
+ if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data.ToString());
+ if (data.TryGetProperty("retry_after_ms", out var retryAfter))
+ {
+ Console.WriteLine($"Login: Waiting {retryAfter.GetInt32()}ms before retrying");
+ await Task.Delay(retryAfter.GetInt32());
+ return await Login(homeserver, username, password);
+ }
+
+ return data.Deserialize<LoginResponse>();
+ //var token = data.GetProperty("access_token").GetString();
+ //return token;
+ }
+
+ public static async Task<ProfileResponse> GetProfile(string homeserver, string mxid)
+ {
+ Console.WriteLine($"Fetching profile for {mxid} on {homeserver}...");
+ homeserver = await ResolveHomeserverFromWellKnown(homeserver);
+ var hc = new HttpClient();
+ var resp = await hc.GetAsync($"{homeserver}/_matrix/client/r0/profile/{mxid}");
+ var data = await resp.Content.ReadFromJsonAsync<JsonElement>();
+ if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data.ToString());
+ return data.Deserialize<ProfileResponse>();
+ }
+
+ public static async Task<string> ResolveHomeserverFromWellKnown(string homeserver)
+ {
+ using var hc = new HttpClient();
+ Console.WriteLine($"Resolving homeserver: {homeserver}");
+ if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver;
+
+ if (await CheckSuccessStatus($"{homeserver}/.well-known/matrix/client"))
+ {
+ var resp = await hc.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/client");
+ var hs = resp.GetProperty("m.homeserver").GetProperty("base_url").GetString();
+ return hs;
+ }
+ Console.WriteLine($"No client well-known...");
+ if (await CheckSuccessStatus($"{homeserver}/.well-known/matrix/server"))
+ {
+ var resp = await hc.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/server");
+ var hs = resp.GetProperty("m.server").GetString();
+ return hs;
+ }
+ Console.WriteLine($"No server well-known...");
+ if (await CheckSuccessStatus($"{homeserver}/_matrix/client/versions")) return homeserver;
+ Console.WriteLine($"Failed to resolve homeserver, not on {homeserver}, nor do client or server well-knowns exist!");
+ throw new InvalidDataException($"Failed to resolve homeserver, not on {homeserver}, nor do client or server well-knowns exist!");
+ }
+
+ private static async Task<bool> CheckSuccessStatus(string url)
+ {
+ //cors causes failure, try to catch
+ try
+ {
+ using var hc = new HttpClient();
+ var resp = await hc.GetAsync(url);
+ return resp.IsSuccessStatusCode;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Failed to check success status: {e.Message}");
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs b/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs
new file mode 100644
index 0000000..cf798ce
--- /dev/null
+++ b/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs
@@ -0,0 +1,14 @@
+using System.Text.Json;
+
+namespace MatrixRoomUtils.Extensions;
+
+public static class ObjectExtensions
+{
+ public static string ToJson(this object obj, bool indent = true, bool ignoreNull = false)
+ {
+ var jso = new JsonSerializerOptions();
+ if(indent) jso.WriteIndented = true;
+ if(ignoreNull) jso.IgnoreNullValues = true;
+ return JsonSerializer.Serialize(obj, jso);
+ }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Extensions/StringExtensions.cs b/MatrixRoomUtils.Core/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..e02f0b9
--- /dev/null
+++ b/MatrixRoomUtils.Core/Extensions/StringExtensions.cs
@@ -0,0 +1,17 @@
+using MatrixRoomUtils.Authentication;
+
+namespace MatrixRoomUtils.Extensions;
+
+public static class StringExtensions
+{
+ public static async Task<string> GetMediaUrl(this string MxcUrl)
+ {
+ //MxcUrl: mxc://rory.gay/ocRVanZoUTCcifcVNwXgbtTg
+ //target: https://matrix.rory.gay/_matrix/media/v3/download/rory.gay/ocRVanZoUTCcifcVNwXgbtTg
+
+ var server = MxcUrl.Split('/')[2];
+ var mediaId = MxcUrl.Split('/')[3];
+ return $"{await MatrixAccount.ResolveHomeserverFromWellKnown(server)}/_matrix/media/v3/download/{server}/{mediaId}";
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj b/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj
new file mode 100644
index 0000000..6836c68
--- /dev/null
+++ b/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj
@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+</Project>
diff --git a/MatrixRoomUtils.Core/Responses/LoginResponse.cs b/MatrixRoomUtils.Core/Responses/LoginResponse.cs
new file mode 100644
index 0000000..eedc970
--- /dev/null
+++ b/MatrixRoomUtils.Core/Responses/LoginResponse.cs
@@ -0,0 +1,31 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Authentication;
+
+namespace MatrixRoomUtils.Responses;
+
+public class LoginResponse
+{
+ [JsonPropertyName("access_token")]
+ public string AccessToken { get; set; }
+ [JsonPropertyName("device_id")]
+ public string DeviceId { get; set; }
+ [JsonPropertyName("home_server")]
+ public string HomeServer { get; set; }
+ [JsonPropertyName("user_id")]
+ public string UserId { get; set; }
+
+ public async Task<ProfileResponse> GetProfile()
+ {
+ var hc = new HttpClient();
+ var resp = await hc.GetAsync($"{HomeServer}/_matrix/client/r0/profile/{UserId}");
+ var data = await resp.Content.ReadFromJsonAsync<JsonElement>();
+ if(!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data.ToString());
+ return data.Deserialize<ProfileResponse>();
+ }
+ public async Task<string> GetCanonicalHomeserverUrl()
+ {
+ return await MatrixAccount.ResolveHomeserverFromWellKnown(HomeServer);
+ }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Responses/ProfileResponse.cs b/MatrixRoomUtils.Core/Responses/ProfileResponse.cs
new file mode 100644
index 0000000..ab6cc92
--- /dev/null
+++ b/MatrixRoomUtils.Core/Responses/ProfileResponse.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+
+namespace MatrixRoomUtils.Authentication;
+
+public class ProfileResponse
+{
+ [JsonPropertyName("avatar_url")]
+ public string? AvatarUrl { get; set; } = "";
+ [JsonPropertyName("displayname")]
+ public string DisplayName { get; set; } = "";
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEvent.cs b/MatrixRoomUtils.Core/StateEvent.cs
new file mode 100644
index 0000000..34cefe4
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEvent.cs
@@ -0,0 +1,53 @@
+namespace MatrixRoomUtils;
+
+public class StateEvent
+{
+ //example:
+ /*
+ {
+ "content": {
+ "avatar_url": "mxc://matrix.org/BnmEjNvGAkStmAoUiJtEbycT",
+ "displayname": "X ⊂ Shekhinah | she/her | you",
+ "membership": "join"
+ },
+ "origin_server_ts": 1682668449785,
+ "room_id": "!wDPwzxYCNPTkHGHCFT:the-apothecary.club",
+ "sender": "@kokern:matrix.org",
+ "state_key": "@kokern:matrix.org",
+ "type": "m.room.member",
+ "unsigned": {
+ "replaces_state": "$7BWfzN15LN8FFUing1hiUQWFfxnOusrEHYFNiOnNrlM",
+ "prev_content": {
+ "avatar_url": "mxc://matrix.org/hEQbGywixsjpxDrWvUYEFNur",
+ "displayname": "X ⊂ Shekhinah | she/her | you",
+ "membership": "join"
+ },
+ "prev_sender": "@kokern:matrix.org"
+ },
+ "event_id": "$6AGoMCaxqcOeIIDbez1f0VKwLkOEq3EiVLdlsoxDpNg",
+ "user_id": "@kokern:matrix.org",
+ "replaces_state": "$7BWfzN15LN8FFUing1hiUQWFfxnOusrEHYFNiOnNrlM",
+ "prev_content": {
+ "avatar_url": "mxc://matrix.org/hEQbGywixsjpxDrWvUYEFNur",
+ "displayname": "X ⊂ Shekhinah | she/her | you",
+ "membership": "join"
+ }
+ }
+ */
+ public dynamic content { get; set; }
+ public long origin_server_ts { get; set; }
+ public string room_id { get; set; }
+ public string sender { get; set; }
+ public string state_key { get; set; }
+ public string type { get; set; }
+ public dynamic unsigned { get; set; }
+ public string event_id { get; set; }
+ public string user_id { get; set; }
+ public string replaces_state { get; set; }
+ public dynamic prev_content { get; set; }
+}
+
+public class StateEvent<T> : StateEvent where T : class
+{
+ public T content { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs
new file mode 100644
index 0000000..45063cc
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs
@@ -0,0 +1,52 @@
+using System.Text.Json.Serialization;
+
+namespace MatrixRoomUtils.StateEventTypes;
+
+public class PolicyRuleStateEventData
+{
+ /// <summary>
+ /// Entity this ban applies to, can use * and ? as globs.
+ /// </summary>
+ [JsonPropertyName("entity")]
+ public string? Entity { get; set; }
+ /// <summary>
+ /// Reason this user is banned
+ /// </summary>
+ [JsonPropertyName("reason")]
+ public string? Reason { get; set; }
+ /// <summary>
+ /// Suggested action to take
+ /// </summary>
+ [JsonPropertyName("recommendation")]
+ public string? Recommendation { get; set; }
+
+ /// <summary>
+ /// Expiry time in milliseconds since the unix epoch, or null if the ban has no expiry.
+ /// </summary>
+ [JsonPropertyName("support.feline.policy.expiry.rev.2")] //stable prefix: expiry, msc pending
+ public long? Expiry { get; set; }
+
+
+ //utils
+ /// <summary>
+ /// Readable expiry time, provided for easy interaction
+ /// </summary>
+ [JsonPropertyName("gay.rory.matrix_room_utils.readable_expiry_time_utc")]
+ public DateTime? ExpiryDateTime
+ {
+ get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime;
+ set => Expiry = ((DateTimeOffset) value).ToUnixTimeMilliseconds();
+ }
+}
+
+public static class PolicyRecommendationTypes
+{
+ /// <summary>
+ /// Ban this user
+ /// </summary>
+ public static string Ban = "m.ban";
+ /// <summary>
+ /// Mute this user
+ /// </summary>
+ public static string Mute = "support.feline.policy.recommendation_mute"; //stable prefix: m.mute, msc pending
+}
\ No newline at end of file
|