diff --git a/BugMine.Sdk/BugMine.Sdk.csproj b/BugMine.Sdk/BugMine.Sdk.csproj
new file mode 100644
index 0000000..092683c
--- /dev/null
+++ b/BugMine.Sdk/BugMine.Sdk.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
+ <HintPath>..\..\..\..\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\8.0.0\lib\net8.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/BugMine.Sdk/BugMineClient.cs b/BugMine.Sdk/BugMineClient.cs
new file mode 100644
index 0000000..be80c3a
--- /dev/null
+++ b/BugMine.Sdk/BugMineClient.cs
@@ -0,0 +1,55 @@
+using System.Text.RegularExpressions;
+using ArcaneLibs.Extensions;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+
+namespace BugMine.Web.Classes;
+
+public class BugMineClient(AuthenticatedHomeserverGeneric homeserver) {
+ public AuthenticatedHomeserverGeneric Homeserver { get; } = homeserver;
+
+ public async IAsyncEnumerable<BugMineProject> GetProjects() {
+ List<Task<BugMineProject>> tasks = [];
+ await foreach (var room in homeserver.GetJoinedRoomsByType(BugMineProject.RoomType)) {
+ tasks.Add(room.AsBugMineProject());
+ }
+
+ var results = tasks.ToAsyncEnumerable();
+ await foreach (var result in results) {
+ yield return result;
+ }
+ }
+
+ public async Task<BugMineProject> CreateProject(ProjectInfo request) {
+ var alias = string.Join('_', Regex.Matches(request.Name, @"[a-zA-Z0-9]+").Select(x => x.Value))+"-bugmine";
+
+ var crr = new CreateRoomRequest() {
+ CreationContent = new() {
+ ["type"] = "gay.rory.bugmine.project"
+ },
+ Name = $"{request.Name} (BugMine project)",
+ RoomAliasName = alias
+ };
+
+ var response = await Homeserver.CreateRoom(crr);
+ await response.SendStateEventAsync(ProjectInfo.EventId, request);
+
+ return await response.AsBugMineProject();
+ }
+
+ public async Task<BugMineProject?> GetProject(string projectSlug) {
+ if (projectSlug.StartsWith('!')) {
+ var room = homeserver.GetRoom(projectSlug);
+ if (room == null) return null;
+
+ return await (await room.AsBugMineProject()).InitializeAsync();
+ }
+ else {
+ var alias = $"#{projectSlug}";
+ var resolveResult = await Homeserver.ResolveRoomAliasAsync(alias);
+ if (string.IsNullOrEmpty(resolveResult?.RoomId)) return null; //TODO: fallback to finding via joined rooms' canonical alias event?
+
+ return await (await homeserver.GetRoom(resolveResult.RoomId).AsBugMineProject()).InitializeAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/BugMine.Sdk/BugMineIssue.cs b/BugMine.Sdk/BugMineIssue.cs
new file mode 100644
index 0000000..6cf3409
--- /dev/null
+++ b/BugMine.Sdk/BugMineIssue.cs
@@ -0,0 +1,12 @@
+using LibMatrix;
+using LibMatrix.RoomTypes;
+
+namespace BugMine.Web.Classes;
+
+public class BugMineIssue(GenericRoom room, StateEventResponse data) {
+ public GenericRoom Room { get; } = room;
+ public StateEventResponse Data { get; } = data;
+ // public async IAsyncEnumerable<StateEventResponse> GetRelatedEventsAsync() {
+ //
+ // }
+}
\ No newline at end of file
diff --git a/BugMine.Sdk/BugMineProject.cs b/BugMine.Sdk/BugMineProject.cs
new file mode 100644
index 0000000..c90ba6e
--- /dev/null
+++ b/BugMine.Sdk/BugMineProject.cs
@@ -0,0 +1,43 @@
+using System.Text.Json.Nodes;
+using LibMatrix.Homeservers;
+using LibMatrix.RoomTypes;
+
+namespace BugMine.Web.Classes;
+
+public class BugMineProject(GenericRoom room) {
+ public const string RoomType = "gay.rory.bugmine.project";
+ public GenericRoom Room { get; } = room;
+ public ProjectInfo Info { get; set; }
+ public string ProjectSlug { get; set; }
+
+ public async Task<BugMineProject> InitializeAsync() {
+ Info = (await Room.GetStateAsync<ProjectInfo>(ProjectInfo.EventId))!;
+ var alias = await room.GetCanonicalAliasAsync();
+
+ if (alias != null)
+ ProjectSlug = alias.Alias?[1..] ?? room.RoomId;
+ else ProjectSlug = room.RoomId;
+
+ return this;
+ }
+
+ public async Task<BugMineIssue> CreateIssue(BugMineIssueData issue) {
+ // add relation to room creation event
+ issue.RelatesTo = new() {
+ EventId = (await Room.GetStateEventAsync("m.room.create")).EventId,
+ RelationType = "gay.rory.bugmine.issue"
+ };
+ var eventId = await Room.SendTimelineEventAsync(BugMineIssueData.EventId, issue);
+
+ // return new BugMineIssueAccessor(Room, await Room.GetEventAsync<>(eventId));
+ var evt = await room.GetEventAsync(eventId.EventId);
+ Console.WriteLine(evt);
+ return new BugMineIssue(Room, evt);
+ }
+}
+
+public static class ProjectRoomExtensions {
+ public static async Task<BugMineProject> AsBugMineProject(this GenericRoom room) {
+ return await new BugMineProject(room).InitializeAsync();
+ }
+}
\ No newline at end of file
diff --git a/BugMine.Sdk/Events/State/ProjectInfo.cs b/BugMine.Sdk/Events/State/ProjectInfo.cs
new file mode 100644
index 0000000..2d15bff
--- /dev/null
+++ b/BugMine.Sdk/Events/State/ProjectInfo.cs
@@ -0,0 +1,11 @@
+using LibMatrix.EventTypes;
+
+namespace BugMine.Web.Classes;
+
+[MatrixEvent(EventName = EventId)]
+public class ProjectInfo : EventContent {
+ public const string EventId = "gay.rory.bugmine.project_info";
+ public string? Name { get; set; }
+ public string? ProjectIcon { get; set; }
+ public string? Repository { get; set; }
+}
\ No newline at end of file
diff --git a/BugMine.Sdk/Events/Timeline/BugMineIssueComment.cs b/BugMine.Sdk/Events/Timeline/BugMineIssueComment.cs
new file mode 100644
index 0000000..50c73a1
--- /dev/null
+++ b/BugMine.Sdk/Events/Timeline/BugMineIssueComment.cs
@@ -0,0 +1,11 @@
+using LibMatrix.EventTypes;
+
+namespace BugMine.Web.Classes;
+
+[MatrixEvent(EventName = EventId)]
+public class BugMineIssueComment : TimelineEventContent {
+ public const string EventId = "gay.rory.bugmine.comment";
+ public string Comment { get; set; }
+ public string Author { get; set; }
+ public DateTime Timestamp { get; set; }
+}
\ No newline at end of file
diff --git a/BugMine.Sdk/Events/Timeline/BugMineIssueData.cs b/BugMine.Sdk/Events/Timeline/BugMineIssueData.cs
new file mode 100644
index 0000000..480102a
--- /dev/null
+++ b/BugMine.Sdk/Events/Timeline/BugMineIssueData.cs
@@ -0,0 +1,12 @@
+using LibMatrix.EventTypes;
+
+namespace BugMine.Web.Classes;
+
+[MatrixEvent(EventName = ProjectInfo.EventId)]
+public class BugMineIssueData : TimelineEventContent {
+ public const string EventId = "gay.rory.bugmine.issue";
+ public string Name { get; set; }
+ public string? AssignedTo { get; set; }
+ public string? Status { get; set; }
+ public string? Priority { get; set; }
+}
\ No newline at end of file
diff --git a/BugMine.Sdk/ServiceInstaller.cs b/BugMine.Sdk/ServiceInstaller.cs
new file mode 100644
index 0000000..06309ac
--- /dev/null
+++ b/BugMine.Sdk/ServiceInstaller.cs
@@ -0,0 +1,25 @@
+using LibMatrix.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+public static class ServiceInstaller {
+ /// <summary>
+ /// Adds BugMine SDK services to the service collection.
+ /// </summary>
+ /// <param name="services">Service collection</param>
+ /// <param name="config">Optional BugMine SDK configuration</param>
+ /// <returns>Input service collection</returns>
+ public static IServiceCollection AddBugMine(this IServiceCollection services, BugMineSdkConfiguration? config = null) {
+ services.AddRoryLibMatrixServices(new() {
+ AppName = config?.AppName ?? "BugMine SDK app"
+ });
+ return services;
+ }
+}
+
+/// <summary>
+/// Configuration for the BugMine SDK.
+/// </summary>
+public class BugMineSdkConfiguration {
+ public string AppName { get; set; } = "BugMine SDK app";
+}
\ No newline at end of file
|