summary refs log tree commit diff
path: root/extra/admin-api/Spacebar.AdminAPI/Controllers
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-10-14 09:07:55 +0200
committerRory& <root@rory.gay>2025-10-14 09:07:55 +0200
commit58d92080d544e255ee3fd0c0e722a7c6f1c474a7 (patch)
treef3ba88ea3ad8f03b8a0a01fce11bea0965d4b4c6 /extra/admin-api/Spacebar.AdminAPI/Controllers
parentSchema fixes, api bug fix (diff)
downloadserver-ts-58d92080d544e255ee3fd0c0e722a7c6f1c474a7.tar.xz
Admin api changes
Diffstat (limited to 'extra/admin-api/Spacebar.AdminAPI/Controllers')
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs316
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs2
2 files changed, 317 insertions, 1 deletions
diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs
new file mode 100644

index 000000000..a15dd2598 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs
@@ -0,0 +1,316 @@ +using System.Diagnostics; +using ArcaneLibs; +using ArcaneLibs.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RabbitMQ.Client; +using Spacebar.AdminApi.Models; +using Spacebar.AdminAPI.Services; +using Spacebar.Db.Contexts; +using Spacebar.Db.Models; +using Spacebar.RabbitMqUtilities; + +namespace Spacebar.AdminAPI.Controllers; + +[ApiController] +[Route("/Guilds")] +public class GuildController(ILogger<GuildController> logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp, AuthenticationService authService) : ControllerBase { + private readonly ILogger<GuildController> _logger = logger; + + [HttpGet] + public IAsyncEnumerable<GuildModel> Get() { + return db.Guilds.Select(x => new GuildModel { + Id = x.Id, + AfkChannelId = x.AfkChannelId, + AfkTimeout = x.AfkTimeout, + Banner = x.Banner, + DefaultMessageNotifications = x.DefaultMessageNotifications, + Description = x.Description, + DiscoverySplash = x.DiscoverySplash, + ExplicitContentFilter = x.ExplicitContentFilter, + Features = x.Features, + PrimaryCategoryId = x.PrimaryCategoryId, + Icon = x.Icon, + Large = x.Large, + MaxMembers = x.MaxMembers, + MaxPresences = x.MaxPresences, + MaxVideoChannelUsers = x.MaxVideoChannelUsers, + MemberCount = x.MemberCount, + PresenceCount = x.PresenceCount, + TemplateId = x.TemplateId, + MfaLevel = x.MfaLevel, + Name = x.Name, + OwnerId = x.OwnerId, + PreferredLocale = x.PreferredLocale, + PremiumSubscriptionCount = x.PremiumSubscriptionCount, + PremiumTier = x.PremiumTier, + PublicUpdatesChannelId = x.PublicUpdatesChannelId, + RulesChannelId = x.RulesChannelId, + Region = x.Region, + Splash = x.Splash, + SystemChannelId = x.SystemChannelId, + SystemChannelFlags = x.SystemChannelFlags, + Unavailable = x.Unavailable, + VerificationLevel = x.VerificationLevel, + WelcomeScreen = x.WelcomeScreen, + WidgetChannelId = x.WidgetChannelId, + WidgetEnabled = x.WidgetEnabled, + NsfwLevel = x.NsfwLevel, + Nsfw = x.Nsfw, + Parent = x.Parent, + PremiumProgressBarEnabled = x.PremiumProgressBarEnabled, + ChannelOrdering = x.ChannelOrdering, + ChannelCount = x.Channels.Count(), + RoleCount = x.Roles.Count(), + EmojiCount = x.Emojis.Count(), + StickerCount = x.Stickers.Count(), + InviteCount = x.Invites.Count(), + MessageCount = x.Messages.Count(), + BanCount = x.Bans.Count(), + VoiceStateCount = x.VoiceStates.Count(), + }).AsAsyncEnumerable(); + } + + [HttpPost("{id}/force_join")] + public async Task<IActionResult> ForceJoinGuild([FromBody] ForceJoinRequest request, string id) { + var guild = await db.Guilds.FindAsync(id); + if (guild == null) { + return NotFound(new { entity = "Guild", id, message = "Guild not found" }); + } + + var userId = request.UserId ?? config.OverrideUid ?? (await authService.GetCurrentUser(Request)).Id; + var user = await db.Users.FindAsync(userId); + if (user == null) { + return NotFound(new { entity = "User", id = userId, message = "User not found" }); + } + + var member = await db.Members.SingleOrDefaultAsync(m => m.GuildId == id && m.Id == userId); + if (member is null) { + member = new Member { + Id = userId, + GuildId = id, + JoinedAt = DateTime.UtcNow, + PremiumSince = 0, + Roles = [await db.Roles.SingleAsync(r => r.Id == id)], + Pending = false + }; + await db.Members.AddAsync(member); + guild.MemberCount++; + db.Guilds.Update(guild); + await db.SaveChangesAsync(); + } + + if (request.MakeOwner) { + guild.OwnerId = userId; + db.Guilds.Update(guild); + await db.SaveChangesAsync(); + } else if (request.MakeAdmin) { + var roles = await db.Roles.Where(r => r.GuildId == id).OrderBy(x=>x.Position).ToListAsync(); + var adminRole = roles.FirstOrDefault(r => r.Permissions == "8" || r.Permissions == "9"); // Administrator + if (adminRole == null) { + adminRole = new Role { + Id = Guid.NewGuid().ToString(), + GuildId = id, + Name = "Instance administrator", + Color = 0, + Hoist = false, + Position = roles.Max(x=>x.Position) + 1, + Permissions = "8", // Administrator + Managed = false, + Mentionable = false + }; + await db.Roles.AddAsync(adminRole); + await db.SaveChangesAsync(); + } + + if (!member.Roles.Any(r => r.Id == adminRole.Id)) { + member.Roles.Add(adminRole); + db.Members.Update(member); + await db.SaveChangesAsync(); + } + } + + // TODO: gateway events + + return Ok(new { entity = "Guild", id, message = "Guild join forced" }); + } + + [HttpGet("{id}/delete")] + public async IAsyncEnumerable<AsyncActionResult> DeleteUser(string id, [FromQuery] int messageDeleteChunkSize = 100) { + var user = await db.Users.FindAsync(id); + if (user == null) { + Console.WriteLine($"User {id} not found"); + yield return new AsyncActionResult("ERROR", new { entity = "User", id, message = "User not found" }); + yield break; + } + + user.Data = "{}"; + user.Deleted = true; + user.Disabled = true; + user.Rights = 0; + db.Users.Update(user); + await db.SaveChangesAsync(); + + var factory = new ConnectionFactory { + Uri = new Uri("amqp://guest:guest@127.0.0.1/") + }; + await using var mqConnection = await factory.CreateConnectionAsync(); + await using var mqChannel = await mqConnection.CreateChannelAsync(); + + var messages = db.Messages + .AsNoTracking() + .Where(m => m.AuthorId == id); + var channels = messages + .Select(m => new { m.ChannelId, m.GuildId }) + .Distinct() + .ToList(); + yield return new("STATS", + new { + total_messages = messages.Count(), total_channels = channels.Count, + messages_per_channel = channels.ToDictionary(c => c.ChannelId, c => messages.Count(m => m.ChannelId == c.ChannelId)) + }); + var results = channels + .Select(ctx => DeleteMessagesForChannel(ctx.GuildId, ctx.ChannelId!, id, mqChannel, messageDeleteChunkSize)) + .ToList(); + var a = AggregateAsyncEnumerablesWithoutOrder(results); + await foreach (var result in a) { + yield return result; + } + + await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages"); + await db.Database.ExecuteSqlRawAsync("REINDEX TABLE messages"); + } + + private async IAsyncEnumerable<AsyncActionResult> DeleteMessagesForChannel( + // context + string? guildId, string channelId, string authorId, + // connections + IChannel mqChannel, + // options + int messageDeleteChunkSize = 100 + ) { + { + await using var ctx = sp.CreateAsyncScope(); + await using var _db = ctx.ServiceProvider.GetRequiredService<SpacebarDbContext>(); + await mqChannel.ExchangeDeclareAsync(exchange: channelId!, type: ExchangeType.Fanout, durable: false); + 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<string>($""" + 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; + } + + var props = new BasicProperties() { Type = "MESSAGE_BULK_DELETE" }; + var publishSuccess = false; + do { + try { + await mqChannel.BasicPublishAsync(exchange: channelId!, routingKey: "", mandatory: true, basicProperties: props, body: new { + ids = messageIds, + channel_id = channelId, + guild_id = guildId, + }.ToJson().AsBytes().ToArray()); + publishSuccess = true; + } + catch (Exception e) { + Console.WriteLine($"[RabbitMQ] Error publishing bulk delete: {e.Message}"); + await Task.Delay(10); + } + } while (!publishSuccess); + + yield return new("BULK_DELETED", new { + channel_id = channelId, + total = messagesInChannel, + deleted = messageIds.Count, + remaining = remaining -= messageIds.Count, + }); + await Task.Yield(); + } + } + } + + private async IAsyncEnumerable<T> AggregateAsyncEnumerablesWithoutOrder<T>(params IEnumerable<IAsyncEnumerable<T>> 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 + } + } + } + } + + // { + // "op": 0, + // "t": "GUILD_ROLE_UPDATE", + // "d": { + // "guild_id": "1006649183970562092", + // "role": { + // "id": "1006706520514028812", + // "guild_id": "1006649183970562092", + // "color": 16711680, + // "hoist": true, + // "managed": false, + // "mentionable": true, + // "name": "Adminstrator", + // "permissions": "9", + // "position": 5, + // "unicode_emoji": "💖", + // "flags": 0 + // } + // }, + // "s": 38 + // } + +} \ 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 a8501baf7..811143fe8 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs
@@ -13,7 +13,7 @@ using Spacebar.RabbitMqUtilities; namespace Spacebar.AdminAPI.Controllers; [ApiController] -[Route("/Users")] +[Route("/users")] public class UserController(ILogger<UserController> logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase { private readonly ILogger<UserController> _logger = logger;