495 lines
20 KiB
C#
495 lines
20 KiB
C#
using System.Diagnostics;
|
|
using ArcaneLibs;
|
|
using ArcaneLibs.Extensions;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
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("/users")]
|
|
public class UserController(ILogger<UserController> logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase {
|
|
private readonly ILogger<UserController> _logger = logger;
|
|
|
|
[HttpGet]
|
|
public IAsyncEnumerable<UserModel> Get() {
|
|
return 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 = ulong.Parse(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();
|
|
}
|
|
|
|
[HttpGet("meow")]
|
|
public async Task Meow() {
|
|
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) {
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
[HttpGet("duplicate")]
|
|
public async Task<IActionResult> Duplicate() {
|
|
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) {
|
|
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() {
|
|
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) {
|
|
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() {
|
|
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();
|
|
}
|
|
}
|
|
} |