From 56dd6a61663dbd9a605cf5340e18c671779b017a Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sat, 28 Feb 2026 21:59:37 +0000 Subject: [PATCH 01/10] Dev Branch yay! --- Commands/HelpCommand.cs | 8 ++-- Commands/SourceCommand.cs | 14 +++++++ Program.cs | 4 ++ utils.cs | 82 --------------------------------------- 4 files changed, 22 insertions(+), 86 deletions(-) create mode 100644 Commands/SourceCommand.cs delete mode 100644 utils.cs diff --git a/Commands/HelpCommand.cs b/Commands/HelpCommand.cs index 493d519..a8c84d5 100644 --- a/Commands/HelpCommand.cs +++ b/Commands/HelpCommand.cs @@ -9,9 +9,9 @@ public static class HelpComamnd string helpMessage = $@"**Reactor Commands**: - `{prefix}help` - Shows this list."; - if (channelCache.TryGetValue(channelId, out var channel)) - { - await channel.SendMessageAsync($"{memberPing}\n{helpMessage}"); - } + if (channelCache.TryGetValue(channelId, out var channel)) + { + await channel.SendMessageAsync($"{memberPing}\n{helpMessage}"); + } } } \ No newline at end of file diff --git a/Commands/SourceCommand.cs b/Commands/SourceCommand.cs new file mode 100644 index 0000000..1b74bde --- /dev/null +++ b/Commands/SourceCommand.cs @@ -0,0 +1,14 @@ +using Valour.Sdk.Models; + +namespace Reactor.Commands; + +public static class SourceComamnd +{ + public static async Task Execute(Dictionary channelCache, long channelId, string memberPing) + { + if (channelCache.TryGetValue(channelId, out var channel)) + { + await channel.SendMessageAsync($"{memberPing} You can see my source code here: https://github.com/SkyJoshua/Reactor"); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 39a91d6..cc6b9af 100644 --- a/Program.cs +++ b/Program.cs @@ -106,6 +106,10 @@ namespace Reactor case "help": await HelpComamnd.Execute(_channelCache, channelId, _prefix, memberPing); break; + + case "source": + await SourceComamnd.Execute(_channelCache, channelId, memberPing); + break; } } diff --git a/utils.cs b/utils.cs deleted file mode 100644 index 8ff18a9..0000000 --- a/utils.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Valour.Sdk.Models; - -namespace Reactor -{ - public static class Utils - { - - private static readonly HttpClient _http = new HttpClient(); - private static long _valourUserCount; - - public static long ValourUserCount => _valourUserCount; - - - - public static bool IsSingleEmoji(string input) - { - if (string.IsNullOrWhiteSpace(input)) - return false; - - input = input.Trim(); - - var enumerator = StringInfo.GetTextElementEnumerator(input); - int count = 0; - - while (enumerator.MoveNext()) - count++; - - return count == 1; - } - - public static bool ContainsAny(string input, params string[] values) - { - var lower = input.ToLower(); - - foreach (var value in values) - { - if (lower.Contains(value.ToLower())) - return true; - }; - - return false; - } - - public static async Task SendReplyAsync(Dictionary channelCache, long channel, string reply) - { - if (channelCache.TryGetValue(channel, out var chan)) - { - await chan.SendMessageAsync(reply); - } - else - { - Console.WriteLine($"Channel {channel} was not found in the cache."); - }; - } - - public static async Task UpdateValourUserCountAsync() - { - try - { - var response = await _http.GetStringAsync("https://api.valour.gg/api/users/count"); - - _valourUserCount = JsonSerializer.Deserialize(response); - - Console.WriteLine($"Valour user count updated: {_valourUserCount}"); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to update Valour user count: {ex.Message}"); - } - } - - public static void StartValourUserUpdater() - { - var timer = new System.Timers.Timer(300_000); - timer.Elapsed += async (_, _) => await UpdateValourUserCountAsync(); - timer.AutoReset = true; - timer.Start(); - } - }; -}; \ No newline at end of file From 940ab3535adfdb2439313ee52f4cf24dc9b1322a Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sat, 28 Feb 2026 22:01:12 +0000 Subject: [PATCH 02/10] added source command to help command --- Commands/HelpCommand.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Commands/HelpCommand.cs b/Commands/HelpCommand.cs index a8c84d5..67e9486 100644 --- a/Commands/HelpCommand.cs +++ b/Commands/HelpCommand.cs @@ -7,7 +7,9 @@ public static class HelpComamnd public static async Task Execute(Dictionary channelCache, long channelId, String prefix, string memberPing) { string helpMessage = $@"**Reactor Commands**: - - `{prefix}help` - Shows this list."; + - `{prefix}help` - Shows this list. + - `{prefix}source` - Shows my source code! + "; if (channelCache.TryGetValue(channelId, out var channel)) { From 2a27da1ab474e50be6dcfdd669796926efcb72df Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sat, 28 Feb 2026 22:40:27 +0000 Subject: [PATCH 03/10] holy shit I am the goat --- Commands/HelpCommand.cs | 2 +- Commands/SourceCommand.cs | 2 +- Program.cs | 118 +++++-------------------------------- Services/BotService.cs | 42 +++++++++++++ Services/MessageService.cs | 46 +++++++++++++++ Services/PlanetService.cs | 39 ++++++++++++ 6 files changed, 145 insertions(+), 104 deletions(-) create mode 100644 Services/BotService.cs create mode 100644 Services/MessageService.cs create mode 100644 Services/PlanetService.cs diff --git a/Commands/HelpCommand.cs b/Commands/HelpCommand.cs index 67e9486..ff4e71a 100644 --- a/Commands/HelpCommand.cs +++ b/Commands/HelpCommand.cs @@ -2,7 +2,7 @@ using Valour.Sdk.Models; namespace Reactor.Commands; -public static class HelpComamnd +public static class HelpCommand { public static async Task Execute(Dictionary channelCache, long channelId, String prefix, string memberPing) { diff --git a/Commands/SourceCommand.cs b/Commands/SourceCommand.cs index 1b74bde..ee3847c 100644 --- a/Commands/SourceCommand.cs +++ b/Commands/SourceCommand.cs @@ -2,7 +2,7 @@ using Valour.Sdk.Models; namespace Reactor.Commands; -public static class SourceComamnd +public static class SourceCommand { public static async Task Execute(Dictionary channelCache, long channelId, string memberPing) { diff --git a/Program.cs b/Program.cs index cc6b9af..1048304 100644 --- a/Program.cs +++ b/Program.cs @@ -1,122 +1,35 @@ using Valour.Sdk.Client; using Valour.Sdk.Models; using DotNetEnv; -using Valour.Shared.Models; -using Reactor.Commands; +using Reactor.Services; namespace Reactor { public class Reactor { - private ValourClient _client; - private Dictionary _channelCache = new(); - private HashSet _initializedPlanets = new(); - private string _prefix = "r."; + private readonly ValourClient _client; + private readonly Dictionary _channelCache = new(); + private readonly HashSet _initializedPlanets = new(); + private readonly string _prefix = "r."; - public Reactor(string token) + public Reactor() { - Env.Load(); _client = new ValourClient("https://api.valour.gg/"); _client.SetupHttpClient(); - InitializeBotAsync(token).GetAwaiter().GetResult(); } - //Initialize the bot. - private async Task InitializeBotAsync(string token) + public async Task StartAsync(string token) { - if (string.IsNullOrWhiteSpace(token)) - { - Console.WriteLine("TOKEN not set."); - return; - } - - var loginResult = await _client.InitializeUser(token); - if (!loginResult.Success) - { - Console.WriteLine($"Login failed: {loginResult.Message}"); - return; - } - - Console.WriteLine($"Logged in as {_client.Me.Name} (ID: {_client.Me.Id})"); - - await InitializePlanetsAsync(); - - _client.PlanetService.JoinedPlanetsUpdated += async () => - { - await InitializePlanetsAsync(); - }; - - _client.MessageService.MessageReceived += async (msg) => await HandleMessageAsync(msg); - - Console.WriteLine("Bot ready and listening..."); + await BotService.InitializeBotAsync( + token, + _client, + _channelCache, + _initializedPlanets, + _prefix + ); } - - //Initalize the planets. - private async Task InitializePlanetsAsync() - { - foreach (var planet in _client.PlanetService.JoinedPlanets) - { - if (_initializedPlanets.Contains(planet.Id)) - continue; - - Console.WriteLine($"Initializing Planet: {planet.Name}"); - - await planet.EnsureReadyAsync(); - await planet.FetchInitialDataAsync(); - - foreach (var channel in planet.Channels) - { - _channelCache[channel.Id] = channel; - - if (channel.ChannelType == ChannelTypeEnum.PlanetChat) - { - await channel.OpenWithResult("Reactor"); - Console.WriteLine($"Realtime opened for: {planet.Name} -> {channel.Name}"); - } - } - - _initializedPlanets.Add(planet.Id); - } - } - - //Message handler. - private async Task HandleMessageAsync(Message message) - { - if (message.AuthorUserId == _client.Me.Id) return; - - string content = message.Content ?? ""; - if (string.IsNullOrWhiteSpace(content)) return; - if (!content.StartsWith(_prefix)) return; - - long channelId = message.ChannelId; - - var member = await message.FetchAuthorMemberAsync(); - string memberPing = member != null ? $"«@m-{member.Id}»" : ""; - - string withoutPrefix = content.Substring(_prefix.Length); - - var parts = withoutPrefix.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 0) return; - - string command = parts[0].ToLower(); - string[] args = parts[1..]; - - switch (command) - { - case "help": - await HelpComamnd.Execute(_channelCache, channelId, _prefix, memberPing); - break; - - case "source": - await SourceComamnd.Execute(_channelCache, channelId, memberPing); - break; - } - } - } - - //Because it required a main or something idk I hate C# :) public class Program { public static async Task Main(string[] args) @@ -131,7 +44,8 @@ namespace Reactor return; } - var bot = new Reactor(token); + var bot = new Reactor(); + await bot.StartAsync(token); await Task.Delay(Timeout.Infinite); } diff --git a/Services/BotService.cs b/Services/BotService.cs new file mode 100644 index 0000000..aea440b --- /dev/null +++ b/Services/BotService.cs @@ -0,0 +1,42 @@ +using Valour.Sdk.Client; +using Valour.Sdk.Models; + +namespace Reactor.Services +{ + public static class BotService + { + public static async Task InitializeBotAsync( + string token, + ValourClient client, + Dictionary channelCache, + HashSet initializedPlanets, + string prefix) + { + if (string.IsNullOrWhiteSpace(token)) + { + Console.WriteLine("TOKEN not set."); + return; + } + + var loginResult = await client.InitializeUser(token); + if (!loginResult.Success) + { + Console.WriteLine($"Login failed: {loginResult.Message}"); + return; + } + + Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})"); + + await PlanetService.InitializePlanetsAsync(client, channelCache, initializedPlanets); + + client.PlanetService.JoinedPlanetsUpdated += async () => + { + await PlanetService.InitializePlanetsAsync(client, channelCache, initializedPlanets); + }; + + client.MessageService.MessageReceived += async (msg) => await MessageService.HandleMessageAsync(client, channelCache, msg, prefix); + + Console.WriteLine("Bot ready and listening..."); + } + } +} \ No newline at end of file diff --git a/Services/MessageService.cs b/Services/MessageService.cs new file mode 100644 index 0000000..c56af1a --- /dev/null +++ b/Services/MessageService.cs @@ -0,0 +1,46 @@ +using Reactor.Commands; +using Valour.Sdk.Client; +using Valour.Sdk.Models; + +namespace Reactor.Services +{ + public static class MessageService + { + public static async Task HandleMessageAsync( + ValourClient client, + Dictionary channelCache, + Message message, + string prefix) + { + if (message.AuthorUserId == client.Me.Id) return; + + string content = message.Content ?? ""; + if (string.IsNullOrWhiteSpace(content)) return; + if (!content.StartsWith(prefix)) return; + + long channelId = message.ChannelId; + + var member = await message.FetchAuthorMemberAsync(); + string memberPing = member != null ? $"«@m-{member.Id}»" : ""; + + string withoutPrefix = content.Substring(prefix.Length); + + var parts = withoutPrefix.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) return; + + string command = parts[0].ToLower(); + string[] args = parts[1..]; + + switch (command) + { + case "help": + await HelpCommand.Execute(channelCache, channelId, prefix, memberPing); + break; + + case "source": + await SourceCommand.Execute(channelCache, channelId, memberPing); + break; + } + } + } +} \ No newline at end of file diff --git a/Services/PlanetService.cs b/Services/PlanetService.cs new file mode 100644 index 0000000..08b1817 --- /dev/null +++ b/Services/PlanetService.cs @@ -0,0 +1,39 @@ +using Valour.Sdk.Client; +using Valour.Sdk.Models; +using Valour.Shared.Models; + +namespace Reactor.Services +{ + public static class PlanetService + { + public static async Task InitializePlanetsAsync( + ValourClient client, + Dictionary channelCache, + HashSet initializedPlanets) + { + foreach (var planet in client.PlanetService.JoinedPlanets) + { + if (initializedPlanets.Contains(planet.Id)) + continue; + + Console.WriteLine($"Initializing Planet: {planet.Name}"); + + await planet.EnsureReadyAsync(); + await planet.FetchInitialDataAsync(); + + foreach (var channel in planet.Channels) + { + channelCache[channel.Id] = channel; + + if (channel.ChannelType == ChannelTypeEnum.PlanetChat) + { + await channel.OpenWithResult("Reactor"); + Console.WriteLine($"Realtime opened for: {planet.Name} -> {channel.Name}"); + } + } + + initializedPlanets.Add(planet.Id); + } + } + } +} \ No newline at end of file From f468b8e2dc05c328045dad86e13ca829f3b9e9c6 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sat, 28 Feb 2026 22:41:28 +0000 Subject: [PATCH 04/10] Rename to Reactor.cs because, Yes. --- Program.cs => Reactor.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Program.cs => Reactor.cs (100%) diff --git a/Program.cs b/Reactor.cs similarity index 100% rename from Program.cs rename to Reactor.cs From e806e3cc1fccd3f31088354016693d2244fad04a Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sat, 28 Feb 2026 23:12:26 +0000 Subject: [PATCH 05/10] DATABASE WOOOOOOOOOOOOOOOOOOO! --- .gitignore | 3 ++- Reactor.csproj | 1 + Services/BotService.cs | 10 ++++++++-- Services/DatabaseService.cs | 40 +++++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 Services/DatabaseService.cs diff --git a/.gitignore b/.gitignore index 932fb31..85bc7cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ bin/ obj/ Reactor.sln -.env \ No newline at end of file +.env +reactor.db \ No newline at end of file diff --git a/Reactor.csproj b/Reactor.csproj index f7b91fe..0209f13 100644 --- a/Reactor.csproj +++ b/Reactor.csproj @@ -9,6 +9,7 @@ + diff --git a/Services/BotService.cs b/Services/BotService.cs index aea440b..8c22ed7 100644 --- a/Services/BotService.cs +++ b/Services/BotService.cs @@ -12,30 +12,36 @@ namespace Reactor.Services HashSet initializedPlanets, string prefix) { + //Check token is valid if (string.IsNullOrWhiteSpace(token)) { Console.WriteLine("TOKEN not set."); return; } + //Login to the bot var loginResult = await client.InitializeUser(token); if (!loginResult.Success) { Console.WriteLine($"Login failed: {loginResult.Message}"); return; } - Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})"); - await PlanetService.InitializePlanetsAsync(client, channelCache, initializedPlanets); + //Initialize the Database + await DatabaseService.InitializeAsync(); + //Initialize the Planets + await PlanetService.InitializePlanetsAsync(client, channelCache, initializedPlanets); client.PlanetService.JoinedPlanetsUpdated += async () => { await PlanetService.InitializePlanetsAsync(client, channelCache, initializedPlanets); }; + //Initialize the Messages client.MessageService.MessageReceived += async (msg) => await MessageService.HandleMessageAsync(client, channelCache, msg, prefix); + //Bot is active and ready Console.WriteLine("Bot ready and listening..."); } } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs new file mode 100644 index 0000000..ae69190 --- /dev/null +++ b/Services/DatabaseService.cs @@ -0,0 +1,40 @@ +using System.Linq.Expressions; +using Microsoft.Data.Sqlite; + +namespace Reactor.Services +{ + public static class DatabaseService + { + private static string _connectionString = "Data Source=reactor.db"; + + public static async Task InitializeAsync() + { + using var connection = new SqliteConnection(_connectionString); + await connection.OpenAsync(); + + //ReactionMessages Table + var cmd1 = connection.CreateCommand(); + cmd1.CommandText = + "CREATE TABLE IF NOT EXISTS ReactionMessages (" + + "Id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "PlanetId INTEGER NOT NULL, " + + "ChannelId INTEGER NOT NULL, " + + "MessageId INTEGER NOT NULL UNIQUE, " + + "DeleteDelaySeconds INTEGER NOT NULL DEFAULT 5" + + ")"; + await cmd1.ExecuteNonQueryAsync(); + + //ReactionRoles table + var cmd2 = connection.CreateCommand(); + cmd2.CommandText = + "CREATE TABLE IF NOT EXISTS ReactionRoles (" + + "Id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "ReactionMessageId INTEGER NOT NULL, " + + "Emoji TEXT NOT NULL, " + + "RoleId INTEGER NOT NULL, " + + "FOREIGN KEY (ReactionMessageId) REFERENCES ReactionMessages(Id) ON DELETE CASCADE" + + ")"; + await cmd2.ExecuteNonQueryAsync(); + } + } +} \ No newline at end of file From df122f1470798b16df8dc22de79a1c953f943b41 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sat, 28 Feb 2026 23:38:21 +0000 Subject: [PATCH 06/10] Loading the database into the memory. --- Models/ReactionRoleModels.cs | 13 ++++++ Services/BotService.cs | 2 + Services/DatabaseService.cs | 1 - Services/ReactionRoleService.cs | 81 +++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 Models/ReactionRoleModels.cs create mode 100644 Services/ReactionRoleService.cs diff --git a/Models/ReactionRoleModels.cs b/Models/ReactionRoleModels.cs new file mode 100644 index 0000000..6aa9f5a --- /dev/null +++ b/Models/ReactionRoleModels.cs @@ -0,0 +1,13 @@ +namespace Reactor.Models +{ + public class ReactionMessage + { + public long Id { get; set; } + public long PlanetId { get; set; } + public long ChannelId { get; set; } + public long MessageId { get; set; } + public int DeleteDelaySeconds { get; set; } = 5; + + public Dictionary Reactions { get; set; } = new(); + } +} \ No newline at end of file diff --git a/Services/BotService.cs b/Services/BotService.cs index 8c22ed7..fd6f93c 100644 --- a/Services/BotService.cs +++ b/Services/BotService.cs @@ -30,6 +30,8 @@ namespace Reactor.Services //Initialize the Database await DatabaseService.InitializeAsync(); + await ReactionRoleService.LoadAllAsync(); + Console.WriteLine($"Loaded {ReactionRoleService.Messages.Count} reaction messages into memory."); //Initialize the Planets await PlanetService.InitializePlanetsAsync(client, channelCache, initializedPlanets); diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index ae69190..771d3a9 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -1,4 +1,3 @@ -using System.Linq.Expressions; using Microsoft.Data.Sqlite; namespace Reactor.Services diff --git a/Services/ReactionRoleService.cs b/Services/ReactionRoleService.cs new file mode 100644 index 0000000..4573511 --- /dev/null +++ b/Services/ReactionRoleService.cs @@ -0,0 +1,81 @@ +using Microsoft.Data.Sqlite; +using Reactor.Models; + +namespace Reactor.Services +{ + public static class ReactionRoleService + { + private static readonly string _connectionString = "Data source=reactor.db"; + + //Memory Cache + public static Dictionary Messages { get; private set; } = new(); + + //Load all messages and reaction role mappings + public static async Task LoadAllAsync() + { + Messages.Clear(); + + using var connection = new SqliteConnection(_connectionString); + await connection.OpenAsync(); + + //Load messages + var cmdMsg = connection.CreateCommand(); + cmdMsg.CommandText = "SELECT Id, PlanetId, ChannelId, MessageId, DeleteDelaySeconds FROM ReactionMessages"; + using var readerMsg = await cmdMsg.ExecuteReaderAsync(); + var tempMessages = new Dictionary(); + + while (await readerMsg.ReadAsync()) + { + var msg = new ReactionMessage + { + Id = readerMsg.GetInt64(0), + PlanetId = readerMsg.GetInt64(1), + ChannelId = readerMsg.GetInt64(2), + MessageId = readerMsg.GetInt64(3), + DeleteDelaySeconds = readerMsg.GetInt32(4), + Reactions = new Dictionary() + }; + tempMessages[msg.Id] = msg; + } + + //Load reaction role mappings + var cmdRoles = connection.CreateCommand(); + cmdRoles.CommandText = "SELECT ReactionMessageId, Emoji, RoleId FROM ReactionRoles"; + using var readerRoles = await cmdRoles.ExecuteReaderAsync(); + while (await readerRoles.ReadAsync()) + { + var msgId = readerRoles.GetInt64(0); + var emoji = readerRoles.GetString(1); + var roleId = readerRoles.GetInt64(2); + + if (tempMessages.ContainsKey(msgId)) + { + tempMessages[msgId].Reactions[emoji] = roleId; + } + } + + //Build lookup by MessageId + Messages = tempMessages.Values.ToDictionary(m => m.MessageId, m => m); + } + + public static async Task AddReactionAsync(long messageId, string emoji, long roleId) + { + if (!Messages.TryGetValue(messageId, out var msg)) + return; + + using var connection = new SqliteConnection(_connectionString); + await connection.OpenAsync(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "INSERT INTO ReactionRoles (ReactionMessageId, Emoji, RoleId) VALUES (@msgId, @emoji, @roleId)"; + cmd.Parameters.AddWithValue("@msgId", msg.Id); + cmd.Parameters.AddWithValue("@emoji", emoji); + cmd.Parameters.AddWithValue("@roleId", roleId); + + await cmd.ExecuteNonQueryAsync(); + + //Update Cache + msg.Reactions[emoji] = roleId; + } + } +} \ No newline at end of file From fda66681b694f9e75f22eb9993fbf06417b3b6b4 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sun, 1 Mar 2026 03:39:44 +0000 Subject: [PATCH 07/10] I am a failure :c --- Commands/AddCommand.cs | 52 ++++++++++++++++++ Commands/CreateCommand.cs | 73 ++++++++++++++++++++++++++ Commands/HelpCommand.cs | 1 + Services/DatabaseService.cs | 1 + Services/MessageService.cs | 43 +++++++++++++++ Services/ReactionRoleService.cs | 93 +++++++++++++++++++++++++++++++++ 6 files changed, 263 insertions(+) create mode 100644 Commands/AddCommand.cs create mode 100644 Commands/CreateCommand.cs diff --git a/Commands/AddCommand.cs b/Commands/AddCommand.cs new file mode 100644 index 0000000..f023d49 --- /dev/null +++ b/Commands/AddCommand.cs @@ -0,0 +1,52 @@ +using Reactor.Services; +using Valour.Sdk.Client; +using Valour.Sdk.Models; + +namespace Reactor.Commands +{ + public static class AddCommand + { + public static async Task Execute( + Dictionary channelCache, + long channelId, + long messageId, + string emoji, + long roleId, + ValourClient client, + Planet planet) + { + //Check if the current channel is in the cache (should never happen but you never know!) + if (!channelCache.TryGetValue(channelId, out var channel)) + { + Console.WriteLine($"Channel {channelId} not found in cache."); + return; + } + + //Check if the message id is a valid reaction message + if (!ReactionRoleService.Messages.TryGetValue(messageId, out var reactionMsg)) + { + await channel.SendMessageAsync($"Message ID {messageId} is not tracked as a reaction message."); + return; + } + + //Fetch recent messages + var recentMessages = await channel.GetLastMessagesAsync(50); + + //Try and find the message inside those recent messages + var message = recentMessages.FirstOrDefault(m => m.Id == messageId); + if (message == null) + { + await channel.SendMessageAsync("Could not find the message in the last 50 messages."); + return; + } + + // Add the emoji to the message + await message.AddReactionAsync(emoji); + + //Add reaction-role mapping to DB and Cache + await ReactionRoleService.AddReactionAsync(messageId, emoji, roleId); + + await channel.SendMessageAsync($"Added reaction {emoji} -> role {roleId} for message {messageId}"); + } + } +} \ No newline at end of file diff --git a/Commands/CreateCommand.cs b/Commands/CreateCommand.cs new file mode 100644 index 0000000..fae80e7 --- /dev/null +++ b/Commands/CreateCommand.cs @@ -0,0 +1,73 @@ +using Microsoft.Data.Sqlite; +using Reactor.Services; +using Valour.Sdk.Client; +using Valour.Sdk.Models; + +namespace Reactor.Commands +{ + public static class CreateCommand + { + //Sends a new Reaction Role Message and Stores it + public static async Task Execute( + ValourClient client, + Dictionary channelCache, + long channelId, + string content, + long planetId, + int deleteDelaySeconds = 5) + { + if (!channelCache.TryGetValue(channelId, out var channel)) + { + Console.WriteLine($"Channel {channelId} not found in cache."); + return; + } + + //Send the Message + var result = await channel.SendMessageAsync(content); + if (!result.Success || result.Data == null) + { + Console.WriteLine("Failed to send message."); + return; + } + + var sentMessage = result.Data; + await channel.SendMessageAsync($"This Reaction Message has the ID of: {sentMessage.Id}"); + + //Insert into DB + using var connection = new SqliteConnection("Data Source=reactor.db"); + await connection.OpenAsync(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = @" + INSERT INTO ReactionMessages (PlanetId, ChannelId, MessageId, DeleteDelaySeconds) + VALUES (@planetId, @channelId, @messageId, @delay); + SELECT last_insert_rowid(); + "; + cmd.Parameters.AddWithValue("@planetId", planetId); + cmd.Parameters.AddWithValue("@channelId", channelId); + cmd.Parameters.AddWithValue("@messageId", sentMessage.Id); + cmd.Parameters.AddWithValue("@delay", deleteDelaySeconds); + + var insertedId = (long)await cmd.ExecuteScalarAsync(); + + //Add to memory + ReactionRoleService.Messages[sentMessage.Id] = new Models.ReactionMessage + { + Id = insertedId, + PlanetId = planetId, + ChannelId = channelId, + MessageId = sentMessage.Id, + DeleteDelaySeconds = deleteDelaySeconds, + Reactions = new Dictionary() + }; + + //Subscribe events + sentMessage.ReactionAdded += async () => + { + await ReactionRoleService.HandleReactionAddedAsync(channelCache, sentMessage); + }; + + Console.WriteLine($"Created reaction message {sentMessage.Id} in channel {channelId}"); + } + } +} \ No newline at end of file diff --git a/Commands/HelpCommand.cs b/Commands/HelpCommand.cs index ff4e71a..6d488c8 100644 --- a/Commands/HelpCommand.cs +++ b/Commands/HelpCommand.cs @@ -9,6 +9,7 @@ public static class HelpCommand string helpMessage = $@"**Reactor Commands**: - `{prefix}help` - Shows this list. - `{prefix}source` - Shows my source code! + - `{prefix}create` - Creates the Reaction Message. "; if (channelCache.TryGetValue(channelId, out var channel)) diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 771d3a9..76eee89 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -8,6 +8,7 @@ namespace Reactor.Services public static async Task InitializeAsync() { + //Connection frfr using var connection = new SqliteConnection(_connectionString); await connection.OpenAsync(); diff --git a/Services/MessageService.cs b/Services/MessageService.cs index c56af1a..39c5fbe 100644 --- a/Services/MessageService.cs +++ b/Services/MessageService.cs @@ -12,6 +12,7 @@ namespace Reactor.Services Message message, string prefix) { + //Bot cant reply to its self hahahahahaha loser! if (message.AuthorUserId == client.Me.Id) return; string content = message.Content ?? ""; @@ -31,6 +32,7 @@ namespace Reactor.Services string command = parts[0].ToLower(); string[] args = parts[1..]; + //Commands.. duh.. switch (command) { case "help": @@ -40,6 +42,47 @@ namespace Reactor.Services case "source": await SourceCommand.Execute(channelCache, channelId, memberPing); break; + + case "create": + if (parts.Length < 2) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} Usage: {prefix}create "); + return; + } + + if (message.PlanetId == null) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} Could not detect planet ID for this message. Please contact me if you are seeing this."); + return; + } + + var messageText = string.Join(' ', parts[1..]); + await CreateCommand.Execute(channelCache, channelId, messageText, message.PlanetId.Value); + break; + + case "add": + if (parts.Length < 4) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} Usage: {prefix}add "); + return; + } + + if (!long.TryParse(parts[1], out var msgId)) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} Invalid message ID."); + return; + } + + var emoji = parts[2]; + + if (!long.TryParse(parts[3], out var roleId)) + { + await channelCache[channelId].SendMessageAsync($"{memberPing} Invalid role ID."); + return; + } + + await AddCommand.Execute(channelCache, channelId, msgId, emoji, roleId, client, message.Planet); + break; } } } diff --git a/Services/ReactionRoleService.cs b/Services/ReactionRoleService.cs index 4573511..d0819b2 100644 --- a/Services/ReactionRoleService.cs +++ b/Services/ReactionRoleService.cs @@ -1,5 +1,7 @@ using Microsoft.Data.Sqlite; using Reactor.Models; +using Valour.Sdk.Client; +using Valour.Sdk.Models; namespace Reactor.Services { @@ -77,5 +79,96 @@ namespace Reactor.Services //Update Cache msg.Reactions[emoji] = roleId; } + + public static async Task HandleReactionAddedAsync( + Dictionary channelCache, + Message message) + { + if (!Messages.TryGetValue(message.Id, out var cachedMsg)) + return; + + if (!channelCache.TryGetValue(cachedMsg.ChannelId, out var channel)) + return; + + foreach (var kvp in message.Reactions) + { + string emoji = kvp.Emoji; + if (!cachedMsg.Reactions.TryGetValue(emoji, out var roleId)) + continue; + + //Fetch role name + var role = channel.Planet.Roles.FirstOrDefault(r => r.Id == roleId); + string roleName = role != null ? role.Name : $"Role {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}"); + await Task.Delay(cachedMsg.DeleteDelaySeconds * 1000); + await confirm.Data.DeleteAsync(); + } + } + + // public static async Task HandleReactionAddedAsync( + // ValourClient client, + // Dictionary channelCache, + // MessageReaction reaction) + // { + // if (!Messages.TryGetValue(reaction.MessageId, out var msg)) + // return; + + // if (!msg.Reactions.TryGetValue(reaction.Emoji, out var roleId)) + // return; + + // if (!channelCache.TryGetValue(msg.ChannelId, out var channel)) + // return; + + // var role = channel.Planet.Roles.FirstOrDefault(r => r.Id == roleId); + // string roleName = role != null ? role.Name : $"Role {roleId}"; + + // //Fetch the member + // var member = await channel.Planet.FetchMemberAsync(reaction.AuthorUserId); + // if (member == null) return; + + // //Add role + // await member.AddRoleAsync(roleId); + + // //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(); + // } + + // public static async Task HandleReactionRemovedAsync( + // ValourClient client, + // Dictionary channelCache, + // MessageReaction reaction) + // { + // if (!Messages.TryGetValue(reaction.MessageId, out var msg)) + // return; + + // if (!msg.Reactions.TryGetValue(reaction.Emoji, out var roleId)) + // return; + + // if (!channelCache.TryGetValue(msg.ChannelId, out var channel)) + // return; + + // var role = channel.Planet.Roles.FirstOrDefault(r => r.Id == roleId); + // string roleName = role != null ? role.Name : $"role {roleId}"; + + // var member = await channel.Planet.FetchMemberAsync(reaction.AuthorUserId); + // if (member == null) return; + + // await member.RemoveRoleAsync(roleId); + + // 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(); + // } } } \ No newline at end of file From 33311d63d7309e487471d2879bc2896c3d452cb1 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sun, 1 Mar 2026 03:49:36 +0000 Subject: [PATCH 08/10] failure in life --- Commands/CreateCommand.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Commands/CreateCommand.cs b/Commands/CreateCommand.cs index fae80e7..a8fa1ca 100644 --- a/Commands/CreateCommand.cs +++ b/Commands/CreateCommand.cs @@ -62,10 +62,10 @@ namespace Reactor.Commands }; //Subscribe events - sentMessage.ReactionAdded += async () => - { - await ReactionRoleService.HandleReactionAddedAsync(channelCache, sentMessage); - }; + // sentMessage.ReactionAdded += async () => + // { + // await ReactionRoleService.HandleReactionAddedAsync(channelCache, sentMessage); + // }; Console.WriteLine($"Created reaction message {sentMessage.Id} in channel {channelId}"); } From 8fde4a49fefa9f194d39752f343f13b1fc4a351f Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Wed, 11 Mar 2026 01:01:13 +0000 Subject: [PATCH 09/10] 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 From 8ed587aa5987701a8ed72dd5e20f1087f3c5d2e9 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Wed, 11 Mar 2026 01:23:41 +0000 Subject: [PATCH 10/10] yes --- Services/BotService.cs | 2 ++ Services/PlanetService.cs | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Services/BotService.cs b/Services/BotService.cs index 61d213b..5478b6b 100644 --- a/Services/BotService.cs +++ b/Services/BotService.cs @@ -88,6 +88,8 @@ namespace Reactor.Services } }; + + //Bot is active and ready Console.WriteLine("Bot ready and listening..."); } diff --git a/Services/PlanetService.cs b/Services/PlanetService.cs index 08b1817..360d589 100644 --- a/Services/PlanetService.cs +++ b/Services/PlanetService.cs @@ -1,4 +1,5 @@ using Valour.Sdk.Client; +using Valour.Sdk.ModelLogic; using Valour.Sdk.Models; using Valour.Shared.Models; @@ -20,6 +21,7 @@ namespace Reactor.Services await planet.EnsureReadyAsync(); await planet.FetchInitialDataAsync(); + foreach (var channel in planet.Channels) { @@ -32,6 +34,28 @@ namespace Reactor.Services } } + Action> channelChangedHandler = (evt) => + { + _ = Task.Run(async () => + { + foreach (var channel in planet.Channels) + { + if (channelCache.ContainsKey(channel.Id)) + continue; + + channelCache[channel.Id] = channel; + + if (channel.ChannelType == ChannelTypeEnum.PlanetChat) + { + await channel.OpenWithResult("Reactor"); + Console.WriteLine($"New channel detected: {planet.Name} -> {channel.Name}"); + } + } + }); + }; + + planet.Channels.Changed += channelChangedHandler; + initializedPlanets.Add(planet.Id); } }