diff --git a/WindowsAppCommunity.Sdk b/WindowsAppCommunity.Sdk index d836c4b..6f00d65 160000 --- a/WindowsAppCommunity.Sdk +++ b/WindowsAppCommunity.Sdk @@ -1 +1 @@ -Subproject commit d836c4b6d44205c1c7d347e418327fcf8db55904 +Subproject commit 6f00d65bb59fa9cd9fd84d2f8a801f44c6ad3dc2 diff --git a/src/Commands/Project/ProjectCommandGroup.cs b/src/Commands/Project/ProjectCommandGroup.cs new file mode 100644 index 0000000..3b9257c --- /dev/null +++ b/src/Commands/Project/ProjectCommandGroup.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Ipfs.CoreApi; +using OwlCore.Kubo; +using OwlCore.Nomad.Kubo; +using OwlCore.Nomad.Kubo.Events; +using OwlCore.Storage; +using Remora.Commands.Attributes; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Results; +using WindowsAppCommunity.Discord.ServerCompanion.Services; +using WindowsAppCommunity.Sdk.Nomad; +using static Org.BouncyCastle.Math.EC.ECCurve; + +namespace WindowsAppCommunity.Discord.ServerCompanion.Commands.Project +{ + + public class ProjectCommandGroup(INomadRepoService nomadRepoService, IInteractionContext interactionContext, IFeedbackService feedbackService, IDiscordRestInteractionAPI interactionAPI, IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, ICommandContext context) : Remora.Commands.Groups.CommandGroup + { + [Command("createProject")] + public async Task CreateProjectAsync(string name, string description) + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + if (!context.TryGetChannelID(out var channelId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the channel ID."); + + var knownId = userId.Value.ToString(); + var repoId = knownId; + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(repoId, knownId, CancellationToken.None); + + + var initialMessage = await channelApi.CreateMessageAsync(channelId, "Starting project creation process..."); + + if (!initialMessage.IsSuccess) + return initialMessage; + + var createdProject = await repositoryContainer.ProjectRepository.CreateAsync(new(KnownId: knownId), Config.CancellationToken); + await channelApi.EditMessageAsync(channelId, initialMessage.Entity.ID, $"Created project with ID {createdProject.Id} via known ID {knownId}"); + + await channelApi.EditMessageAsync(channelId, initialMessage.Entity.ID, "Setting name and description..."); + await createdProject.UpdateNameAsync(name, Config.CancellationToken); + await createdProject.UpdateDescriptionAsync(description, Config.CancellationToken); + + // await initialMessage.EditMessageAsync(initialMessage.Entity.ID, "Publishing local event stream to ipns..."); + await createdProject.FlushAsync(Config.CancellationToken); + + await channelApi.EditMessageAsync(channelId, initialMessage.Entity.ID, "Saving repository keys..."); + await repoSettings.SaveAsync(Config.CancellationToken); + + await channelApi.DeleteMessageAsync(channelId, initialMessage.Entity.ID); + return await feedbackService.SendContextualSuccessAsync($"Project created successfully with id '{createdProject.Id}' name '{name}' and description '{description}'"); + } + + [Command("getProject")] + public async Task GetProjectAsync(string projectId) + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + if (!context.TryGetChannelID(out var channelId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the channel ID."); + var repoId = string.Empty; + var knownId = repoId = userId.Value.ToString(); + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(repoId, knownId, CancellationToken.None); + + var initialMessage = await channelApi.CreateMessageAsync(channelId, $"Getting project {projectId}"); + + var project = await repositoryContainer.ProjectRepository.GetAsync(projectId, Config.CancellationToken); + var responseBuilder = new StringBuilder(); + + responseBuilder.AppendLine($"Project ID: {project.Id}"); + responseBuilder.AppendLine($"Name: {project.Name}"); + responseBuilder.AppendLine($"Description: {project.Description}"); + responseBuilder.AppendLine($"Extended Description: {project.ExtendedDescription}"); + responseBuilder.AppendLine($"Accent Color: {project.AccentColor}"); + responseBuilder.AppendLine($"Category: {project.Category}"); + + responseBuilder.AppendLine("Features:"); + foreach (var feature in project.Features) + { + responseBuilder.AppendLine($" - {feature}"); + } + + responseBuilder.AppendLine("Links:"); + foreach (var link in project.Links) + { + responseBuilder.AppendLine($" - ID: {link.Id}"); + responseBuilder.AppendLine($" Name: {link.Name}"); + responseBuilder.AppendLine($" Description: {link.Description}"); + responseBuilder.AppendLine($" URL: {link.Url}"); + } + + responseBuilder.AppendLine("Images:"); + await foreach (var image in project.GetImageFilesAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {image.Id}"); + responseBuilder.AppendLine($" Name: {image.Name}"); + + var cid = await image.GetCidAsync(Config.Client, new AddFileOptions { Pin = Config.KuboOptions.ShouldPin }, Config.CancellationToken); + responseBuilder.AppendLine($" CID: {cid}"); + responseBuilder.AppendLine($" Type: {image.GetType()}"); + } + + responseBuilder.AppendLine("Connections:"); + await foreach (var connection in project.GetConnectionsAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {connection.Id}"); + responseBuilder.AppendLine($" Value: {await connection.GetValueAsync(Config.CancellationToken)}"); + } + + responseBuilder.AppendLine("Users:"); + await foreach (var user in project.GetUsersAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {user.Id}"); + responseBuilder.AppendLine($" Name: {user.Name}"); + responseBuilder.AppendLine($" Role:"); + responseBuilder.AppendLine($" ID: {user.Role.Id}"); + responseBuilder.AppendLine($" Name: {user.Role.Name}"); + responseBuilder.AppendLine($" Description: {user.Role.Description}"); + } + + responseBuilder.AppendLine("Publisher:"); + var publisher = await project.GetPublisherAsync(Config.CancellationToken); + if (publisher is not null) + { + responseBuilder.AppendLine($" - ID: {publisher.Id}"); + responseBuilder.AppendLine($" Name: {publisher.Name}"); + } + + responseBuilder.AppendLine("Dependencies:"); + await foreach (var dependency in project.Dependencies.GetProjectsAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {dependency.Id}"); + responseBuilder.AppendLine($" Name: {dependency.Name}"); + } + + await channelApi.DeleteMessageAsync(channelId, initialMessage.Entity.ID); + + return await feedbackService.SendContextualSuccessAsync(responseBuilder.ToString()); + } + + [Command("listProject")] + public async Task ListProjectAsync() + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + if (!context.TryGetChannelID(out var channelId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the channel ID."); + var repoId = string.Empty; + var knownId = repoId = userId.Value.ToString(); + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(repoId, knownId, CancellationToken.None); + + var initialMessage = await channelApi.CreateMessageAsync(channelId, $"Listing projects..."); + + var responseBuilder = new StringBuilder(); + await foreach (var project in repositoryContainer.ProjectRepository.GetAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($"{nameof(project.Id)}: {project.Id}"); + responseBuilder.AppendLine($"{nameof(project.Name)}: {project.Name}"); + } + responseBuilder.AppendLine($"Finished listing projects for repository {repoId}"); + + await channelApi.DeleteMessageAsync(channelId, initialMessage.Entity.ID); + return await feedbackService.SendContextualSuccessAsync(responseBuilder.ToString()); + } + } +} diff --git a/src/Commands/Publisher/PublisherCommandGroup.cs b/src/Commands/Publisher/PublisherCommandGroup.cs new file mode 100644 index 0000000..4376557 --- /dev/null +++ b/src/Commands/Publisher/PublisherCommandGroup.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Ipfs.CoreApi; +using OwlCore.Kubo; +using OwlCore.Nomad.Kubo; +using OwlCore.Nomad.Kubo.Events; +using OwlCore.Storage; +using Remora.Commands.Attributes; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Results; +using WindowsAppCommunity.Discord.ServerCompanion.Services; +using static Org.BouncyCastle.Math.EC.ECCurve; + +namespace WindowsAppCommunity.Discord.ServerCompanion.Commands.Publisher +{ + public class PublisherCommandGroup(INomadRepoService nomadRepoService, IInteractionContext interactionContext, IFeedbackService feedbackService, IDiscordRestInteractionAPI interactionAPI, IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, ICommandContext context) : Remora.Commands.Groups.CommandGroup + { + [Command("createPublisher")] + public async Task CreatePublisherAsync(string repoId, string name, string description) + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + if (!context.TryGetChannelID(out var channelId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the channel ID."); + + var knownId = userId.Value.ToString(); + + var initialMessage = await channelApi.CreateMessageAsync(channelId, $"Starting publisher creation for repository {repoId}..."); + if (!initialMessage.IsSuccess) + return initialMessage; + + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(repoId, knownId, CancellationToken.None); + + + await channelApi.EditMessageAsync(channelId, initialMessage.Entity.ID, "Creating publisher..."); + var createdPublisher = await repositoryContainer.PublisherRepository.CreateAsync(new(KnownId: knownId), Config.CancellationToken); + await channelApi.EditMessageAsync(channelId, initialMessage.Entity.ID, $"Created publisher with ID {createdPublisher.Id}"); + + await channelApi.EditMessageAsync(channelId, initialMessage.Entity.ID, "Setting name and description..."); + await createdPublisher.UpdateNameAsync(name, Config.CancellationToken); + await createdPublisher.UpdateDescriptionAsync(description, Config.CancellationToken); + + await channelApi.EditMessageAsync(channelId, initialMessage.Entity.ID, "Publishing local event stream..."); + await createdPublisher.PublishLocalAsync(Config.CancellationToken); + + await channelApi.EditMessageAsync(channelId, initialMessage.Entity.ID, "Publishing roaming value..."); + await createdPublisher.PublishRoamingAsync(Config.CancellationToken); + + await channelApi.EditMessageAsync(channelId, initialMessage.Entity.ID, "Saving repository keys..."); + await repoSettings.SaveAsync(Config.CancellationToken); + + await channelApi.DeleteMessageAsync(channelId, initialMessage.Entity.ID); + return await feedbackService.SendContextualSuccessAsync( + $"Publisher created successfully with ID '{createdPublisher.Id}', name '{name}', and description '{description}'."); + } + + + [Command("getPublisher")] + public async Task GetPublisherAsync(string publisherId) + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + if (!context.TryGetChannelID(out var channelId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the channel ID."); + + var repoId = userId.Value.ToString(); + var knownId = repoId; + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(repoId, knownId, CancellationToken.None); + + var initialMessage = await channelApi.CreateMessageAsync(channelId, $"Getting publisher {publisherId}"); + + var publisher = await repositoryContainer.PublisherRepository.GetAsync(publisherId, Config.CancellationToken); + var responseBuilder = new StringBuilder(); + + responseBuilder.AppendLine($"Publisher ID: {publisher.Id}"); + responseBuilder.AppendLine($"Name: {publisher.Name}"); + responseBuilder.AppendLine($"Description: {publisher.Description}"); + responseBuilder.AppendLine($"Extended Description: {publisher.ExtendedDescription}"); + responseBuilder.AppendLine($"Accent Color: {publisher.AccentColor}"); + + responseBuilder.AppendLine("Links:"); + foreach (var link in publisher.Links) + { + responseBuilder.AppendLine($" - ID: {link.Id}"); + responseBuilder.AppendLine($" Name: {link.Name}"); + responseBuilder.AppendLine($" Description: {link.Description}"); + responseBuilder.AppendLine($" URL: {link.Url}"); + } + + responseBuilder.AppendLine("Images:"); + await foreach (var image in publisher.GetImageFilesAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {image.Id}"); + responseBuilder.AppendLine($" Name: {image.Name}"); + + var cid = await image.GetCidAsync(Config.Client, new AddFileOptions { Pin = Config.KuboOptions.ShouldPin }, Config.CancellationToken); + responseBuilder.AppendLine($" CID: {cid}"); + responseBuilder.AppendLine($" Type: {image.GetType()}"); + } + + responseBuilder.AppendLine("Connections:"); + await foreach (var connection in publisher.GetConnectionsAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {connection.Id}"); + responseBuilder.AppendLine($" Value: {await connection.GetValueAsync(Config.CancellationToken)}"); + } + + responseBuilder.AppendLine("Projects:"); + await foreach (var project in publisher.GetProjectsAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {project.Id}"); + responseBuilder.AppendLine($" Name: {project.Name}"); + } + + responseBuilder.AppendLine("Users:"); + await foreach (var user in publisher.GetUsersAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {user.Id}"); + responseBuilder.AppendLine($" Name: {user.Name}"); + responseBuilder.AppendLine($" Role:"); + responseBuilder.AppendLine($" ID: {user.Role.Id}"); + responseBuilder.AppendLine($" Name: {user.Role.Name}"); + responseBuilder.AppendLine($" Description: {user.Role.Description}"); + } + + responseBuilder.AppendLine("Parent Publishers:"); + await foreach (var parentPublisher in publisher.ParentPublishers.GetPublishersAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {parentPublisher.Id}"); + responseBuilder.AppendLine($" Name: {parentPublisher.Name}"); + } + + responseBuilder.AppendLine("Child Publishers:"); + await foreach (var childPublisher in publisher.ChildPublishers.GetPublishersAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {childPublisher.Id}"); + responseBuilder.AppendLine($" Name: {childPublisher.Name}"); + } + + await channelApi.DeleteMessageAsync(channelId, initialMessage.Entity.ID); + + return await feedbackService.SendContextualSuccessAsync(responseBuilder.ToString()); + } + + + [Command("listPublisher")] + public async Task ListPublisherAsync() + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + if (!context.TryGetChannelID(out var channelId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the channel ID."); + + var repoId = userId.Value.ToString(); + var knownId = repoId; + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(repoId, knownId, CancellationToken.None); + + var initialMessage = await channelApi.CreateMessageAsync(channelId, "Listing publishers..."); + + var responseBuilder = new StringBuilder(); + await foreach (var publisher in repositoryContainer.PublisherRepository.GetAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($"{nameof(publisher.Id)}: {publisher.Id}"); + responseBuilder.AppendLine($"{nameof(publisher.Name)}: {publisher.Name}"); + } + responseBuilder.AppendLine($"Finished listing publishers for repository {repoId}"); + + await channelApi.DeleteMessageAsync(channelId, initialMessage.Entity.ID); + return await feedbackService.SendContextualSuccessAsync(responseBuilder.ToString()); + } + + + } +} diff --git a/src/Commands/Repo/RepoCommandGroup.cs b/src/Commands/Repo/RepoCommandGroup.cs new file mode 100644 index 0000000..e5b0e1b --- /dev/null +++ b/src/Commands/Repo/RepoCommandGroup.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using OwlCore.Nomad.Kubo.Events; +using Remora.Commands.Attributes; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Results; +using WindowsAppCommunity.Discord.ServerCompanion.Services; +using WindowsAppCommunity.Sdk.Nomad; + +namespace WindowsAppCommunity.Discord.ServerCompanion.Commands.Repo +{ + public class RepoCommandGroup(INomadRepoService nomadRepoService, IInteractionContext interactionContext, IFeedbackService feedbackService, IDiscordRestInteractionAPI interactionAPI, IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, ICommandContext context) : Remora.Commands.Groups.CommandGroup + { + [Command("createRepo")] + public async Task CreateRepoAsync() + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + var knownId = Guid.NewGuid().ToString(); + var repoId = knownId; + var repo = await nomadRepoService.CreateRepoAsync(knownId, CancellationToken.None); + + + return await feedbackService.SendContextualSuccessAsync($"Repository created with id {repo}"); + } + + [Command("getRepo")] + public async Task GetRepoAsync(string repoId) + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + if (!context.TryGetChannelID(out var channelId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the channel ID."); + + var knownId = repoId; + // var repoId = knownId; + + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(repoId, knownId, CancellationToken.None); + + var initialMessage = await channelApi.CreateMessageAsync(channelId, $"Getting repository {userId}"); + + var savedMissingFromRepoConfigs = repositoryContainer.PublisherRepository.ManagedConfigs + .Where(x => repoSettings.ManagedPublisherConfigs.All(y => x.RoamingId != y.RoamingId)) + .ToList(); + + var repoMissingFromSavedConfigs = repoSettings.ManagedPublisherConfigs + .Where(x => repositoryContainer.PublisherRepository.ManagedConfigs.All(y => x.RoamingId != y.RoamingId)) + .ToList(); + + foreach (var item in savedMissingFromRepoConfigs) + repositoryContainer.PublisherRepository.ManagedConfigs.Add(item); + + var responseBuilder = new StringBuilder(); + responseBuilder.AppendLine($"Repository ID: {userId}"); + responseBuilder.AppendLine($"Synced missing configs: {savedMissingFromRepoConfigs.Count}"); + responseBuilder.AppendLine($"Configs missing from repo: {repoMissingFromSavedConfigs.Count}"); + + responseBuilder.AppendLine("Users:"); + await foreach (var user in repositoryContainer.UserRepository.GetAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {user.Id}"); + } + + responseBuilder.AppendLine("Projects:"); + await foreach (var project in repositoryContainer.ProjectRepository.GetAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {project.Id}"); + } + + responseBuilder.AppendLine("Publishers:"); + await foreach (var publisher in repositoryContainer.PublisherRepository.GetAsync(Config.CancellationToken)) + { + responseBuilder.AppendLine($" - ID: {publisher.Id}"); + } + + foreach (var item in repoMissingFromSavedConfigs) + repoSettings.ManagedPublisherConfigs = [.. repoSettings.ManagedPublisherConfigs, item]; + + await repoSettings.SaveAsync(); + responseBuilder.AppendLine("Repository store saved successfully."); + + // Clean up initial message + await channelApi.DeleteMessageAsync(channelId, initialMessage.Entity.ID); + + return await feedbackService.SendContextualSuccessAsync(responseBuilder.ToString()); + } + + + [Command("listRepo")] + public async Task ListRepoAsync() + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + var knownId = Guid.NewGuid().ToString(); + var repoId = knownId; + var repo = await nomadRepoService.ListRepoAsync(CancellationToken.None); + + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("Repositories:"); + foreach (var item in repo) + { + stringBuilder.AppendLine($" - {item}"); + } + + return await feedbackService.SendContextualSuccessAsync($"Repository list {stringBuilder.ToString()}"); + } + + [Command("deleteRepo")] + public async Task DeleteRepoAsync(string repoId) + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + var isDeleted = await nomadRepoService.DeleteRepoAsync(repoId, CancellationToken.None); + + if(isDeleted) + return await feedbackService.SendContextualSuccessAsync($"Repository {repoId} deleted successfully."); + else + return await feedbackService.SendContextualErrorAsync($"Failed to delete repository {repoId}."); + } + } +} diff --git a/src/Commands/User/UserCommandGroup.cs b/src/Commands/User/UserCommandGroup.cs new file mode 100644 index 0000000..07ee190 --- /dev/null +++ b/src/Commands/User/UserCommandGroup.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Ipfs.CoreApi; +using Org.BouncyCastle.Ocsp; +using OwlCore.Diagnostics; +using OwlCore.Kubo; +using OwlCore.Nomad.Kubo; +using OwlCore.Nomad.Kubo.Events; +using OwlCore.Storage; +using Remora.Commands.Attributes; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Results; +using WindowsAppCommunity.Discord.ServerCompanion.Services; +using WindowsAppCommunity.Sdk.Nomad; +using static Org.BouncyCastle.Math.EC.ECCurve; +using WindowsAppCommunity.Discord.ServerCompanion.Extensions; + +namespace WindowsAppCommunity.Discord.ServerCompanion.Commands.User +{ + public class UserCommandGroup(INomadRepoService nomadRepoService, IInteractionContext interactionContext, IFeedbackService feedbackService, IDiscordRestInteractionAPI interactionAPI, IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, ICommandContext context) : Remora.Commands.Groups.CommandGroup + { + [Command("createUser")] + public async Task CreateUser(string name, string description) + { + if (!context.TryGetUserID(out var userId)) + return await feedbackService.SendContextualErrorAsync("Could not determine the user ID."); + + var knownId = userId.Value.ToString(); + var repoId = knownId; + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(repoId, knownId, CancellationToken.None); + + var createdUser = await repositoryContainer.UserRepository.CreateAsync(new(KnownId: knownId), Config.CancellationToken); + + Logger.LogInformation($"Setting name and description"); + + if (!string.IsNullOrWhiteSpace(name)) + await createdUser.UpdateNameAsync(name, Config.CancellationToken); + + if (!string.IsNullOrWhiteSpace(description)) + await createdUser.UpdateDescriptionAsync(description, Config.CancellationToken); + + Logger.LogInformation($"Publishing local event stream to ipns"); + await createdUser.PublishLocalAsync(Config.CancellationToken); + + Logger.LogInformation($"Publishing roaming value to ipns"); + await createdUser.PublishRoamingAsync(Config.CancellationToken); + + Logger.LogInformation($"Saving repository keys"); + await repoSettings.SaveAsync(Config.CancellationToken); + + return await feedbackService.SendContextualSuccessAsync($"User created with id {createdUser.Id}"); + } + + [Command("getUser")] + public async Task GetUser(string userId) + { + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(userId, userId, CancellationToken.None); + + Logger.LogInformation($"Getting user {userId}"); + var user = await repositoryContainer.UserRepository.GetAsync(userId, Config.CancellationToken); + { + Logger.LogInformation($"{nameof(user.Id)}: {user.Id}"); + Logger.LogInformation($"{nameof(user.Name)}: {user.Name}"); + Logger.LogInformation($"{nameof(user.Description)}: {user.Description}"); + Logger.LogInformation($"{nameof(user.ExtendedDescription)}: {user.ExtendedDescription}"); + + Logger.LogInformation($"{nameof(user.Links)}:"); + foreach (var link in user.Links) + { + Logger.LogInformation($"- {nameof(link.Id)}: {link.Id}"); + Logger.LogInformation($" {nameof(link.Name)}: {link.Name}"); + Logger.LogInformation($" {nameof(link.Description)}: {link.Description}"); + Logger.LogInformation($" {nameof(link.Url)}: {link.Url}"); + } + + Logger.LogInformation($"{nameof(user.GetImageFilesAsync)}:"); + await foreach (var image in user.GetImageFilesAsync(Config.CancellationToken)) + { + Logger.LogInformation($"- {nameof(image.Id)}: {image.Id}"); + Logger.LogInformation($" {nameof(image.Name)}: {image.Name}"); + + var cid = await image.GetCidAsync(Config.Client, new AddFileOptions { Pin = Config.KuboOptions.ShouldPin }, Config.CancellationToken); + Logger.LogInformation($" {nameof(StorableKuboExtensions.GetCidAsync)}: {cid}"); + Logger.LogInformation($" Type: {image.GetType()}"); + } + + Logger.LogInformation($"{nameof(user.GetConnectionsAsync)}:"); + await foreach (var connection in user.GetConnectionsAsync(Config.CancellationToken)) + { + Logger.LogInformation($"- {nameof(connection.Id)}: {connection.Id}"); + Logger.LogInformation($" {nameof(connection.GetValueAsync)}: {await connection.GetValueAsync(Config.CancellationToken)}"); + } + + Logger.LogInformation($"{nameof(user.GetPublishersAsync)}:"); + await foreach (var publisher in user.GetPublishersAsync(Config.CancellationToken)) + { + Logger.LogInformation($"- {nameof(publisher.Id)}: {publisher.Id}"); + Logger.LogInformation($" {nameof(publisher.Name)}: {publisher.Name}"); + Logger.LogInformation($" {nameof(publisher.Role)}:"); + Logger.LogInformation($" {nameof(publisher.Role.Id)}: {publisher.Role.Id}"); + Logger.LogInformation($" {nameof(publisher.Role.Name)}: {publisher.Role.Name}"); + Logger.LogInformation($" {nameof(publisher.Role.Description)}: {publisher.Role.Description}"); + } + + Logger.LogInformation($"{nameof(user.GetProjectsAsync)}:"); + await foreach (var project in user.GetProjectsAsync(Config.CancellationToken)) + { + Logger.LogInformation($"- {nameof(project.Id)}: {project.Id}"); + Logger.LogInformation($" {nameof(project.Name)}: {project.Name}"); + Logger.LogInformation($" {nameof(project.Role)}:"); + Logger.LogInformation($" {nameof(project.Role.Id)}: {project.Role.Id}"); + Logger.LogInformation($" {nameof(project.Role.Name)}: {project.Role.Name}"); + Logger.LogInformation($" {nameof(project.Role.Description)}: {project.Role.Description}"); + } + } + return await feedbackService.SendContextualSuccessAsync($"User {user.Id}, Username {user.Name}, User Description {user.Description}"); + } + + [Command("listUser")] + public async Task ListUser(string repoId) + { + var (repositoryContainer, Config, repoSettings) = await nomadRepoService.GetNomadRepoAsync(repoId, repoId, CancellationToken.None); + var response = new StringBuilder($"Listing users for repository {repoId}\n"); + Logger.LogInformation($"Listing users for repository {repoId}"); + await foreach (var user in repositoryContainer.UserRepository.GetAsync(Config.CancellationToken)) + { + response.Append($"{nameof(user.Id)}: {user.Id}\n"); + response.Append($"{nameof(user.Name)}: {user.Name}\n"); + } + response.Append($"Finished listing users for repository {repoId}"); + + return await feedbackService.SendContextualSuccessAsync(response.ToString()); + } + } +} diff --git a/src/Program.cs b/src/Program.cs index 595c0f5..929da2b 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,7 +1,10 @@ -using Microsoft.Extensions.Configuration; +using System.Threading; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OwlCore.Diagnostics; +using OwlCore.Kubo; +using OwlCore.Nomad.Kubo; using OwlCore.Storage.System.IO; using Remora.Commands.Extensions; using Remora.Discord.API.Abstractions.Gateway.Commands; @@ -15,7 +18,15 @@ using WindowsAppCommunity.Discord.ServerCompanion; using WindowsAppCommunity.Discord.ServerCompanion.Autocomplete; using WindowsAppCommunity.Discord.ServerCompanion.Commands; +using WindowsAppCommunity.Discord.ServerCompanion.Commands.User; using WindowsAppCommunity.Discord.ServerCompanion.Interactivity; +using WindowsAppCommunity.Sdk.Nomad; +using Org.BouncyCastle.Ocsp; +using OwlCore.Storage; +using WindowsAppCommunity.Discord.ServerCompanion.Services; +using WindowsAppCommunity.Discord.ServerCompanion.Commands.Project; +using WindowsAppCommunity.Discord.ServerCompanion.Commands.Repo; +using WindowsAppCommunity.Discord.ServerCompanion.Commands.Publisher; // Cancellation setup var cancellationSource = new CancellationTokenSource(); @@ -65,22 +76,69 @@ var appData = new SystemFolder(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); var serverCompanionData = (SystemFolder)await appData.CreateFolderAsync("WindowsAppCommunity.Discord.ServerCompanion", overwrite: false, cancelTok); -// Service setup and init +// Cancellation +var cancellationTokenSource = new CancellationTokenSource(); +var cancellationToken = cancellationTokenSource.Token; +Console.CancelKeyPress += (sender, eventArgs) => +{ + eventArgs.Cancel = true; + cancellationTokenSource.Cancel(); +}; + +// Storage setup +var userProfileFolder = new SystemFolder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); +var wacsdkRepoFolder = (SystemFolder)await userProfileFolder.CreateFolderAsync(".wacsdk", overwrite: false, cancellationToken); + +// Dedicated ipfs repo for testing in. +var kuboRepoFolder = (SystemFolder)await wacsdkRepoFolder.CreateFolderAsync(".ipfs", overwrite: false, cancellationToken); + +// Bootstrap and start Kubo +var kubo = new KuboBootstrapper(kuboRepoFolder.Path) +{ + GatewayUri = new Uri("http://127.0.0.1:8025"), + ApiUri = new Uri("http://127.0.0.1:5025"), + GatewayUriMode = ConfigMode.OverwriteExisting, + ApiUriMode = ConfigMode.OverwriteExisting, + LaunchConflictMode = BootstrapLaunchConflictMode.Attach, + RoutingMode = DhtRoutingMode.None, +}; +await kubo.StartAsync(cancellationToken); + +// Command data / config +var commandConfig = new WacsdkCommandConfig +{ + CancellationToken = cancellationToken, + KuboOptions = new KuboOptions + { + IpnsLifetime = TimeSpan.FromDays(1), + ShouldPin = false, + UseCache = false, + }, + Client = kubo.Client, + RepositoryStorage = wacsdkRepoFolder, +}; + +// Service setup and init var services = new ServiceCollection() - .AddSingleton(config) - .AddDiscordGateway(_ => botToken) - .AddDiscordCommands(enableSlash: true) - .AddInteractivity() - .AddInteractionGroup() - .AddCommands() - .AddCommandTree() - .WithCommandGroup() - .WithCommandGroup() - .Finish() - .AddResponder() - .Configure(g => g.Intents |= GatewayIntents.MessageContents) - .AddAutocompleteProvider() - .BuildServiceProvider(); + .AddSingleton(config) + .AddDiscordGateway(_ => botToken) + .AddDiscordCommands(enableSlash: true) + .AddInteractivity() + .AddInteractionGroup() + .AddCommands() + .AddCommandTree() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .Finish() + .AddResponder() + .Configure(g => g.Intents |= GatewayIntents.MessageContents) + .AddAutocompleteProvider() + .AddSingleton(provider => new NomadRepoService(commandConfig)) // Pass WacsdkCommandConfig to INomadRepoService + .BuildServiceProvider(); var log = services.GetRequiredService>(); var gatewayClient = services.GetRequiredService(); diff --git a/src/Services/INomadRepoService.cs b/src/Services/INomadRepoService.cs new file mode 100644 index 0000000..1410364 --- /dev/null +++ b/src/Services/INomadRepoService.cs @@ -0,0 +1,17 @@ +using WindowsAppCommunity.Sdk.Nomad; + +namespace WindowsAppCommunity.Discord.ServerCompanion.Services +{ + public interface INomadRepoService + { + public Task<(RepositoryContainer, WacsdkCommandConfig, WacsdkNomadSettings)> GetNomadRepoAsync(string repoId, string knownId, CancellationToken token); + Task CreateRepoAsync(string repoId, CancellationToken token); + + Task> ListRepoAsync(CancellationToken token); + + Task DeleteRepoAsync(string repoId,CancellationToken token); + + public WacsdkCommandConfig Config { get; } + + } +} \ No newline at end of file diff --git a/src/Services/NomadRepoService.cs b/src/Services/NomadRepoService.cs new file mode 100644 index 0000000..faebb14 --- /dev/null +++ b/src/Services/NomadRepoService.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using OwlCore.Diagnostics; +using OwlCore.Storage; +using WindowsAppCommunity.Sdk.Nomad; + +namespace WindowsAppCommunity.Discord.ServerCompanion.Services +{ + public class NomadRepoService : INomadRepoService + { + public WacsdkCommandConfig Config { get; } + + public NomadRepoService(WacsdkCommandConfig config) + { + Config = config; + } + + public async Task<(RepositoryContainer, WacsdkCommandConfig, WacsdkNomadSettings)> GetNomadRepoAsync(string repoId, string knownId, CancellationToken token) + { + if (token.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + + var thisRepoStorage = (IModifiableFolder)await Config.RepositoryStorage.CreateFolderAsync(repoId, overwrite: false); + + Logger.LogInformation($"Getting repo store with ID {repoId} at {thisRepoStorage.GetType().Name} {thisRepoStorage.Id}"); + var repoSettings = new WacsdkNomadSettings(thisRepoStorage); + await repoSettings.LoadAsync(); + + var repositoryContainer = new RepositoryContainer(Config.KuboOptions, Config.Client, repoSettings.ManagedKeys, repoSettings.ManagedUserConfigs, repoSettings.ManagedProjectConfigs, repoSettings.ManagedPublisherConfigs); + + return (repositoryContainer, Config, repoSettings); + } + + public async Task CreateRepoAsync(string repoId, CancellationToken token) + { + if (token.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + + var thisRepoStorage = (IModifiableFolder)await Config.RepositoryStorage.CreateFolderAsync(repoId, overwrite: false); + + return thisRepoStorage.Id; + } + + public async Task> ListRepoAsync(CancellationToken token) + { + var repoIds = new List(); + Logger.LogInformation($"Listing repositories"); + await foreach (var item in Config.RepositoryStorage.GetFoldersAsync(Config.CancellationToken)) + { + repoIds.Add(item.Id); + } + + return repoIds; + } + + public async Task DeleteRepoAsync(string repoId, CancellationToken token) + { + var existingItem = await Config.RepositoryStorage.GetFirstByNameAsync(repoId); + + if (existingItem == null) + return false; + + await Config.RepositoryStorage.DeleteAsync(existingItem); + Logger.LogInformation($"Deleted repo store with ID {repoId} at {existingItem.Id}"); + + return true; + } + } +} diff --git a/src/Settings/NewtonsoftSerializer.cs b/src/Settings/NewtonsoftSerializer.cs new file mode 100644 index 0000000..d308f01 --- /dev/null +++ b/src/Settings/NewtonsoftSerializer.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using OwlCore.ComponentModel; +using OwlCore.Extensions; +using System.Text; + +namespace WindowsAppCommunity.Discord.ServerCompanion.Services; + +/// +/// An and implementation for serializing and deserializing streams using System.Text.Json. +/// +public class NewtonsoftSerializer : IAsyncSerializer, ISerializer +{ + /// + /// A singleton instance for . + /// + public static NewtonsoftSerializer Singleton { get; } = new(); + + /// + public Task SerializeAsync(T data, CancellationToken? cancellationToken = null) => Task.Run(() => Serialize(data), cancellationToken ?? CancellationToken.None); + + /// + public Task SerializeAsync(Type inputType, object data, CancellationToken? cancellationToken = null) => Task.Run(() => Serialize(inputType, data), cancellationToken ?? CancellationToken.None); + + /// + public Task DeserializeAsync(Stream serialized, CancellationToken? cancellationToken = null) => Task.Run(() => Deserialize(serialized), cancellationToken ?? CancellationToken.None); + + /// + public Task DeserializeAsync(Type returnType, Stream serialized, CancellationToken? cancellationToken = null) => Task.Run(() => Deserialize(returnType, serialized), cancellationToken ?? CancellationToken.None); + + /// + public Stream Serialize(T data) + { + var res = JsonConvert.SerializeObject(data, typeof(T), null); + return new MemoryStream(Encoding.UTF8.GetBytes(res)); + } + + /// + public Stream Serialize(Type type, object data) + { + var res = JsonConvert.SerializeObject(data, type, null); + return new MemoryStream(Encoding.UTF8.GetBytes(res)); + } + + /// + public TResult Deserialize(Stream serialized) + { + serialized.Position = 0; + var str = Encoding.UTF8.GetString(serialized.ToBytes()); + return (TResult)JsonConvert.DeserializeObject(str, typeof(TResult))!; + } + + /// + public object Deserialize(Type type, Stream serialized) + { + serialized.Position = 0; + var str = Encoding.UTF8.GetString(serialized.ToBytes()); + return JsonConvert.DeserializeObject(str, type)!; + } +} diff --git a/src/WacsdkCommandConfig.cs b/src/WacsdkCommandConfig.cs new file mode 100644 index 0000000..cb6b218 --- /dev/null +++ b/src/WacsdkCommandConfig.cs @@ -0,0 +1,11 @@ +using Ipfs.Http; +using OwlCore.Nomad.Kubo; +using OwlCore.Storage.System.IO; + +public class WacsdkCommandConfig +{ + public CancellationToken CancellationToken { get; set; } + public KuboOptions KuboOptions { get; set; } + public IpfsClient Client { get; set; } + public SystemFolder RepositoryStorage { get; set; } +} \ No newline at end of file diff --git a/src/WacsdkNomadSettings.cs b/src/WacsdkNomadSettings.cs new file mode 100644 index 0000000..ff32dbe --- /dev/null +++ b/src/WacsdkNomadSettings.cs @@ -0,0 +1,77 @@ +using OwlCore.ComponentModel; +using OwlCore.Kubo; +using OwlCore.Nomad.Kubo; +using OwlCore.Storage; +using WindowsAppCommunity.Discord.ServerCompanion.Services; + +public class WacsdkNomadSettings : SettingsBase +{ + public WacsdkNomadSettings(IModifiableFolder folder) + : base(folder, NewtonsoftSerializer.Singleton) + { + } + + public List ManagedKeys + { + get => GetSetting>(() => []); + set => SetSetting(value); + } + + public List> ManagedUserConfigs + { + get => GetSetting>>(() => []); + set => SetSetting(value); + } + + public List> ManagedProjectConfigs + { + get => GetSetting>>(() => []); + set => SetSetting(value); + } + + public List> ManagedPublisherConfigs + { + get => GetSetting>>(() => []); + set => SetSetting(value); + } + + public override async Task LoadAsync(CancellationToken? cancellationToken = null) + { + cancellationToken?.ThrowIfCancellationRequested(); + + await base.LoadAsync(cancellationToken); + + // Roaming value is used as the initial value for the event stream handlers. + // Clear it out to ensure we start event playback from a clean slate. + foreach (var config in ManagedUserConfigs) + { + config.RoamingValue = new WindowsAppCommunity.Sdk.Models.User + { + Sources = [], + }; + + config.LocalValue = null; + config.ResolvedEventStreamEntries = null; + } + + foreach (var config in ManagedProjectConfigs) + { + config.RoamingValue = new WindowsAppCommunity.Sdk.Models.Project + { + Sources = [], + }; + config.LocalValue = null; + config.ResolvedEventStreamEntries = null; + } + + foreach (var config in ManagedPublisherConfigs) + { + config.RoamingValue = new WindowsAppCommunity.Sdk.Models.Publisher + { + Sources = [], + }; + config.LocalValue = null; + config.ResolvedEventStreamEntries = null; + } + } +}