From a06e981150359c9765b229c297671f75fc2e4704 Mon Sep 17 00:00:00 2001 From: Rory& Date: Fri, 20 Feb 2026 03:18:44 +0100 Subject: [PATCH] Admin API work --- .../Controllers/ChannelController.cs | 134 ++++++++++++++++++ .../Controllers/DiscoveryController.cs | 27 ++++ .../Controllers/GuildController.cs | 9 +- .../TestControllers/EmptyDmsController.cs | 54 +++++++ .../Controllers/UserController.cs | 8 +- .../Spacebar.AdminApi/Spacebar.AdminApi.http | 12 ++ .../appsettings.Development.json | 30 ++-- 7 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 extra/admin-api/Spacebar.AdminApi/Controllers/ChannelController.cs create mode 100644 extra/admin-api/Spacebar.AdminApi/Controllers/DiscoveryController.cs create mode 100644 extra/admin-api/Spacebar.AdminApi/Controllers/TestControllers/EmptyDmsController.cs create mode 100644 extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.http diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/ChannelController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/ChannelController.cs new file mode 100644 index 000000000..45a9e4370 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/ChannelController.cs @@ -0,0 +1,134 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.Interop.Replication.Abstractions; +using Spacebar.AdminApi.Extensions; +using Spacebar.Models.AdminApi; +using Spacebar.Interop.Authentication.AspNetCore; +using Spacebar.Models.Db.Contexts; +using Spacebar.Models.Db.Models; + +namespace Spacebar.AdminApi.Controllers; + +[ApiController] +[Route("/channels")] +public class ChannelController( + ILogger logger, + SpacebarDbContext db, + IServiceProvider sp, + SpacebarAspNetAuthenticationService auth, + ISpacebarReplication replication +) : ControllerBase { + + [HttpDelete("{id}")] + public async Task DeleteById(string id) { + (await auth.GetCurrentUserAsync(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + replication.SendAsync(new() { + Origin = "AdminApi/DeleteChannelById", + ChannelId = id, + Event = "CHANNEL_DELETE", + Payload = await db.Channels.SingleAsync (x=>x.Id == id) + }); + + await db.Channels.Where(x => x.Id == id).ExecuteDeleteAsync(); + } + + private async IAsyncEnumerable DeleteMessagesForChannel( + // context + string? guildId, string channelId, string authorId, + // options + int messageDeleteChunkSize = 100 + ) { + { + await using var ctx = sp.CreateAsyncScope(); + await using var _db = ctx.ServiceProvider.GetRequiredService(); + var messagesInChannel = _db.Messages.AsNoTracking().Count(m => m.AuthorId == authorId && m.ChannelId == channelId && m.GuildId == guildId); + var remaining = messagesInChannel; + while (true) { + var messageIds = _db.Database.SqlQuery($""" + DELETE FROM messages + WHERE id IN ( + SELECT id FROM messages + WHERE author_id = {authorId} + AND channel_id = {channelId} + AND guild_id = {guildId} + LIMIT {messageDeleteChunkSize} + ) RETURNING id; + """).ToList(); + if (messageIds.Count == 0) { + break; + } + + await replication.SendAsync(new() { + ChannelId = channelId, + Event = "MESSAGE_BULK_DELETE", + Payload = new { + ids = messageIds, + channel_id = channelId, + guild_id = guildId, + }, + Origin = "Admin API (GuildController.DeleteUser)", + }); + + yield return new("BULK_DELETED", new { + channel_id = channelId, + total = messagesInChannel, + deleted = messageIds.Count, + remaining = remaining -= messageIds.Count, + }); + await Task.Yield(); + } + } + } + + private async IAsyncEnumerable AggregateAsyncEnumerablesWithoutOrder(params IEnumerable> enumerables) { + var enumerators = enumerables.Select(e => e.GetAsyncEnumerator()).ToList(); + var tasks = enumerators.Select(e => e.MoveNextAsync().AsTask()).ToList(); + + try { + while (tasks.Count > 0) { + var completedTask = await Task.WhenAny(tasks); + var completedTaskIndex = tasks.IndexOf(completedTask); + + if (completedTask.IsCanceled) { + try { + await enumerators[completedTaskIndex].DisposeAsync(); + } + catch { + // ignored + } + + enumerators.RemoveAt(completedTaskIndex); + tasks.RemoveAt(completedTaskIndex); + continue; + } + + if (await completedTask) { + var enumerator = enumerators[completedTaskIndex]; + yield return enumerator.Current; + tasks[completedTaskIndex] = enumerator.MoveNextAsync().AsTask(); + } + else { + try { + await enumerators[completedTaskIndex].DisposeAsync(); + } + catch { + // ignored + } + + enumerators.RemoveAt(completedTaskIndex); + tasks.RemoveAt(completedTaskIndex); + } + } + } + finally { + foreach (var enumerator in enumerators) { + try { + await enumerator.DisposeAsync(); + } + catch { + // ignored + } + } + } + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/DiscoveryController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/DiscoveryController.cs new file mode 100644 index 000000000..39d72b45c --- /dev/null +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/DiscoveryController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.Interop.Replication.Abstractions; +using Spacebar.AdminApi.Extensions; +using Spacebar.Models.AdminApi; +using Spacebar.Interop.Authentication.AspNetCore; +using Spacebar.Models.Db.Contexts; +using Spacebar.Models.Db.Models; + +namespace Spacebar.AdminApi.Controllers; + +[ApiController] +[Route("/discovery")] +public class DiscoveryController( + ILogger logger, + SpacebarDbContext db, + IServiceProvider sp, + SpacebarAspNetAuthenticationService auth, + ISpacebarReplication replication +) : ControllerBase { + [HttpGet] + public async Task GetDiscoverableGuilds() { + (await auth.GetCurrentUserAsync(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + // var discoverableGuilds = db.Guilds + // .Where(x=>x.) + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs index f140cccd9..62df5a38c 100644 --- a/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs @@ -98,10 +98,15 @@ public class GuildController( member = new Member { Id = userId, GuildId = id, - JoinedAt = DateTime.UtcNow, + JoinedAt = DateTime.Now, PremiumSince = 0, Roles = [await db.Roles.SingleAsync(r => r.Id == id)], - Pending = false + Pending = false, + Settings = "{}", + Bio = "", + Mute = false, + Deaf = false, + }; await db.Members.AddAsync(member); guild.MemberCount++; diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/TestControllers/EmptyDmsController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/TestControllers/EmptyDmsController.cs new file mode 100644 index 000000000..862585b33 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/TestControllers/EmptyDmsController.cs @@ -0,0 +1,54 @@ +using System.Diagnostics; +using ArcaneLibs; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.AdminApi.Extensions; +using Spacebar.Interop.Authentication; +using Spacebar.Interop.Authentication.AspNetCore; +using Spacebar.Interop.Replication.Abstractions; +using Spacebar.Models.AdminApi; +using Spacebar.Models.Db.Contexts; + +namespace Spacebar.AdminApi.Controllers.TestControllers; + +[ApiController] +public class EmptyDmsController( + ILogger logger, + SpacebarAuthenticationConfiguration config, + SpacebarDbContext db, + IServiceProvider sp, + SpacebarAspNetAuthenticationService auth, + ISpacebarReplication replication +) : ControllerBase { + [HttpGet("emptydms")] + public async IAsyncEnumerable GetEmptyDms() { + (await auth.GetCurrentUserAsync(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + + // TODO channel type enum + var channels = db.Channels + .Include(x=>x.Recipients) + .Include(x=>x.Messages) + .Where(x => x.Type == 1) + .Where(x => !x.Messages.Any() && x.Recipients.Count == 1) + ; + + await using var db2Scope = sp.CreateAsyncScope(); + await using var db3Scope = sp.CreateAsyncScope(); + var db2 = db2Scope.ServiceProvider.GetRequiredService(); + var db3 = db3Scope.ServiceProvider.GetRequiredService(); + int count = 0; + await foreach (var channel in channels.AsAsyncEnumerable()) { + count++; + yield return new { + id = channel.Id, + msgs = await db2.Messages.Where(x => x.ChannelId == channel.Id).CountAsync(), + recips = await db3.Recipients.Where(x => x.ChannelId == channel.Id).CountAsync() + }; + } + + logger.LogInformation("Got {count} empty DM channels", count); + + yield break; + + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs index 7b76aabde..ebc43b6f6 100644 --- a/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; -using ArcaneLibs; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Spacebar.AdminApi.Extensions; @@ -8,7 +6,6 @@ using Spacebar.Interop.Authentication.AspNetCore; using Spacebar.Interop.Replication.Abstractions; using Spacebar.Models.AdminApi; using Spacebar.Models.Db.Contexts; -using Spacebar.Models.Db.Models; namespace Spacebar.AdminApi.Controllers; @@ -184,6 +181,11 @@ public class UserController( } } + // [HttpGet("{id}/Dms")] + // public async IEnumerable GetDmsAsync(string userId) { + // yield break; // TODO + // } + private async IAsyncEnumerable DeleteMessagesForChannel( // context string? guildId, string channelId, string authorId, diff --git a/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.http b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.http new file mode 100644 index 000000000..06d9d1747 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.http @@ -0,0 +1,12 @@ +@Spacebar.AdminApi_HostAddress = http://localhost:5112 + +POST {{Spacebar.AdminApi_HostAddress}}/_spacebar/admin/guilds/1473141782615941382/force_join +Content-Type: application/json +Accept: application/json + +{ + "MakeOwner": true, + "UserId": "1006598230156341276" +} + +### diff --git a/extra/admin-api/Spacebar.AdminApi/appsettings.Development.json b/extra/admin-api/Spacebar.AdminApi/appsettings.Development.json index 032457733..0ee920fff 100644 --- a/extra/admin-api/Spacebar.AdminApi/appsettings.Development.json +++ b/extra/admin-api/Spacebar.AdminApi/appsettings.Development.json @@ -2,16 +2,17 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Trace", //Warning - "Microsoft.AspNetCore.Mvc": "Warning", //Warning - "Microsoft.AspNetCore.HostFiltering": "Warning", //Warning - "Microsoft.AspNetCore.Cors": "Warning", //Warning - // "Microsoft.EntityFrameworkCore": "Warning" - "Microsoft.EntityFrameworkCore.Database.Command": "Debug" + "Microsoft.AspNetCore": "Trace", + "Microsoft.AspNetCore.Mvc": "Warning", + "Microsoft.AspNetCore.HostFiltering": "Warning", + "Microsoft.AspNetCore.Cors": "Warning", + // "Microsoft.EntityFrameworkCore": "Warning" + "Microsoft.EntityFrameworkCore.Database.Command": "Debug", + "Microsoft.AspNetCore.Server.Kestrel.Connections": "Information" } }, "ConnectionStrings": { - "Spacebar": "Host=127.0.0.1; Username=postgres; Database=spacebar; Port=5432; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;", + "Spacebar": "Host=127.0.0.1; Username=postgres; Database=spacebar; Port=5432; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;" }, "RabbitMQ": { "Host": "127.0.0.1", @@ -19,9 +20,16 @@ "Username": "guest", "Password": "guest" }, - "SpacebarAdminApi": { - "Enforce2FA": true, - "OverrideUid": null, - "DisableAuthentication": false + "Spacebar": { + "Authentication": { + "Enforce2FA": false, + "OverrideUid": "1006598230156341276", + "DisableAuthentication": true, + "PublicKeyPath": "../../../jwt.key.pub", + "PrivateKeyPath": "../../../jwt.key" + }, + "UnixSocketReplication": { + "SocketDir": "../../.." + } } }