summary refs log tree commit diff
path: root/extra/admin-api/Spacebar.AdminAPI
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-12-14 23:18:35 +0100
committerRory& <root@rory.gay>2025-12-14 23:18:35 +0100
commit4534d4dbf5f5071fa71c5dec4444bf4b2bbd040c (patch)
tree614304bd951f09a73155a9bd9ee192cf946e854a /extra/admin-api/Spacebar.AdminAPI
parentNo longer generate security_jwtSecret, switch webauthn to modern tokens, dont (diff)
downloadserver-ts-4534d4dbf5f5071fa71c5dec4444bf4b2bbd040c.tar.xz
admin api packaging
Diffstat (limited to 'extra/admin-api/Spacebar.AdminAPI')
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs323
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs30
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Controllers/PingController.cs32
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs519
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs10
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs79
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Program.cs77
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Properties/launchSettings.json23
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs42
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs11
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/Spacebar.AdminAPI.csproj23
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/appsettings.Development.json27
-rw-r--r--extra/admin-api/Spacebar.AdminAPI/appsettings.json9
13 files changed, 0 insertions, 1205 deletions
diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs
deleted file mode 100644

index 03b38e64d..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs +++ /dev/null
@@ -1,323 +0,0 @@ -using ArcaneLibs.Extensions; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using RabbitMQ.Client; -using Spacebar.AdminAPI.Extensions; -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 auth) : ControllerBase { - private readonly ILogger<GuildController> _logger = logger; - - [HttpGet] - public async IAsyncEnumerable<GuildModel> Get() { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - var results = 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(); - await foreach (var result in results) { - yield return result; - } - } - - [HttpPost("{id}/force_join")] - public async Task<IActionResult> ForceJoinGuild([FromBody] ForceJoinRequest request, string id) { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - 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 auth.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) { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - 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/Media/UserMediaController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs deleted file mode 100644
index a4d915e5a..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs +++ /dev/null
@@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Spacebar.AdminAPI.Extensions; -using Spacebar.AdminApi.Models; -using Spacebar.AdminAPI.Services; -using Spacebar.Db.Contexts; -using Spacebar.Db.Models; -using Spacebar.RabbitMqUtilities; - -namespace Spacebar.AdminAPI.Controllers.Media; - -[ApiController] -[Route("/media/user")] -public class UserMediaController(ILogger<UserMediaController> logger, SpacebarDbContext db, RabbitMQService mq, AuthenticationService auth, IServiceProvider sp) : ControllerBase { - [HttpGet("{userId}/attachments")] - public async IAsyncEnumerable<Attachment> GetAttachmentsByUser(string userId) { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - var db2 = sp.CreateScope().ServiceProvider.GetService<SpacebarDbContext>(); - var attachments = db.Attachments - // .IgnoreAutoIncludes() - .Where(x => x.Message!.AuthorId == userId) - .AsAsyncEnumerable(); - await foreach (var attachment in attachments) { - attachment.Message = await db2.Messages.FindAsync(attachment.MessageId); - // attachment.Message.Author = await db2.Users.FindAsync(attachment.Message.AuthorId); - yield return attachment; - } - } -} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/PingController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/PingController.cs deleted file mode 100644
index a2aceb504..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/PingController.cs +++ /dev/null
@@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Spacebar.AdminAPI.Services; - -namespace Spacebar.AdminAPI.Controllers; - -[ApiController] -[Route("/")] -public class PingController(ILogger<PingController> logger, IServiceProvider sp, AuthenticationService auth) : ControllerBase { - private readonly ILogger<PingController> _logger = logger; - - [HttpGet("ping")] - public async Task<object> Ping() { - return new { - ok = true - }; - } - - [HttpGet("whoami")] - public async Task<object> WhoAmI() { - var user = await auth.GetCurrentUser(Request); - return new { - user.Id, - user.Username, - user.Discriminator, - user.Bot, - user.Flags, - user.Rights, - user.MfaEnabled, - user.WebauthnEnabled, - }; - } -} \ 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 deleted file mode 100644
index c727077ab..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs +++ /dev/null
@@ -1,519 +0,0 @@ -using System.Diagnostics; -using ArcaneLibs; -using ArcaneLibs.Extensions; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using RabbitMQ.Client; -using Spacebar.AdminAPI.Extensions; -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("/users")] -public class UserController(ILogger<UserController> logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp, AuthenticationService auth) : ControllerBase { - private readonly ILogger<UserController> _logger = logger; - - [HttpGet] - public async IAsyncEnumerable<UserModel> Get() { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - var results = db.Users.Select(x => new UserModel { - Id = x.Id, - Username = x.Username, - Discriminator = x.Discriminator, - Avatar = x.Avatar, - AccentColor = x.AccentColor, - Banner = x.Banner, - ThemeColors = x.ThemeColors, - Pronouns = x.Pronouns, - Phone = x.Phone, - Desktop = x.Desktop, - Mobile = x.Mobile, - Premium = x.Premium, - PremiumType = x.PremiumType, - Bot = x.Bot, - Bio = x.Bio, - System = x.System, - NsfwAllowed = x.NsfwAllowed, - MfaEnabled = x.MfaEnabled, - WebauthnEnabled = x.WebauthnEnabled, - CreatedAt = x.CreatedAt, - PremiumSince = x.PremiumSince, - Verified = x.Verified, - Disabled = x.Disabled, - Deleted = x.Deleted, - Email = x.Email, - Flags = x.Flags, - PublicFlags = x.PublicFlags, - Rights = x.Rights, - ApplicationBotUser = x.ApplicationBotUser == null ? null : new(), - ConnectedAccounts = new List<UserModel.ConnectedAccountModel>(), - MessageCount = x.MessageAuthors.Count, // This property is weirdly named due to scaffolding, might patch later - SessionCount = x.Sessions.Count, - TemplateCount = x.Templates.Count, - VoiceStateCount = x.VoiceStates.Count, - GuildCount = x.Guilds.Count, - OwnedGuildCount = x.Guilds.Count(g => g.OwnerId == x.Id) - }).AsAsyncEnumerable(); - - await foreach (var user in results) { - yield return user; - } - } - - [HttpGet("meow")] - public async Task Meow() { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - Console.WriteLine("meow"); - - ConnectionFactory factory = new ConnectionFactory(); - factory.Uri = new Uri("amqp://guest:guest@127.0.0.1/"); - using var connection = await factory.CreateConnectionAsync(); - using var channel = await connection.CreateChannelAsync(); - - // await using var channel = mq.CreateChannel(); - // var channel2 = await channel.CreateChannelAsync(); - - var body = - $$""" - { - "id": "{{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}}", - "channel_id": "1322343566206308390", - "guild_id": "1322343566084673571", - "author": { - "username": "test", - "discriminator": "9177", - "id": "1322329228934500382", - "public_flags": 0, - "avatar": null, - "accent_color": null, - "banner": null, - "bio": "", - "bot": false, - "premium_since": "2024-12-27T22:24:15.867Z", - "premium_type": 2, - "theme_colors": null, - "pronouns": null, - "badge_ids": null - }, - "member": { - "index": 2, - "id": "1322329228934500382", - "guild_id": "1322343566084673571", - "nick": null, - "joined_at": "2024-12-27T23:21:14.396Z", - "premium_since": null, - "deaf": false, - "mute": false, - "pending": false, - "last_message_id": "1322346635635753061", - "joined_by": null, - "avatar": null, - "banner": null, - "bio": "", - "theme_colors": null, - "pronouns": null, - "communication_disabled_until": null, - "roles": [] - }, - "content": "{{Random.Shared.NextInt64()}}", - "timestamp": "{{DateTime.UtcNow:O}}", - "edited_timestamp": null, - "tts": false, - "mention_everyone": false, - "mentions": [], - "mention_roles": [], - "attachments": [], - "embeds": [], - "reactions": [], - "nonce": "{{Random.Shared.NextInt64()}}", - "pinned": false, - "type": 0 - } - """ - .AsBytes().ToArray(); - - await channel.ExchangeDeclareAsync(exchange: "1322343566206308390", type: ExchangeType.Fanout, durable: false); - var props = new BasicProperties() { Type = "MESSAGE_CREATE" }; - await channel.BasicPublishAsync(exchange: "1322343566206308390", routingKey: "", mandatory: true, basicProperties: props, body: body); - - await channel.CloseAsync(); - await connection.CloseAsync(); - Console.WriteLine("meowww"); - } - - [HttpGet("{id}/delete")] - public async IAsyncEnumerable<AsyncActionResult> DeleteUser(string id, [FromQuery] int messageDeleteChunkSize = 100) { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - 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)) - }); - if (messages.Any()) { - 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; - } - - if (messages.Count() >= 100) { - 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(); - } - } - } - - [HttpGet("duplicate")] - public async Task<IActionResult> Duplicate() { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - var msg = db.Messages.First(); - var channels = db.Channels.Select(x => new { x.Id, x.GuildId }).ToList(); - int count = 1; - while (true) { - foreach (var channel in channels) { - var newMsg = new Message { - Id = $"{Random.Shared.NextInt64()}", - ChannelId = channel.Id, - GuildId = channel.GuildId, - AuthorId = msg.AuthorId, - Content = msg.Content, - MemberId = msg.MemberId, - Timestamp = msg.Timestamp, - EditedTimestamp = msg.EditedTimestamp, - Tts = msg.Tts, - MentionEveryone = msg.MentionEveryone, - Attachments = msg.Attachments, - Embeds = msg.Embeds, - Reactions = msg.Reactions, - Nonce = msg.Nonce, - PinnedAt = msg.PinnedAt, - Type = msg.Type, - }; - db.Messages.Add(newMsg); - count++; - } - - if (count % 100 == 0) { - await db.SaveChangesAsync(); - await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages"); - } - - if (count >= 100_000) { - await db.SaveChangesAsync(); - await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages"); - await db.Database.ExecuteSqlRawAsync("REINDEX TABLE messages"); - return Ok(); - } - } - } - - [HttpGet("duplicate/{id}")] - public async Task<IActionResult> DuplicateMessage(ulong id, [FromQuery] int count = 100) { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - var msg = await db.Messages.FindAsync(id.ToString()); - int createdCount = 1; - while (true) { - var newMsg = new Message { - Id = $"{Random.Shared.NextInt64()}", - ChannelId = msg.ChannelId, - GuildId = msg.GuildId, - AuthorId = msg.AuthorId, - Content = msg.Content, - MemberId = msg.MemberId, - Timestamp = msg.Timestamp, - EditedTimestamp = msg.EditedTimestamp, - Tts = msg.Tts, - MentionEveryone = msg.MentionEveryone, - Attachments = msg.Attachments, - Embeds = msg.Embeds, - Reactions = msg.Reactions, - Nonce = msg.Nonce, - PinnedAt = msg.PinnedAt, - Type = msg.Type, - }; - db.Messages.Add(newMsg); - createdCount++; - - if (createdCount % 100 == 0) { - await db.SaveChangesAsync(); - } - - if (createdCount >= count) { - await db.SaveChangesAsync(); - await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages"); - await db.Database.ExecuteSqlRawAsync("REINDEX TABLE messages"); - return Ok(); - } - } - - await db.SaveChangesAsync(); - await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages"); - - return Ok(); - } - - [HttpGet("truncate_messages")] - public async Task TruncateMessages() { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - var channels = db.Channels.Select(x => new { x.Id, x.GuildId }).ToList(); - - var ss = new SemaphoreSlim(12, 12); - - async Task TruncateChannelMessages(string channelId, string guildId) { - await ss.WaitAsync(); - var tasks = Enumerable.Range(0, 99).Select(i => Task.Run(async () => { - await using var scope = sp.CreateAsyncScope(); - await using var _db = scope.ServiceProvider.GetRequiredService<SpacebarDbContext>(); - // set timeout - _db.Database.SetCommandTimeout(6000); - await _db.Database.ExecuteSqlAsync($""" - DELETE FROM messages - WHERE channel_id = '{channelId}' - AND guild_id = '{guildId}' - AND id LIKE '%{i:00}'; - """); - - Console.WriteLine($"Truncated messages for {channelId} in {guildId} ending with {i}"); - })).ToList(); - await Task.WhenAll(tasks); - ss.Release(); - } - - var tasks = channels.Select(c => TruncateChannelMessages(c.Id, c.GuildId)).ToList(); - await Task.WhenAll(tasks); - } - - private async IAsyncEnumerable<T> AggregateAsyncEnumerablesWithoutOrder<T>(params IEnumerable<IAsyncEnumerable<T>> enumerables) { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - 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 - // } - - [HttpGet("test")] - public async IAsyncEnumerable<string> Test() { - (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - - var factory = new ConnectionFactory { - Uri = new Uri(amqpConfig.ToConnectionString()) - }; - await using var mqConnection = await factory.CreateConnectionAsync(); - await using var mqChannel = await mqConnection.CreateChannelAsync(); - - var guildId = "1006649183970562092"; - // var roleId = "1006706520514028812"; //Administrator - var roleId = "1391303296148639051"; //Spacebar Maintainer - // int color = 16711680; //Administrator - int color = 99839; //Spacebar Maintainer - - await mqChannel.ExchangeDeclareAsync(exchange: guildId, type: ExchangeType.Fanout, durable: false); - - var props = new BasicProperties() { Type = "GUILD_ROLE_UPDATE" }; - int framerate = 30; - float delay = 1000f / framerate; - var secondsPerRotation = 6.243f; - // use delay, 255f = one rotation, lengthFactor = iterations to make a full rotation - var lengthFactor = (secondsPerRotation * 1000f / delay); - Console.WriteLine("Length factor: {0}, RPS: {1}", lengthFactor, 0); - var re = new RainbowEnumerator(lengthFactor: lengthFactor, offset: color, skip: 1); - var sw = Stopwatch.StartNew(); - while (true) { - var clr = re.Next(); - color = clr.r << 16 | clr.g << 8 | clr.b; - var publishSuccess = false; - do { - try { - await mqChannel.BasicPublishAsync(exchange: guildId, routingKey: "", mandatory: false, basicProperties: props, body: new { - guild_id = guildId, - role = new { - id = roleId, - guild_id = guildId, - color, - hoist = false, - managed = false, - mentionable = true, - name = "Spacebar Maintainer", - permissions = "8", - position = 5, - unicode_emoji = "", - flags = 0 - } - }.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 $"{clr.r:X2} {clr.g:X2} {clr.b:X2} | {color:X8} | {sw.Elapsed} (waiting {Math.Max(0, (int)delay - (int)sw.ElapsedMilliseconds)} out of {delay} ms)"; - await Task.Delay(Math.Max(0, (int)delay - (int)sw.ElapsedMilliseconds)); - sw.Restart(); - } - } -} diff --git a/extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs b/extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs deleted file mode 100644
index f6318e8f3..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs +++ /dev/null
@@ -1,10 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Spacebar.AdminApi.Models; -using Spacebar.Db.Models; - -namespace Spacebar.AdminAPI.Extensions; - -public static class DbExtensions { - public static string? GetString(this DbSet<Config> config, string key) => config.Find(key)?.Value; - public static SpacebarRights.Rights GetRights(this User user) => (SpacebarRights.Rights)user.Rights; -} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs b/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs deleted file mode 100644
index dea89586c..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs +++ /dev/null
@@ -1,79 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Cryptography; -using Microsoft.IdentityModel.Tokens; -using Spacebar.AdminAPI.Services; -using Spacebar.Db.Contexts; -using Spacebar.Db.Models; - -namespace Spacebar.AdminAPI.Middleware; - -public class AuthenticationMiddleware(RequestDelegate next) { - private static Dictionary<string, User> _userCache = new(); - private static Dictionary<string, DateTime> _userCacheExpiry = new(); - - public async Task InvokeAsync(HttpContext context, IServiceProvider sp) { - var config = sp.GetRequiredService<Configuration>(); - if (context.Request.Path.StartsWithSegments("/ping") || config.DisableAuthentication) { - await next(context); - return; - } - - if (!context.Request.Headers.ContainsKey("Authorization")) { - context.Response.StatusCode = 401; - await context.Response.WriteAsync("Authorization header is missing"); - return; - } - - var token = context.Request.Headers["Authorization"].ToString().Split(' ').Last(); - - var handler = new JwtSecurityTokenHandler(); - var secretFile = File.ReadAllText("../../../jwt.key.pub"); - var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); - key.ImportFromPem(secretFile); - - var res = await handler.ValidateTokenAsync(token, new TokenValidationParameters { - IssuerSigningKey = new ECDsaSecurityKey(key), - ValidAlgorithms = new[] { "ES512" }, - LogValidationExceptions = true, - // These are required to be false for the token to be valid as they aren't provided by the token - ValidateIssuer = false, - ValidateLifetime = false, - ValidateAudience = false, - }); - - if (!res.IsValid) { - context.Response.StatusCode = 401; - await context.Response.WriteAsync("Invalid token"); - return; - } - - User user; - if (_userCacheExpiry.ContainsKey(token) && _userCacheExpiry[token] < DateTime.Now) { - _userCache.Remove(token); - _userCacheExpiry.Remove(token); - } - - if (!_userCache.ContainsKey(token)) { - var db = sp.GetRequiredService<SpacebarDbContext>(); - user = await db.Users.FindAsync(config.OverrideUid ?? res.ClaimsIdentity.Claims.First(x => x.Type == "id").Value) - ?? throw new InvalidOperationException(); - _userCache[token] = user; - _userCacheExpiry[token] = DateTime.Now.AddMinutes(5); - } - - user = _userCache[token]; - if (user.Disabled) { - context.Response.StatusCode = 403; - await context.Response.WriteAsync("User is disabled"); - return; - } - - if (user.Deleted) { - context.Response.StatusCode = 403; - await context.Response.WriteAsync("User is deleted"); - return; - } - - await next(context); - } -} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Program.cs b/extra/admin-api/Spacebar.AdminAPI/Program.cs deleted file mode 100644
index 93b28b15c..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Program.cs +++ /dev/null
@@ -1,77 +0,0 @@ -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Http.Timeouts; -using Microsoft.EntityFrameworkCore; -using Spacebar.AdminAPI.Middleware; -using Spacebar.AdminAPI.Services; -using Spacebar.Db.Contexts; -using Spacebar.RabbitMqUtilities; - -var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. - -builder.Services.AddControllers(options => { - options.MaxValidationDepth = null; - // options.MaxIAsyncEnumerableBufferLimit = 1; -}).AddJsonOptions(options => { - options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - options.JsonSerializerOptions.WriteIndented = true; - // options.JsonSerializerOptions.DefaultBufferSize = ; -}).AddMvcOptions(o=> { - o.SuppressOutputFormatterBuffering = true; -}); - -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); -builder.Services.AddDbContextPool<SpacebarDbContext>(options => { - options - .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar")) - .EnableDetailedErrors(); -}); -builder.Services.AddScoped<AuthenticationService>(); -builder.Services.AddScoped<Configuration>(); -builder.Services.AddSingleton<RabbitMQConfiguration>(); -builder.Services.AddSingleton<RabbitMQService>(); - -builder.Services.AddRequestTimeouts(x => { - x.DefaultPolicy = new RequestTimeoutPolicy { - Timeout = TimeSpan.FromMinutes(10), - WriteTimeoutResponse = async context => { - context.Response.StatusCode = 504; - context.Response.ContentType = "application/json"; - await context.Response.StartAsync(); - await context.Response.WriteAsJsonAsync(new { error = "Unknown error" }); - await context.Response.CompleteAsync(); - } - }; -}); -// builder.Services.AddCors(options => { -// options.AddPolicy( -// "Open", -// policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); -// }); - -var app = builder.Build(); -app.Use((context, next) => { - context.Response.Headers["Access-Control-Allow-Origin"] = "*"; - context.Response.Headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"; - context.Response.Headers["Access-Control-Allow-Headers"] = "*, Authorization"; - if (context.Request.Method == "OPTIONS") { - context.Response.StatusCode = 200; - return Task.CompletedTask; - } - - return next(); -}); -app.UsePathBase("/_spacebar/admin"); -// app.UseCors("Open"); - -// Configure the HTTP request pipeline. -app.MapOpenApi(); - -app.UseMiddleware<AuthenticationMiddleware>(); -app.UseAuthorization(); - -app.MapControllers(); - -app.Run(); \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Properties/launchSettings.json b/extra/admin-api/Spacebar.AdminAPI/Properties/launchSettings.json deleted file mode 100644
index d362517ca..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Properties/launchSettings.json +++ /dev/null
@@ -1,23 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "Development": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5112", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Local": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5112", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Local" - } - } - } -} diff --git a/extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs b/extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs deleted file mode 100644
index d402c5b96..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs +++ /dev/null
@@ -1,42 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Cryptography; -using Microsoft.IdentityModel.Tokens; -using Spacebar.Db.Contexts; -using Spacebar.Db.Models; - -namespace Spacebar.AdminAPI.Services; - -public class AuthenticationService(SpacebarDbContext db, Configuration config) { - private static Dictionary<string, User> _userCache = new(); - private static Dictionary<string, DateTime> _userCacheExpiry = new(); - - public async Task<User> GetCurrentUser(HttpRequest request) { - if (!request.Headers.ContainsKey("Authorization")) { - Console.WriteLine(string.Join(", ", request.Headers.Keys)); - throw new UnauthorizedAccessException(); - } - - var token = request.Headers["Authorization"].ToString().Split(' ').Last(); - - var handler = new JwtSecurityTokenHandler(); - var secretFile = File.ReadAllText("../../../jwt.key.pub"); - var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); - key.ImportFromPem(secretFile); - - var res = await handler.ValidateTokenAsync(token, new TokenValidationParameters { - IssuerSigningKey = new ECDsaSecurityKey(key), - ValidAlgorithms = ["ES512"], - LogValidationExceptions = true, - // These are required to be false for the token to be valid as they aren't provided by the token - ValidateIssuer = false, - ValidateLifetime = false, - ValidateAudience = false, - }); - - if (!res.IsValid && !config.DisableAuthentication) { - throw new UnauthorizedAccessException(); - } - - return await db.Users.FindAsync(config.OverrideUid ?? res.ClaimsIdentity.Claims.First(x => x.Type == "id").Value) ?? throw new InvalidOperationException(); - } -} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs b/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs deleted file mode 100644
index a58ef5c07..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs +++ /dev/null
@@ -1,11 +0,0 @@ -namespace Spacebar.AdminAPI.Services; - -public class Configuration { - public Configuration(IConfiguration configuration) { - configuration.GetRequiredSection("SpacebarAdminApi").Bind(this); - } - - public string? OverrideUid { get; set; } - public bool DisableAuthentication { get; set; } = false; - public bool Enforce2FA { get; set; } = true; -} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Spacebar.AdminAPI.csproj b/extra/admin-api/Spacebar.AdminAPI/Spacebar.AdminAPI.csproj deleted file mode 100644
index 872e11882..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/Spacebar.AdminAPI.csproj +++ /dev/null
@@ -1,23 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk.Web"> - - <PropertyGroup> - <TargetFramework>net10.0</TargetFramework> - <Nullable>enable</Nullable> - <ImplicitUsings>enable</ImplicitUsings> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20251005-232225" /> - <PackageReference Include="ArcaneLibs.StringNormalisation" Version="1.0.0-preview.20251005-232225" /> - <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" /> - <PackageReference Include="RabbitMQ.Client" Version="7.2.0" /> - <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="..\Spacebar.AdminApi.Models\Spacebar.AdminApi.Models.csproj" /> - <ProjectReference Include="..\Spacebar.Db\Spacebar.Db.csproj" /> - <ProjectReference Include="..\Utilities\Spacebar.RabbitMqUtilities\Spacebar.RabbitMqUtilities.csproj" /> - </ItemGroup> - -</Project> diff --git a/extra/admin-api/Spacebar.AdminAPI/appsettings.Development.json b/extra/admin-api/Spacebar.AdminAPI/appsettings.Development.json deleted file mode 100644
index 032457733..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/appsettings.Development.json +++ /dev/null
@@ -1,27 +0,0 @@ -{ - "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" - } - }, - "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;", - }, - "RabbitMQ": { - "Host": "127.0.0.1", - "Port": 5673, - "Username": "guest", - "Password": "guest" - }, - "SpacebarAdminApi": { - "Enforce2FA": true, - "OverrideUid": null, - "DisableAuthentication": false - } -} diff --git a/extra/admin-api/Spacebar.AdminAPI/appsettings.json b/extra/admin-api/Spacebar.AdminAPI/appsettings.json deleted file mode 100644
index 10f68b8c8..000000000 --- a/extra/admin-api/Spacebar.AdminAPI/appsettings.json +++ /dev/null
@@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -}