From 8fde4a49fefa9f194d39752f343f13b1fc4a351f Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Wed, 11 Mar 2026 01:01:13 +0000 Subject: [PATCH] HOLY SHIT IT WORKS! --- .gitignore | 2 +- Commands/AddCommand.cs | 9 +- Commands/CreateCommand.cs | 6 - Commands/DeleteCommand.cs | 47 +++++++ Commands/HelpCommand.cs | 3 + Commands/RemoveCommand.cs | 50 +++++++ Services/BotService.cs | 51 +++++++- Services/MessageService.cs | 76 ++++++++++- Services/ReactionRoleService.cs | 223 ++++++++++++++++++++++---------- 9 files changed, 385 insertions(+), 82 deletions(-) create mode 100644 Commands/DeleteCommand.cs create mode 100644 Commands/RemoveCommand.cs diff --git a/.gitignore b/.gitignore index 85bc7cc..3d31385 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ bin/ obj/ Reactor.sln .env -reactor.db \ No newline at end of file +reactor.db diff --git a/Commands/AddCommand.cs b/Commands/AddCommand.cs index f023d49..9217062 100644 --- a/Commands/AddCommand.cs +++ b/Commands/AddCommand.cs @@ -7,13 +7,12 @@ namespace Reactor.Commands public static class AddCommand { public static async Task Execute( + ValourClient client, Dictionary channelCache, long channelId, long messageId, string emoji, - long roleId, - ValourClient client, - Planet planet) + long roleId) { //Check if the current channel is in the cache (should never happen but you never know!) if (!channelCache.TryGetValue(channelId, out var channel)) @@ -40,12 +39,14 @@ namespace Reactor.Commands return; } - // Add the emoji to the message + //Add the emoji to the message await message.AddReactionAsync(emoji); //Add reaction-role mapping to DB and Cache await ReactionRoleService.AddReactionAsync(messageId, emoji, roleId); + ReactionRoleService.SubscribeToMessageReactions(client, channelCache, message); + await channel.SendMessageAsync($"Added reaction {emoji} -> role {roleId} for message {messageId}"); } } diff --git a/Commands/CreateCommand.cs b/Commands/CreateCommand.cs index a8fa1ca..8d6c0fa 100644 --- a/Commands/CreateCommand.cs +++ b/Commands/CreateCommand.cs @@ -61,12 +61,6 @@ namespace Reactor.Commands Reactions = new Dictionary() }; - //Subscribe events - // sentMessage.ReactionAdded += async () => - // { - // await ReactionRoleService.HandleReactionAddedAsync(channelCache, sentMessage); - // }; - Console.WriteLine($"Created reaction message {sentMessage.Id} in channel {channelId}"); } } diff --git a/Commands/DeleteCommand.cs b/Commands/DeleteCommand.cs new file mode 100644 index 0000000..571c73f --- /dev/null +++ b/Commands/DeleteCommand.cs @@ -0,0 +1,47 @@ +using Reactor.Services; +using Valour.Sdk.Client; +using Valour.Sdk.Models; + +namespace Reactor.Commands +{ + public static class DeleteCommand + { + public static async Task Execute( + ValourClient client, + Dictionary channelCache, + long channelId, + long messageId) + { + //Check if channel in cache + if (!channelCache.TryGetValue(channelId, out var channel)) + { + Console.WriteLine($"Channel {channelId} not found in cache."); + return; + } + + //Check if message is actually a reaction message + if (!ReactionRoleService.Messages.TryGetValue(messageId, out var reactionMsg)) + { + await channel.SendMessageAsync($"Message ID {messageId} is not tracked as a reaction message."); + return; + } + + //Delete the actual message + var recentMessages = await channel.GetLastMessagesAsync(50); + var message = recentMessages.FirstOrDefault(m => m.Id == messageId); + if (message != null) + { + await message.DeleteAsync(); + } else + { + Console.WriteLine($"Message {messageId} not found in recent messages, skipping deletion of message."); + } + + //Remove from cache and database + await ReactionRoleService.RemoveMessageAsync(messageId); + ReactionRoleService.ResetSubscription(messageId); + + await channel.SendMessageAsync($"Deleted reaction message {messageId} and all its role mappings."); + } + } +} \ No newline at end of file diff --git a/Commands/HelpCommand.cs b/Commands/HelpCommand.cs index 6d488c8..8c78981 100644 --- a/Commands/HelpCommand.cs +++ b/Commands/HelpCommand.cs @@ -10,6 +10,9 @@ public static class HelpCommand - `{prefix}help` - Shows this list. - `{prefix}source` - Shows my source code! - `{prefix}create` - Creates the Reaction Message. + - `{prefix}delete` - Deletes a Reaction Message. + - `{prefix}add` - Adds a Reaction Role to a Valid Message. + - `{prefix}remove` - Removes a Reaction Role from a Valid Message. "; if (channelCache.TryGetValue(channelId, out var channel)) diff --git a/Commands/RemoveCommand.cs b/Commands/RemoveCommand.cs new file mode 100644 index 0000000..7272f21 --- /dev/null +++ b/Commands/RemoveCommand.cs @@ -0,0 +1,50 @@ +using Reactor.Services; +using Valour.Sdk.Client; +using Valour.Sdk.Models; + +namespace Reactor.Commands +{ + public static class RemoveCommand + { + public static async Task Execute( + ValourClient client, + Dictionary channelCache, + long channelId, + long messageId, + string emoji) + { + //Check if channel in cache + if (!channelCache.TryGetValue(channelId, out var channel)) + { + Console.WriteLine($"Channel {channelId} not found in cache."); + return; + } + + //Check if message is actually a reaction message + if (!ReactionRoleService.Messages.TryGetValue(messageId, out var reactionMsg)) + { + await channel.SendMessageAsync($"Message ID {messageId} is not tracked as a reaction message."); + return; + } + + //Check if the emoji is actually a valid reaction on the message + if (!reactionMsg.Reactions.ContainsKey(emoji)) + { + await channel.SendMessageAsync($"Emoji {emoji} is not mapped to any role on message {messageId}."); + return; + } + + //Fetch the message and remove the reaction + var recentMessages = await channel.GetLastMessagesAsync(50); + var message = recentMessages.FirstOrDefault(m => m.Id == messageId); + if (message != null) + { + await message.RemoveReactionAsync(emoji); + } + + await ReactionRoleService.RemoveReactionAsync(messageId, emoji); + + await channel.SendMessageAsync($"Removed reaction {emoji} from message {messageId}."); + } + } +} \ No newline at end of file diff --git a/Services/BotService.cs b/Services/BotService.cs index fd6f93c..61d213b 100644 --- a/Services/BotService.cs +++ b/Services/BotService.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using Valour.Sdk.Client; using Valour.Sdk.Models; @@ -30,7 +31,7 @@ namespace Reactor.Services //Initialize the Database await DatabaseService.InitializeAsync(); - await ReactionRoleService.LoadAllAsync(); + await ReactionRoleService.LoadAllAsync(client); Console.WriteLine($"Loaded {ReactionRoleService.Messages.Count} reaction messages into memory."); //Initialize the Planets @@ -40,8 +41,52 @@ namespace Reactor.Services await PlanetService.InitializePlanetsAsync(client, channelCache, initializedPlanets); }; - //Initialize the Messages - client.MessageService.MessageReceived += async (msg) => await MessageService.HandleMessageAsync(client, channelCache, msg, prefix); + //Fucking pain in my ass is what this is, i dont even wanna comment on it + foreach (var reactionMessage in ReactionRoleService.Messages.Values.ToList()) + { + try + { + if(!channelCache.TryGetValue(reactionMessage.ChannelId, out var channel)) + { + Console.WriteLine($"Channel {reactionMessage.ChannelId} not found, pruning message {reactionMessage.MessageId}."); + await ReactionRoleService.RemoveMessageAsync(reactionMessage.MessageId); + continue; + } + + var messages = await channel.GetMessagesAsync(reactionMessage.MessageId + 1, 50); + Console.WriteLine($"Fetched {messages?.Count ?? 0} messages from channel {reactionMessage.ChannelId}"); + var match = messages?.FirstOrDefault(m => m.Id == reactionMessage.MessageId); + + if (match == null) + { + Console.WriteLine($"Message {reactionMessage.MessageId} not found, pruning."); + await ReactionRoleService.RemoveMessageAsync(reactionMessage.MessageId); + continue; + } + + ReactionRoleService.SubscribeToMessageReactions(client, channelCache, match); + Console.WriteLine($"Subscribed to reactions for message {reactionMessage.MessageId}"); + } catch (Exception ex) + { + Console.WriteLine($"Error setting up message {reactionMessage.MessageId}: {ex.Message}, pruning."); + await ReactionRoleService.RemoveMessageAsync(reactionMessage.MessageId); + } + } + + client.MessageService.MessageReceived += async (message) => + { + await MessageService.HandleMessageAsync(client, channelCache, message, prefix); + }; + + client.MessageService.MessageDeleted += async (message) => + { + if (ReactionRoleService.Messages.ContainsKey(message.Id)) + { + await ReactionRoleService.RemoveMessageAsync(message.Id); + ReactionRoleService.ResetSubscription(message.Id); + Console.WriteLine($"Reaction message {message.Id} was deleted, removed from DB and cache."); + } + }; //Bot is active and ready Console.WriteLine("Bot ready and listening..."); diff --git a/Services/MessageService.cs b/Services/MessageService.cs index 39c5fbe..36ba40e 100644 --- a/Services/MessageService.cs +++ b/Services/MessageService.cs @@ -1,6 +1,7 @@ using Reactor.Commands; using Valour.Sdk.Client; using Valour.Sdk.Models; +using Valour.Shared.Authorization; namespace Reactor.Services { @@ -24,6 +25,8 @@ namespace Reactor.Services var member = await message.FetchAuthorMemberAsync(); string memberPing = member != null ? $"«@m-{member.Id}»" : ""; + bool hasPermission = await HasPermissionAsync(member, channelCache[channelId]); + string withoutPrefix = content.Substring(prefix.Length); var parts = withoutPrefix.Split(' ', StringSplitOptions.RemoveEmptyEntries); @@ -44,6 +47,13 @@ namespace Reactor.Services break; case "create": + + if (!hasPermission) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} You need Manage Roles or Full Control to use this command."); + return; + } + if (parts.Length < 2) { await channelCache[channelId].SendMessageAsync($"{memberPing} Usage: {prefix}create "); @@ -57,10 +67,40 @@ namespace Reactor.Services } var messageText = string.Join(' ', parts[1..]); - await CreateCommand.Execute(channelCache, channelId, messageText, message.PlanetId.Value); + await CreateCommand.Execute(client, channelCache, channelId, messageText, message.PlanetId.Value); + break; + + case "delete": + + if (!hasPermission) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} You need Manage Roles or Full Control to use this command."); + return; + } + + if (parts.Length < 2) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} Usage: {prefix}delete "); + return; + } + + if (!long.TryParse(parts[1], out var deleteMsgId)) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} Invalid message ID."); + return; + } + + await DeleteCommand.Execute(client, channelCache, channelId, deleteMsgId); break; case "add": + + if (!hasPermission) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} You need Manage Roles or Full Control to use this command."); + return; + } + if (parts.Length < 4) { await channelCache[channelId].SendMessageAsync($"{memberPing} Usage: {prefix}add "); @@ -81,9 +121,41 @@ namespace Reactor.Services return; } - await AddCommand.Execute(channelCache, channelId, msgId, emoji, roleId, client, message.Planet); + await AddCommand.Execute(client, channelCache, channelId, msgId, emoji, roleId); + break; + + case "remove": + + if (!hasPermission) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} You need Manage Roles or Full Control to use this command."); + return; + } + + if (parts.Length < 3) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} Usage: {prefix}remove "); + return; + } + + if (!long.TryParse(parts[1], out var removeMsgId)) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} Invalid message ID."); + return; + } + + var removeEmoji = parts[2]; + await RemoveCommand.Execute(client, channelCache, channelId, removeMsgId, removeEmoji); break; } } + + private static async Task HasPermissionAsync(PlanetMember member, Channel channel) + { + if (member == null) return false; + + return member.HasPermission(PlanetPermissions.FullControl) || + member.HasPermission(PlanetPermissions.ManageRoles); + } } } \ No newline at end of file diff --git a/Services/ReactionRoleService.cs b/Services/ReactionRoleService.cs index d0819b2..8a2784d 100644 --- a/Services/ReactionRoleService.cs +++ b/Services/ReactionRoleService.cs @@ -13,7 +13,7 @@ namespace Reactor.Services public static Dictionary Messages { get; private set; } = new(); //Load all messages and reaction role mappings - public static async Task LoadAllAsync() + public static async Task LoadAllAsync(ValourClient client) { Messages.Clear(); @@ -80,95 +80,186 @@ namespace Reactor.Services msg.Reactions[emoji] = roleId; } - public static async Task HandleReactionAddedAsync( + public static async Task RemoveReactionAsync(long messageId, string emoji) + { + if (!Messages.TryGetValue(messageId, out var msg)) + return; + + using var connection = new SqliteConnection(_connectionString); + await connection.OpenAsync(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "DELETE FROM ReactionRoles WHERE ReactionMessageId = @msgId AND Emoji = @emoji"; + cmd.Parameters.AddWithValue("@msgId", msg.Id); + cmd.Parameters.AddWithValue("@emoji", emoji); + await cmd.ExecuteNonQueryAsync(); + + msg.Reactions.Remove(emoji); + Console.WriteLine($"Removed reaction {emoji} from message {messageId}."); + } + + private static readonly HashSet _subscribedMessages = new(); + + public static void SubscribeToMessageReactions( + ValourClient client, Dictionary channelCache, - Message message) + Message syncedMessage) + { + if (!_subscribedMessages.Add(syncedMessage.Id)) + { + Console.WriteLine($"Already subscribed to message {syncedMessage.Id}, skipping."); + return; + } + + Action addhandler = (reaction) => + { + _ = Task.Run(async () => + { + try + { + Console.WriteLine($"Reaction added: {reaction.Emoji} by {reaction.AuthorUserId}"); + await HandleReactionAddedAsync(client, channelCache, syncedMessage, reaction); + } + catch (Exception ex) + { + Console.WriteLine($"Error in addhandler: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + }); + }; + + Action removehandler = (reaction) => + { + _ = Task.Run(async () => + { + try + { + Console.WriteLine($"Reaction removed: {reaction.Emoji} by {reaction.AuthorUserId}"); + await HandleReactionRemovedAsync(client, channelCache, syncedMessage, reaction); + } + catch (Exception ex) + { + Console.WriteLine($"Error in removehandler: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + }); + }; + + syncedMessage.ReactionAdded += addhandler; + syncedMessage.ReactionRemoved += removehandler; + } + + public static void ResetSubscription(long messageId) + { + _subscribedMessages.Remove(messageId); + } + + public static async Task HandleReactionAddedAsync( + ValourClient client, + Dictionary channelCache, + Message message, + MessageReaction reaction) { if (!Messages.TryGetValue(message.Id, out var cachedMsg)) return; if (!channelCache.TryGetValue(cachedMsg.ChannelId, out var channel)) return; - - foreach (var kvp in message.Reactions) + + if (!cachedMsg.Reactions.TryGetValue(reaction.Emoji, out var roleId)) + return; + + var role = channel.Planet.Roles.FirstOrDefault(r => r.Id == roleId); + string roleName = role != null ? role.Name : $"Role {roleId}"; + + var member = await channel.Planet.FetchMemberByUserAsync(reaction.AuthorUserId); + if (member == null) return; + + // Check if member already has the role + if (member.Roles.Any(r => r.Id == roleId)) { - string emoji = kvp.Emoji; - if (!cachedMsg.Reactions.TryGetValue(emoji, out var roleId)) - continue; + Console.WriteLine($"User {reaction.AuthorUserId} already has role {roleId}, skipping."); + return; + } - //Fetch role name - var role = channel.Planet.Roles.FirstOrDefault(r => r.Id == roleId); - string roleName = role != null ? role.Name : $"Role {roleId}"; + await member.AddRoleAsync(roleId); - //Fetch member - var member = await channel.Planet.FetchMemberAsync(kvp.AuthorUserId); - if (member == null) return; - - //Apply role to user - await member.AddRoleAsync(roleId); - - //Confirmation - var confirm = await channel.SendMessageAsync($"«@m-{member.Id}» has been given the role {roleName}"); + var confirm = await channel.SendMessageAsync($"«@m-{member.Id}» has been added to the role {roleName}"); + if (confirm.Success && confirm.Data != null) + { await Task.Delay(cachedMsg.DeleteDelaySeconds * 1000); - await confirm.Data.DeleteAsync(); + if (client.Cache.Messages.TryGet(confirm.Data.Id, out var cachedConfirm)) + { + await cachedConfirm.DeleteAsync(); + } else + { + Console.WriteLine($"Could not find confirmation message {confirm.Data.Id} in cache."); + } } } - // public static async Task HandleReactionAddedAsync( - // ValourClient client, - // Dictionary channelCache, - // MessageReaction reaction) - // { - // if (!Messages.TryGetValue(reaction.MessageId, out var msg)) - // return; + public static async Task HandleReactionRemovedAsync( + ValourClient client, + Dictionary channelCache, + Message message, + MessageReaction reaction) + { + if (!Messages.TryGetValue(message.Id, out var cachedMsg)) + return; - // if (!msg.Reactions.TryGetValue(reaction.Emoji, out var roleId)) - // return; - - // if (!channelCache.TryGetValue(msg.ChannelId, out var channel)) - // return; + if (!channelCache.TryGetValue(cachedMsg.ChannelId, out var channel)) + return; - // var role = channel.Planet.Roles.FirstOrDefault(r => r.Id == roleId); - // string roleName = role != null ? role.Name : $"Role {roleId}"; + if (!cachedMsg.Reactions.TryGetValue(reaction.Emoji, out var roleId)) + return; - // //Fetch the member - // var member = await channel.Planet.FetchMemberAsync(reaction.AuthorUserId); - // if (member == null) return; + var role = channel.Planet.Roles.FirstOrDefault(r => r.Id == roleId); + string roleName = role != null ? role.Name : $"Role {roleId}"; - // //Add role - // await member.AddRoleAsync(roleId); + var member = await channel.Planet.FetchMemberByUserAsync(reaction.AuthorUserId); + if (member == null) return; - // //Confirmation - // var confirm = await channel.SendMessageAsync($"«@m-{member.Id}» has been given the role {roleName}"); - // await Task.Delay(msg.DeleteDelaySeconds * 1000); - // await confirm.Data.DeleteAsync(); - // } + // Check if member actually has the role before removing + if (!member.Roles.Any(r => r.Id == roleId)) + { + Console.WriteLine($"User {reaction.AuthorUserId} does not have role {roleId}, skipping."); + return; + } - // public static async Task HandleReactionRemovedAsync( - // ValourClient client, - // Dictionary channelCache, - // MessageReaction reaction) - // { - // if (!Messages.TryGetValue(reaction.MessageId, out var msg)) - // return; + await member.RemoveRoleAsync(roleId); - // if (!msg.Reactions.TryGetValue(reaction.Emoji, out var roleId)) - // return; - - // if (!channelCache.TryGetValue(msg.ChannelId, out var channel)) - // return; + var confirm = await channel.SendMessageAsync($"«@m-{member.Id}» has been removed from the role {roleName}"); + if (confirm.Success && confirm.Data != null) + { + await Task.Delay(cachedMsg.DeleteDelaySeconds * 1000); + if (client.Cache.Messages.TryGet(confirm.Data.Id, out var cachedConfirm)) + { + await cachedConfirm.DeleteAsync(); + } else + { + Console.WriteLine($"Could not find confirmation message {confirm.Data.Id} in cache."); + } + } + } - // var role = channel.Planet.Roles.FirstOrDefault(r => r.Id == roleId); - // string roleName = role != null ? role.Name : $"role {roleId}"; + public static async Task RemoveMessageAsync(long messageId) + { + if (!Messages.TryGetValue(messageId, out var msg)) return; - // var member = await channel.Planet.FetchMemberAsync(reaction.AuthorUserId); - // if (member == null) return; + using var connection = new SqliteConnection(_connectionString); + await connection.OpenAsync(); - // await member.RemoveRoleAsync(roleId); + var cmd = connection.CreateCommand(); + cmd.CommandText = @" + DELETE FROM ReactionRoles WHERE ReactionMessageId = @id; + DELETE FROM ReactionMessages WHERE MessageId = @messageId; + "; + cmd.Parameters.AddWithValue("@id", msg.Id); + cmd.Parameters.AddWithValue("@messageId", messageId); + await cmd.ExecuteNonQueryAsync(); - // var confirm = await channel.SendMessageAsync($"«@m-{member.Id}» has been removed from the role {roleName}"); - // await Task.Delay(msg.DeleteDelaySeconds * 1000); - // await confirm.Data.DeleteAsync(); - // } + Messages.Remove(messageId); + Console.WriteLine($"Removed stale reaction message {messageId} from DB and memory."); + } } } \ No newline at end of file