From d10417339b76bf2750f3e54f4e3b714dd3ed369a Mon Sep 17 00:00:00 2001 From: Rory& Date: Wed, 4 Sep 2024 05:02:20 +0200 Subject: Changes --- .gitignore | 12 +- .idea/.idea.ModerationClient/.idea/avalonia.xml | 3 + FilesystemBenchmark/Benchmarks.cs | 134 ++++++++++++++++++ FilesystemBenchmark/FilesystemBenchmark.csproj | 19 +++ FilesystemBenchmark/Program.cs | 8 ++ LibMatrix | 2 +- ModerationClient.sln | 14 +- ModerationClient/App.axaml.cs | 12 +- ModerationClient/Models/SpaceTreeNodes/RoomNode.cs | 14 ++ .../Models/SpaceTreeNodes/SpaceNode.cs | 15 ++ ModerationClient/ModerationClient.csproj | 1 - ModerationClient/Program.cs | 19 ++- ModerationClient/Services/ClientContainer.cs | 8 ++ .../Services/CommandLineConfiguration.cs | 33 +++-- ModerationClient/Services/FileStorageProvider.cs | 68 +++++++-- .../Services/MatrixAuthenticationService.cs | 4 - .../Services/ModerationClientConfiguration.cs | 5 - ModerationClient/Services/StatusBarService.cs | 19 +++ ModerationClient/Services/TestRunner.cs | 36 +++++ ModerationClient/ViewModels/ClientViewModel.cs | 155 ++++++++++++++++----- ModerationClient/ViewModels/MainWindowViewModel.cs | 5 +- .../UserManagement/UserManagementViewModel.cs | 13 +- ModerationClient/Views/MainWindow/ClientView.axaml | 20 ++- .../Views/MainWindow/ClientView.axaml.cs | 1 - .../Views/MainWindow/LoginView.axaml.cs | 2 +- ModerationClient/Views/MainWindow/MainWindow.axaml | 37 ++--- .../Views/MainWindow/MainWindow.axaml.cs | 113 ++++++++++----- Test/Program.cs | 21 +++ Test/Test.csproj | 18 +++ 29 files changed, 658 insertions(+), 153 deletions(-) create mode 100644 FilesystemBenchmark/Benchmarks.cs create mode 100644 FilesystemBenchmark/FilesystemBenchmark.csproj create mode 100644 FilesystemBenchmark/Program.cs create mode 100644 ModerationClient/Models/SpaceTreeNodes/RoomNode.cs create mode 100644 ModerationClient/Models/SpaceTreeNodes/SpaceNode.cs create mode 100644 ModerationClient/Services/ClientContainer.cs create mode 100644 ModerationClient/Services/StatusBarService.cs create mode 100644 ModerationClient/Services/TestRunner.cs create mode 100644 Test/Program.cs create mode 100644 Test/Test.csproj diff --git a/.gitignore b/.gitignore index a6807bd..668274e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ -# Dotnet build outputs +# Regular dotnet things **/bin/ **/obj/ +**/*.[Dd]ot[Ss]ettings.[Uu]ser +**/BenchmarkDotNet.Artifacts/ -# User files -*.DotSettings.user -*.patch +# Local files /patches/ - -# Local files: appsettings.Local*.json +appservice.yaml +appservice.json .run/local/*.run.xml \ No newline at end of file diff --git a/.idea/.idea.ModerationClient/.idea/avalonia.xml b/.idea/.idea.ModerationClient/.idea/avalonia.xml index 6bc9c45..b045202 100644 --- a/.idea/.idea.ModerationClient/.idea/avalonia.xml +++ b/.idea/.idea.ModerationClient/.idea/avalonia.xml @@ -8,6 +8,9 @@ + + + diff --git a/FilesystemBenchmark/Benchmarks.cs b/FilesystemBenchmark/Benchmarks.cs new file mode 100644 index 0000000..9ab044a --- /dev/null +++ b/FilesystemBenchmark/Benchmarks.cs @@ -0,0 +1,134 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using BenchmarkDotNet.Attributes; + +namespace FilesystemBenchmark; + +public class Benchmarks { + private string testPath = "/home/Rory/.local/share/ModerationClient/default/syncCache"; + + private EnumerationOptions enumOpts = new EnumerationOptions() { + MatchType = MatchType.Simple, + AttributesToSkip = FileAttributes.None, + IgnoreInaccessible = false, + RecurseSubdirectories = true + }; + + [Benchmark] + public void GetFilesMatching() { + _ = Directory.GetFiles(testPath, "*.*", SearchOption.AllDirectories).Count(); + } + + [Benchmark] + public void EnumerateFilesMatching() { + _ = Directory.EnumerateFiles(testPath, "*.*", SearchOption.AllDirectories).Count(); + } + + [Benchmark] + public void GetFilesMatchingSingleStar() { + _ = Directory.GetFiles(testPath, "*", SearchOption.AllDirectories).Count(); + } + + [Benchmark] + public void EnumerateFilesMatchingSingleStar() { + _ = Directory.EnumerateFiles(testPath, "*", SearchOption.AllDirectories).Count(); + } + + [Benchmark] + public void GetFilesMatchingSingleStarSimple() { + _ = Directory.GetFiles(testPath, "*", new EnumerationOptions() { + MatchType = MatchType.Simple, + AttributesToSkip = FileAttributes.None, + IgnoreInaccessible = false, + RecurseSubdirectories = true + }).Count(); + } + + [Benchmark] + public void EnumerateFilesMatchingSingleStarSimple() { + _ = Directory.EnumerateFiles(testPath, "*", new EnumerationOptions() { + MatchType = MatchType.Simple, + AttributesToSkip = FileAttributes.None, + IgnoreInaccessible = false, + RecurseSubdirectories = true + }).Count(); + } + + [Benchmark] + public void GetFilesMatchingSingleStarSimpleCached() { + _ = Directory.GetFiles(testPath, "*", enumOpts).Count(); + } + + [Benchmark] + public void EnumerateFilesMatchingSingleStarSimpleCached() { + _ = Directory.EnumerateFiles(testPath, "*", enumOpts).Count(); + } + + // [Benchmark] + // public void GetFilesRecursiveFunc() { + // GetFilesRecursive(testPath); + // } + // + // [Benchmark] + // public void GetFilesRecursiveParallelFunc() { + // GetFilesRecursiveParallel(testPath); + // } + // + // [Benchmark] + // public void GetFilesRecursiveEntriesFunc() { + // GetFilesRecursiveEntries(testPath); + // } + // + // [Benchmark] + // public void GetFilesRecursiveAsyncFunc() { + // GetFilesRecursiveAsync(testPath).ToBlockingEnumerable(); + // } + + + private List GetFilesRecursive(string path) { + var result = new List(); + foreach (var dir in Directory.GetDirectories(path)) { + result.AddRange(GetFilesRecursive(dir)); + } + + result.AddRange(Directory.GetFiles(path)); + return result; + } + + private List GetFilesRecursiveEntries(string path) { + var result = new List(); + foreach (var entry in Directory.EnumerateFileSystemEntries(path)) { + if (Directory.Exists(entry)) { + result.AddRange(GetFilesRecursiveEntries(entry)); + } + else { + result.Add(entry); + } + } + + return result; + } + + private List GetFilesRecursiveParallel(string path) { + var result = new ConcurrentBag(); + Directory.GetDirectories(path).AsParallel().ForAll(dir => { + GetFilesRecursiveParallel(dir).ForEach(result.Add); + }); + + Directory.GetFiles(path).AsParallel().ForAll(result.Add); + return result.ToList(); + } + + private async IAsyncEnumerable GetFilesRecursiveAsync(string path) { + foreach (var dir in Directory.GetDirectories(path)) { + foreach (var file in GetFilesRecursiveAsync(dir).ToBlockingEnumerable()) { + yield return file; + } + } + + foreach (var file in Directory.GetFiles(path)) { + yield return file; + } + } +} \ No newline at end of file diff --git a/FilesystemBenchmark/FilesystemBenchmark.csproj b/FilesystemBenchmark/FilesystemBenchmark.csproj new file mode 100644 index 0000000..bb0af83 --- /dev/null +++ b/FilesystemBenchmark/FilesystemBenchmark.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + preview + enable + enable + + + + + + + + + + + diff --git a/FilesystemBenchmark/Program.cs b/FilesystemBenchmark/Program.cs new file mode 100644 index 0000000..9454263 --- /dev/null +++ b/FilesystemBenchmark/Program.cs @@ -0,0 +1,8 @@ +// See https://aka.ms/new-console-template for more information + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; +using FilesystemBenchmark; + +BenchmarkRunner.Run(); \ No newline at end of file diff --git a/LibMatrix b/LibMatrix index bba7333..a8d20e9 160000 --- a/LibMatrix +++ b/LibMatrix @@ -1 +1 @@ -Subproject commit bba7333ee6581a92bbbc7479d72325e704fe7fa6 +Subproject commit a8d20e9d57857296e4600f44807893f4dcad72d1 diff --git a/ModerationClient.sln b/ModerationClient.sln index c23825d..cff05e1 100644 --- a/ModerationClient.sln +++ b/ModerationClient.sln @@ -47,6 +47,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "Lib EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLib.Tests", "LibMatrix\ArcaneLibs\ArcaneLib.Tests\ArcaneLib.Tests.csproj", "{046D35DF-E1F2-41DA-94D3-80CF960C100A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FilesystemBenchmark", "FilesystemBenchmark\FilesystemBenchmark.csproj", "{6F3FA65D-5661-4840-B421-BF64CD4DBBD2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -129,6 +133,14 @@ Global {046D35DF-E1F2-41DA-94D3-80CF960C100A}.Debug|Any CPU.Build.0 = Debug|Any CPU {046D35DF-E1F2-41DA-94D3-80CF960C100A}.Release|Any CPU.ActiveCfg = Release|Any CPU {046D35DF-E1F2-41DA-94D3-80CF960C100A}.Release|Any CPU.Build.0 = Release|Any CPU + {BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}.Release|Any CPU.Build.0 = Release|Any CPU + {6F3FA65D-5661-4840-B421-BF64CD4DBBD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F3FA65D-5661-4840-B421-BF64CD4DBBD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F3FA65D-5661-4840-B421-BF64CD4DBBD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F3FA65D-5661-4840-B421-BF64CD4DBBD2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {6FDD7CD7-4526-410B-858D-35FA8292317C} = {C5771E60-0DA8-42AC-B54B-94C91B5B719B} @@ -154,7 +166,7 @@ Global {046D35DF-E1F2-41DA-94D3-80CF960C100A} = {D7A5A7D3-4D9E-4B15-9C48-5E75A91483CF} EndGlobalSection GlobalSection(RiderSharedRunConfigurations) = postSolution - File = .run\local\ModerationClient (Apothecary) test.run.xml File = .run\local\ModerationClient (Apothecary).run.xml + File = .run\local\ModerationClient (matrix.org).run.xml EndGlobalSection EndGlobal diff --git a/ModerationClient/App.axaml.cs b/ModerationClient/App.axaml.cs index c44b5a2..b15c0fa 100644 --- a/ModerationClient/App.axaml.cs +++ b/ModerationClient/App.axaml.cs @@ -1,11 +1,11 @@ using System; using System.IO; +using System.Threading; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using LibMatrix.Services; -using MatrixUtils.Abstractions; using MatrixUtils.Desktop; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -29,7 +29,6 @@ public partial class App : Application { // ReSharper disable once AsyncVoidMethod public override async void OnFrameworkInitializationCompleted() { var builder = Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder(Environment.GetCommandLineArgs()); - builder.Services.AddTransient(); ConfigureServices(builder.Services); Host = builder.Build(); @@ -47,10 +46,11 @@ public partial class App : Application { } private static IServiceProvider ConfigureServices(IServiceCollection services) { + var cfg = CommandLineConfiguration.FromProcessArgs(); services.AddRoryLibMatrixServices(new() { AppName = "ModerationClient", }); - services.AddSingleton(CommandLineConfiguration.FromProcessArgs()); + services.AddSingleton(cfg); services.AddSingleton(); services.AddSingleton(); @@ -72,9 +72,15 @@ public partial class App : Application { services.AddTransient(); // Register ViewModels + services.AddTransient(); services.AddTransient(); services.AddTransient(); + if (cfg.TestConfiguration is not null) { + services.AddSingleton(cfg.TestConfiguration); + services.AddHostedService(); + } + return services.BuildServiceProvider(); } } \ No newline at end of file diff --git a/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs b/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs new file mode 100644 index 0000000..76d5aa9 --- /dev/null +++ b/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs @@ -0,0 +1,14 @@ +using ArcaneLibs; + +namespace ModerationClient.Models.SpaceTreeNodes; + +public class RoomNode : NotifyPropertyChanged { + private string? _name; + + public string RoomID { get; set; } + + public string? Name { + get => _name; + set => SetField(ref _name, value); + } +} \ No newline at end of file diff --git a/ModerationClient/Models/SpaceTreeNodes/SpaceNode.cs b/ModerationClient/Models/SpaceTreeNodes/SpaceNode.cs new file mode 100644 index 0000000..b8042ae --- /dev/null +++ b/ModerationClient/Models/SpaceTreeNodes/SpaceNode.cs @@ -0,0 +1,15 @@ +using System.Collections.ObjectModel; + +namespace ModerationClient.Models.SpaceTreeNodes; + +public class SpaceNode : RoomNode { + private bool _isExpanded = false; + + public SpaceNode(bool includeSelf = true) { + if(includeSelf) + ChildRooms = [this]; + } + + public ObservableCollection ChildSpaces { get; set; } = []; + public ObservableCollection ChildRooms { get; set; } = []; +} \ No newline at end of file diff --git a/ModerationClient/ModerationClient.csproj b/ModerationClient/ModerationClient.csproj index 9876af9..c64d0c3 100644 --- a/ModerationClient/ModerationClient.csproj +++ b/ModerationClient/ModerationClient.csproj @@ -9,7 +9,6 @@ - diff --git a/ModerationClient/Program.cs b/ModerationClient/Program.cs index 9229194..82e10aa 100644 --- a/ModerationClient/Program.cs +++ b/ModerationClient/Program.cs @@ -1,10 +1,12 @@ -using Avalonia; +using Avalonia; using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace ModerationClient; -internal sealed class Program -{ +internal sealed class Program { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. @@ -18,4 +20,13 @@ internal sealed class Program .UsePlatformDetect() .WithInterFont() .LogToTrace(); -} \ No newline at end of file + + // private static FileStream f = new("/dev/input/by-path/platform-pcspkr-event-spkr", FileMode.Open, FileAccess.Write, FileShare.Write, 24); + public static void Beep(short freq, short duration) { + // f.Write([..new byte[16], 0x12, 0x00, 0x02, 0x00, (byte)(freq & 0xFF), (byte)((freq >> 8) & 0xFF), 0x00, 0x00]); + // if (duration > 0) { + // Thread.Sleep(duration); + // f.Write([..new byte[16], 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00]); + // } + } +} diff --git a/ModerationClient/Services/ClientContainer.cs b/ModerationClient/Services/ClientContainer.cs new file mode 100644 index 0000000..fa3abef --- /dev/null +++ b/ModerationClient/Services/ClientContainer.cs @@ -0,0 +1,8 @@ +namespace ModerationClient.Services; + +public class ClientContainer { + public ClientContainer(MatrixAuthenticationService authService, CommandLineConfiguration cfg) + { + + } +} \ No newline at end of file diff --git a/ModerationClient/Services/CommandLineConfiguration.cs b/ModerationClient/Services/CommandLineConfiguration.cs index 63c3691..4f7da2d 100644 --- a/ModerationClient/Services/CommandLineConfiguration.cs +++ b/ModerationClient/Services/CommandLineConfiguration.cs @@ -2,15 +2,14 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Text.Json; using ArcaneLibs; -using Microsoft.Extensions.Logging; +using ArcaneLibs.Extensions; namespace ModerationClient.Services; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public record CommandLineConfiguration { - private readonly string? _loginData; - public static CommandLineConfiguration FromProcessArgs() { // logger.LogInformation("Command line arguments: " + string.Join(", ", Environment.GetCommandLineArgs())); CommandLineConfiguration cfg = FromSerialised(Environment.GetCommandLineArgs()); @@ -27,24 +26,27 @@ public record CommandLineConfiguration { if (!string.IsNullOrWhiteSpace(cfg.LoginData)) { File.WriteAllText(Path.Combine(cfg.ProfileDirectory, "login.json"), cfg.LoginData); } + + return cfg; } - public string[] Serialise() { + var current = FromProcessArgs(); List args = new(); - if (Profile != "default") args.AddRange(["--profile", Profile]); + if (Profile != current.Profile) args.AddRange(["--profile", Profile]); if (IsTemporary) args.Add("--temporary"); if (Math.Abs(Scale - 1f) > float.Epsilon) args.AddRange(["--scale", Scale.ToString()]); - if (ProfileDirectory != Util.ExpandPath("$HOME/.local/share/ModerationClient/default")) args.AddRange(["--profile-dir", ProfileDirectory]); - if (!string.IsNullOrWhiteSpace(_loginData)) args.AddRange(["--login-data", _loginData!]); + if (ProfileDirectory != current.ProfileDirectory) args.AddRange(["--profile-dir", ProfileDirectory]); + if (!string.IsNullOrWhiteSpace(_loginData) && _loginData != current.LoginData) args.AddRange(["--login-data", _loginData!]); + if (TestConfiguration is not null && TestConfiguration != current.TestConfiguration) args.AddRange(["--test-config", TestConfiguration!.ToJson()]); return args.ToArray(); } - public static CommandLineConfiguration FromSerialised(string[] args) { CommandLineConfiguration cfg = new(); for (var i = 0; i < args.Length; i++) { switch (args[i]) { case "--profile": + case "-p": cfg = cfg with { Profile = args[++i] }; break; case "--temporary": @@ -59,12 +61,16 @@ public record CommandLineConfiguration { case "--login-data": cfg = cfg with { LoginData = args[++i] }; break; + case "--test-config": + cfg = cfg with { testConfiguration = args[++i] }; + break; } } return cfg; } + private readonly string? _loginData; public string Profile { get; init; } = "default"; public bool IsTemporary { get; init; } @@ -78,4 +84,15 @@ public record CommandLineConfiguration { _loginData = value; } } + + private string? testConfiguration { + get => TestConfiguration?.ToJson(); + init => TestConfiguration = value is null ? null : JsonSerializer.Deserialize(value); + } + + public TestConfig? TestConfiguration { get; init; } + + public class TestConfig { + public List Mxids { get; set; } = new(); + } } \ No newline at end of file diff --git a/ModerationClient/Services/FileStorageProvider.cs b/ModerationClient/Services/FileStorageProvider.cs index 3658369..5b43ce4 100644 --- a/ModerationClient/Services/FileStorageProvider.cs +++ b/ModerationClient/Services/FileStorageProvider.cs @@ -1,19 +1,28 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using ArcaneLibs.Extensions; -using LibMatrix.Extensions; using LibMatrix.Interfaces.Services; -using Microsoft.Extensions.Logging; -namespace MatrixUtils.Abstractions; +namespace ModerationClient.Services; public class FileStorageProvider : IStorageProvider { - private readonly ILogger _logger; + // private readonly ILogger _logger; + private static readonly JsonSerializerOptions Options = new() { + WriteIndented = true + }; + + private static readonly EnumerationOptions EnumOpts = new EnumerationOptions() { + MatchType = MatchType.Simple, + AttributesToSkip = FileAttributes.None, + IgnoreInaccessible = false, + RecurseSubdirectories = true + }; public string TargetPath { get; } @@ -30,25 +39,58 @@ public class FileStorageProvider : IStorageProvider { } } - public async Task SaveObjectAsync(string key, T value) => await File.WriteAllTextAsync(Path.Join(TargetPath, key), value?.ToJson()); + public async Task SaveObjectAsync(string key, T value) { + EnsureContainingDirectoryExists(GetFullPath(key)); + await using var fileStream = File.Create(GetFullPath(key)); + await JsonSerializer.SerializeAsync(fileStream, value, Options); + } [RequiresUnreferencedCode("This API uses reflection to deserialize JSON")] - public async Task LoadObjectAsync(string key) => JsonSerializer.Deserialize(await File.ReadAllTextAsync(Path.Join(TargetPath, key))); + public async Task LoadObjectAsync(string key) { + await using var fileStream = File.OpenRead(GetFullPath(key)); + return JsonSerializer.Deserialize(fileStream); + } - public Task ObjectExistsAsync(string key) => Task.FromResult(File.Exists(Path.Join(TargetPath, key))); + public Task ObjectExistsAsync(string key) => Task.FromResult(File.Exists(GetFullPath(key))); - public Task> GetAllKeysAsync() => Task.FromResult(Directory.GetFiles(TargetPath).Select(Path.GetFileName).ToList()); + public async Task> GetAllKeysAsync() { + var sw = Stopwatch.StartNew(); + // var result = Directory.EnumerateFiles(TargetPath, "*", SearchOption.AllDirectories) + var result = Directory.EnumerateFiles(TargetPath, "*", EnumOpts) + .Select(s => s.Replace(TargetPath, "").TrimStart('/')); + // Console.WriteLine($"GetAllKeysAsync got {result.Count()} results in {sw.ElapsedMilliseconds}ms"); + // Environment.Exit(0); + return result; + } public Task DeleteObjectAsync(string key) { - File.Delete(Path.Join(TargetPath, key)); + File.Delete(GetFullPath(key)); return Task.CompletedTask; } public async Task SaveStreamAsync(string key, Stream stream) { - Directory.CreateDirectory(Path.GetDirectoryName(Path.Join(TargetPath, key)) ?? throw new InvalidOperationException()); - await using var fileStream = File.Create(Path.Join(TargetPath, key)); + EnsureContainingDirectoryExists(GetFullPath(key)); + await using var fileStream = File.Create(GetFullPath(key)); await stream.CopyToAsync(fileStream); } - public Task LoadStreamAsync(string key) => Task.FromResult(File.Exists(Path.Join(TargetPath, key)) ? File.OpenRead(Path.Join(TargetPath, key)) : null); -} + public Task LoadStreamAsync(string key) => Task.FromResult(File.Exists(GetFullPath(key)) ? File.OpenRead(GetFullPath(key)) : null); + + public Task CopyObjectAsync(string sourceKey, string destKey) { + EnsureContainingDirectoryExists(GetFullPath(destKey)); + File.Copy(GetFullPath(sourceKey), GetFullPath(destKey)); + return Task.CompletedTask; + } + + public Task MoveObjectAsync(string sourceKey, string destKey) { + EnsureContainingDirectoryExists(GetFullPath(destKey)); + File.Move(GetFullPath(sourceKey), GetFullPath(destKey)); + return Task.CompletedTask; + } + + private string GetFullPath(string key) => Path.Join(TargetPath, key); + + private void EnsureContainingDirectoryExists(string path) { + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException()); + } +} \ No newline at end of file diff --git a/ModerationClient/Services/MatrixAuthenticationService.cs b/ModerationClient/Services/MatrixAuthenticationService.cs index 7e9ce70..46ec067 100644 --- a/ModerationClient/Services/MatrixAuthenticationService.cs +++ b/ModerationClient/Services/MatrixAuthenticationService.cs @@ -1,16 +1,12 @@ -using System; using System.IO; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using ArcaneLibs; using ArcaneLibs.Extensions; -using Avalonia.Controls.Diagnostics; using LibMatrix; using LibMatrix.Homeservers; using LibMatrix.Responses; using LibMatrix.Services; -using MatrixUtils.Desktop; using Microsoft.Extensions.Logging; namespace ModerationClient.Services; diff --git a/ModerationClient/Services/ModerationClientConfiguration.cs b/ModerationClient/Services/ModerationClientConfiguration.cs index f770fef..3cc4ffb 100644 --- a/ModerationClient/Services/ModerationClientConfiguration.cs +++ b/ModerationClient/Services/ModerationClientConfiguration.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using ArcaneLibs; -using ArcaneLibs.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/ModerationClient/Services/StatusBarService.cs b/ModerationClient/Services/StatusBarService.cs new file mode 100644 index 0000000..57aff21 --- /dev/null +++ b/ModerationClient/Services/StatusBarService.cs @@ -0,0 +1,19 @@ +using System; +using ArcaneLibs; + +namespace ModerationClient.Services; + +public class StatusBarService : NotifyPropertyChanged { + private string _statusText = "Ready"; + private bool _isBusy; + + public string StatusText { + get => _statusText + " " + DateTime.Now.ToString("u")[..^1]; + set => SetField(ref _statusText, value); + } + + public bool IsBusy { + get => _isBusy; + set => SetField(ref _isBusy, value); + } +} \ No newline at end of file diff --git a/ModerationClient/Services/TestRunner.cs b/ModerationClient/Services/TestRunner.cs new file mode 100644 index 0000000..dbacf99 --- /dev/null +++ b/ModerationClient/Services/TestRunner.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace ModerationClient.Services; + +public class TestRunner(CommandLineConfiguration.TestConfig testConfig, MatrixAuthenticationService mas) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + Console.WriteLine("TestRunner: Starting test runner"); + mas.PropertyChanged += (_, args) => { + if (args.PropertyName == nameof(MatrixAuthenticationService.IsLoggedIn) && mas.IsLoggedIn) { + Console.WriteLine("TestRunner: Logged in, starting test"); + _ = Run(); + } + }; + } + + public async Task StopAsync(CancellationToken cancellationToken) { + Console.WriteLine("TestRunner: Stopping test runner"); + } + + private async Task Run() { + var hs = mas.Homeserver!; + Console.WriteLine("TestRunner: Running test on homeserver " + hs); + foreach (var mxid in testConfig.Mxids) { + var room = await hs.CreateRoom(new() { + Name = mxid, + Invite = testConfig.Mxids + }); + + await room.SendMessageEventAsync(new("test")); + + } + } +} \ No newline at end of file diff --git a/ModerationClient/ViewModels/ClientViewModel.cs b/ModerationClient/ViewModels/ClientViewModel.cs index 312b46a..fb3681e 100644 --- a/ModerationClient/ViewModels/ClientViewModel.cs +++ b/ModerationClient/ViewModels/ClientViewModel.cs @@ -1,17 +1,20 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; +using ArcaneLibs; using ArcaneLibs.Collections; +using LibMatrix; using LibMatrix.EventTypes.Spec.State; using LibMatrix.Helpers; using LibMatrix.Responses; -using MatrixUtils.Abstractions; using Microsoft.Extensions.Logging; +using ModerationClient.Models.SpaceTreeNodes; using ModerationClient.Services; namespace ModerationClient.ViewModels; @@ -22,7 +25,13 @@ public partial class ClientViewModel : ViewModelBase { _authService = authService; _cfg = cfg; DisplayedSpaces.Add(_allRoomsNode = new AllRoomsSpaceNode(this)); - _ = Task.Run(Run); + DisplayedSpaces.Add(DirectMessages = new SpaceNode(false) { Name = "Direct messages" }); + _ = Task.Run(Run).ContinueWith(x => { + if (x.IsFaulted) { + Status = "Critical error running client view model: " + x.Exception?.Message; + _logger.LogError(x.Exception, "Error running client view model."); + } + }); } private readonly ILogger _logger; @@ -33,6 +42,9 @@ public partial class ClientViewModel : ViewModelBase { private string _status = "Loading..."; public ObservableCollection DisplayedSpaces { get; } = []; public ObservableDictionary AllRooms { get; } = new(); + public SpaceNode DirectMessages { get; } + + public bool Paused { get; set; } = false; public SpaceNode CurrentSpace { get => _currentSpace ?? _allRoomsNode; @@ -45,25 +57,50 @@ public partial class ClientViewModel : ViewModelBase { } public async Task Run() { - Status = "Interrupted."; - return; + Console.WriteLine("Running client view model loop..."); + ArgumentNullException.ThrowIfNull(_authService.Homeserver, nameof(_authService.Homeserver)); + // var sh = new SyncStateResolver(_authService.Homeserver, _logger, storageProvider: new FileStorageProvider(Path.Combine(_cfg.ProfileDirectory, "syncCache"))); + var store = new FileStorageProvider(Path.Combine(_cfg.ProfileDirectory, "syncCache")); + Console.WriteLine($"Sync store at {store.TargetPath}"); + + var sh = new SyncHelper(_authService.Homeserver, _logger, storageProvider: store) { + // MinimumDelay = TimeSpan.FromSeconds(1) + }; + Console.WriteLine("Sync helper created."); + + //optimise - we create a new scope here to make ssr go out of scope + // if((await sh.GetUnoptimisedStoreCount()) > 1000) + { + Console.WriteLine("RUN - Optimising sync store..."); + Status = "Optimising sync store, please wait..."; + var ssr = new SyncStateResolver(_authService.Homeserver, _logger, storageProvider: store); + Console.WriteLine("Created sync state resolver..."); + Status = "Optimising sync store, please wait... Creating new snapshot..."; + await ssr.OptimiseStore(); + Status = "Optimising sync store, please wait... Deleting old intermediate snapshots..."; + await ssr.RemoveOldSnapshots(); + } + + var unoptimised = await sh.GetUnoptimisedStoreCount(); // this is slow, so we cache Status = "Doing initial sync..."; - var sh = new SyncStateResolver(_authService.Homeserver, _logger, storageProvider: new FileStorageProvider(Path.Combine(_cfg.ProfileDirectory, "syncCache"))); - // var res = await sh.SyncAsync(); - //await sh.OptimiseStore(); - while (true) { - // Status = "Syncing..."; - var res = await sh.ContinueAsync(); - Status = $"Processing sync... {res.next.NextBatch}"; - await ApplySpaceChanges(res.next); - //OnPropertyChanged(nameof(CurrentSpace)); - //OnPropertyChanged(nameof(CurrentSpace.ChildRooms)); - // Console.WriteLine($"mow A={AllRooms.Count}|D={DisplayedSpaces.Count}"); - // for (int i = 0; i < GC.MaxGeneration; i++) { - // GC.Collect(i, GCCollectionMode.Forced, blocking: true); - // GC.WaitForPendingFinalizers(); - // } - Status = "Syncing..."; + await foreach (var res in sh.EnumerateSyncAsync()) { + Program.Beep((short)250, 0); + Status = $"Processing sync... {res.NextBatch}"; + await ApplySyncChanges(res); + + Program.Beep(0, 0); + if (Paused) { + Status = "Sync loop interrupted... Press pause/break to resume."; + while (Paused) await Task.Delay(1000); + } + else Status = $"Syncing... {unoptimised++} unoptimised sync responses..."; + } + } + + private async Task ApplySyncChanges(SyncResponse newSync) { + await ApplySpaceChanges(newSync); + if (newSync.AccountData?.Events?.FirstOrDefault(x => x.Type == "m.direct") is { } evt) { + await ApplyDirectMessagesChanges(evt); } } @@ -71,20 +108,25 @@ public partial class ClientViewModel : ViewModelBase { List tasks = []; foreach (var room in newSync.Rooms?.Join ?? []) { if (!AllRooms.ContainsKey(room.Key)) { - AllRooms.Add(room.Key, new RoomNode { Name = "Loading..." }); + // AllRooms.Add(room.Key, new RoomNode { Name = "Loading..." }); + AllRooms.Add(room.Key, new RoomNode { Name = "", RoomID = room.Key }); } if (room.Value.State?.Events is not null) { var nameEvent = room.Value.State!.Events!.FirstOrDefault(x => x.Type == "m.room.name" && x.StateKey == ""); - AllRooms[room.Key].Name = (nameEvent?.TypedContent as RoomNameEventContent)?.Name ?? ""; - if (string.IsNullOrWhiteSpace(AllRooms[room.Key].Name)) { - AllRooms[room.Key].Name = "Loading..."; - tasks.Add(_authService.Homeserver!.GetRoom(room.Key).GetNameOrFallbackAsync().ContinueWith(r => AllRooms[room.Key].Name = r.Result)); - } + if (nameEvent is not null) + AllRooms[room.Key].Name = (nameEvent?.TypedContent as RoomNameEventContent)?.Name ?? ""; + } + + if (string.IsNullOrWhiteSpace(AllRooms[room.Key].Name)) { + AllRooms[room.Key].Name = "Loading..."; + tasks.Add(_authService.Homeserver!.GetRoom(room.Key).GetNameOrFallbackAsync().ContinueWith(r => AllRooms[room.Key].Name = r.Result)); + // Status = $"Getting room name for {room.Key}..."; + // AllRooms[room.Key].Name = await _authService.Homeserver!.GetRoom(room.Key).GetNameOrFallbackAsync(); } } - - await Task.WhenAll(tasks); + + await AwaitTasks(tasks, "Waiting for {0}/{1} tasks while applying room changes..."); return; @@ -111,20 +153,59 @@ public partial class ClientViewModel : ViewModelBase { handledRoomIds.Add(roomId); } } -} -public class SpaceNode : RoomNode { - public ObservableCollection ChildSpaces { get; set; } = []; - public ObservableCollection ChildRooms { get; set; } = []; -} + private async Task ApplyDirectMessagesChanges(StateEventResponse evt) { + _logger.LogCritical("Direct messages updated!"); + var dms = evt.RawContent.Deserialize>(); + List tasks = []; + foreach (var (userId, roomIds) in dms) { + if (roomIds is null || roomIds.Length == 0) continue; + var space = DirectMessages.ChildSpaces.FirstOrDefault(x => x.RoomID == userId); + if (space is null) { + space = new SpaceNode { Name = userId, RoomID = userId }; + tasks.Add(_authService.Homeserver!.GetProfileAsync(userId) + .ContinueWith(r => space.Name = string.IsNullOrWhiteSpace(r.Result?.DisplayName) ? userId : r.Result.DisplayName)); + DirectMessages.ChildSpaces.Add(space); + } + + foreach (var roomId in roomIds) { + var room = space.ChildRooms.FirstOrDefault(x => x.RoomID == roomId); + if (room is null) { + room = AllRooms.TryGetValue(roomId, out var existing) ? existing : new RoomNode { Name = "Unknown: " + roomId, RoomID = roomId }; + space.ChildRooms.Add(room); + } + } -public class RoomNode { - public string Name { get; set; } + foreach (var spaceChildRoom in space.ChildRooms.ToList()) { + if (!roomIds.Contains(spaceChildRoom.RoomID)) { + space.ChildRooms.Remove(spaceChildRoom); + } + } + } + + await AwaitTasks(tasks, "Waiting for {0}/{1} tasks while applying DM changes..."); + } + + private async Task AwaitTasks(List tasks, string message) { + if (tasks.Count > 0) { + int total = tasks.Count; + while (tasks.Any(x => !x.IsCompleted)) { + int incomplete = tasks.Count(x => !x.IsCompleted); + Program.Beep((short)MathUtil.Map(incomplete, 0, total, 20, 7500), 5); + // Program.Beep(0, 0); + Status = string.Format(message, incomplete, total); + await Task.WhenAny(tasks); + tasks.RemoveAll(x => x.IsCompleted); + } + + Program.Beep(0, 0); + } + } } // implementation details public class AllRoomsSpaceNode : SpaceNode { - public AllRoomsSpaceNode(ClientViewModel vm) { + public AllRoomsSpaceNode(ClientViewModel vm) : base(false) { Name = "All rooms"; vm.AllRooms.CollectionChanged += (_, args) => { switch (args.Action) { diff --git a/ModerationClient/ViewModels/MainWindowViewModel.cs b/ModerationClient/ViewModels/MainWindowViewModel.cs index be64de4..5cd5c45 100644 --- a/ModerationClient/ViewModels/MainWindowViewModel.cs +++ b/ModerationClient/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,6 @@ using System; using Avalonia; using ModerationClient.Services; -using ModerationClient.Views; namespace ModerationClient.ViewModels; @@ -9,10 +8,10 @@ public partial class MainWindowViewModel(MatrixAuthenticationService authService // public MainWindow? MainWindow { get; set; } private float _scale = 1.0f; - private ViewModelBase _currentViewModel = new LoginViewModel(authService); + private ViewModelBase? _currentViewModel = null; private Size _physicalSize = new Size(300, 220); - public ViewModelBase CurrentViewModel { + public ViewModelBase? CurrentViewModel { get => _currentViewModel; set => SetProperty(ref _currentViewModel, value); } diff --git a/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs b/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs index 7a2ad63..90020d6 100644 --- a/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs +++ b/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs @@ -1,20 +1,10 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.Diagnostics; -using System.IO; -using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using ArcaneLibs.Collections; using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; -using LibMatrix.Responses; -using MatrixUtils.Abstractions; using Microsoft.Extensions.Logging; using ModerationClient.Services; @@ -51,6 +41,7 @@ public partial class UserManagementViewModel : ViewModelBase { } await foreach (var user in synapse.Admin.SearchUsersAsync(chunkLimit: 100)) { + Program.Beep(250, 1); Console.WriteLine("USERMANAGER GOT USER: " + user.ToJson(indent:false, ignoreNull: true)); Users.Add(JsonSerializer.Deserialize(user.ToJson())!); } @@ -58,6 +49,6 @@ public partial class UserManagementViewModel : ViewModelBase { } } -public class User : AdminUserListResult.AdminUserListResultUser { +public class User : SynapseAdminUserListResult.SynapseAdminUserListResultUser { } \ No newline at end of file diff --git a/ModerationClient/Views/MainWindow/ClientView.axaml b/ModerationClient/Views/MainWindow/ClientView.axaml index ba030e4..e0cd4e0 100644 --- a/ModerationClient/Views/MainWindow/ClientView.axaml +++ b/ModerationClient/Views/MainWindow/ClientView.axaml @@ -4,36 +4,42 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:views="clr-namespace:ModerationClient.Views" xmlns:viewModels="clr-namespace:ModerationClient.ViewModels" + xmlns:spaceTreeNodes="clr-namespace:ModerationClient.Models.SpaceTreeNodes" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ModerationClient.Views.ClientView" x:DataType="viewModels:ClientViewModel"> - + - + - + - + + + + + + - + - + - +