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": "*"
-}
|