From 9f5131346fcd0e6d52deb93286633a0f10329705 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Wed, 6 May 2026 23:58:03 +0100 Subject: [PATCH] 0.3.4.0 - Blacklist system, Ping/Uptime commands & fixes --- COMMANDS.md | 3 + PRIVACY.md | 4 +- README.md | 12 ++-- SkyBot/Commands/Dev/Blacklist.cs | 91 +++++++++++++++++++++++++++++ SkyBot/Commands/Dev/React.cs | 2 +- SkyBot/Commands/Info/Ping.cs | 26 +++++++++ SkyBot/Commands/Info/Uptime.cs | 28 +++++++++ SkyBot/Services/BlacklistService.cs | 71 ++++++++++++++++++++++ SkyBot/Services/DatabaseService.cs | 12 ++++ SkyBot/Services/MessageService.cs | 5 +- SkyBot/SkyBot.cs | 5 +- SkyBot/SkyBot.csproj | 2 +- 12 files changed, 248 insertions(+), 13 deletions(-) create mode 100644 SkyBot/Commands/Dev/Blacklist.cs create mode 100644 SkyBot/Commands/Info/Ping.cs create mode 100644 SkyBot/Commands/Info/Uptime.cs create mode 100644 SkyBot/Services/BlacklistService.cs diff --git a/COMMANDS.md b/COMMANDS.md index d7aeb1a..548a1c2 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -18,7 +18,9 @@ All commands are prefixed with your configured prefix (e.g. `sd/`). Arguments in |---|---|---| | `help` | `cmds` | Shows all commands organised by category with an interactive embed | | `info [@user]` | — | Shows detailed info about the current planet or a user | +| `ping` | — | Shows the bot's response time | | `source` | `src` | Link to the bot's source code | +| `uptime` | — | Shows how long the bot has been running | | `version` | `ver` | Shows the bot version, Valour server version, and Valour SDK version (current and latest) | --- @@ -75,6 +77,7 @@ All commands are prefixed with your configured prefix (e.g. `sd/`). Arguments in | Command | Aliases | Description | |---|---|---| +| `blacklist <@user\|userid>` | — | Add or remove a user from the bot blacklist | | `delete` | — | Deletes a replied-to message. Deletes bot messages directly; requires `ManageMessages` for other members' messages. Reply to a message to use. | | `planet ` | — | Manage the planets the bot is a member of | | `react [amount]` | — | Add a reaction to a replied-to message a given number of times | diff --git a/PRIVACY.md b/PRIVACY.md index 204f491..1c39aac 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -24,6 +24,7 @@ The Bot collects only the minimum data necessary to provide its intended functio The following data is written to a local SQLite database (`database.db`) and is retained across restarts: 1. Marriage records — the Valour user IDs of both partners and the timestamp the marriage was created +2. Blacklist entries — the Valour user ID of each blacklisted user ### Information Never Stored @@ -41,6 +42,7 @@ Temporarily held information is used exclusively to: 2. Enable core bot functionality during the current session 3. Track echo message pairs to support automatic cleanup when the original command message is deleted 4. Operate the marriage system (tracking active marriages and pending proposals/divorces) +5. Enforce the blacklist (preventing blacklisted users from using bot commands) The Bot does not use any information for profiling, marketing, analytics, or tracking purposes. @@ -48,7 +50,7 @@ The Bot does not use any information for profiling, marketing, analytics, or tra ## 3. Data Storage and Security -In-memory data is automatically cleared when the Bot restarts. Marriage records are written to a local SQLite database file (`database.db`) on the machine running the bot. No data is sent to any external storage or cloud service. +In-memory data is automatically cleared when the Bot restarts. Marriage records and blacklist entries are written to a local SQLite database file (`database.db`) on the machine running the bot. No data is sent to any external storage or cloud service. The Bot does not sell, rent, trade, or otherwise share any data with third parties. diff --git a/README.md b/README.md index bf41518..5d0138c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,3 @@ -Currently in the process of remaking the bot, for current official bot information goto: [v2 branch](../v2/) - - - - @@ -27,10 +22,11 @@ SkyBot is a Valour.gg bot built with .NET 10. ### Command Categories - **Fun** — echo and similar utilities -- **Info** — bot info, command listing, planet/user info +- **Info** — bot info, ping, uptime, command listing, planet/user info - **Moderation** — kick and ban with reason and duration support - **RP** — emotes (35 actions via nekos.best) and a marriage system - **Utils** — accept, decline, confirm, cancel for pending actions +- **Dev** — owner-only tools including blacklist management, planet control, and message utilities Full command list: [COMMANDS.md](COMMANDS.md) @@ -38,13 +34,13 @@ Full command list: [COMMANDS.md](COMMANDS.md) ## Data & Privacy -SkyBot stores only the minimum data required for operation. Marriage data is persisted to a local SQLite database. All other data is stored in-memory and is lost on restart. +SkyBot stores only the minimum data required for operation. Marriage records and blacklist entries are persisted to a local SQLite database. All other data is stored in-memory and is lost on restart. SkyBot does **not** store: - Message content - Direct messages -- Personal user data beyond what is needed for the marriage system +- Personal user data beyond what is needed for the marriage and blacklist systems Full privacy policy: [PRIVACY.md](PRIVACY.md) diff --git a/SkyBot/Commands/Dev/Blacklist.cs b/SkyBot/Commands/Dev/Blacklist.cs new file mode 100644 index 0000000..5c651ea --- /dev/null +++ b/SkyBot/Commands/Dev/Blacklist.cs @@ -0,0 +1,91 @@ +using System.Runtime.Serialization; +using SkyBot.Helpers; +using SkyBot.Models; +using SkyBot.Services; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class Blacklist : ICommand + { + public string Name => "blacklist"; + public string[] Aliases => []; + public string Description => "adds or removes a user from the blacklist"; + public string Category => "Dev"; + public string Usage => "blacklist <@user|userid>"; + public string[] SubCommands => ["add", "remove"]; + + public async Task Execute(CommandContext ctx) + { + if (!PermissionHelper.IsOwner(ctx.Member)) + { + await MessageHelper.ReplyAsync(ctx, "You do not have permission to execute this command."); + return; + } + + long? userId = null; + + if (ctx.Message.Mentions?.Any() == true) + { + var member = await ctx.Planet.FetchMemberAsync(ctx.Message.Mentions.First().TargetId); + if (member is null) { await MessageHelper.ReplyAsync(ctx, "Could not find that member."); return; } + userId = member.UserId; + } + else if (long.TryParse(ctx.Args.ElementAtOrDefault(1), out long parsed)) + { + var member = await ctx.Planet.FetchMemberAsync(parsed); + userId = member?.UserId ?? parsed; + } + + if (userId is null) + { + await MessageHelper.ReplyAsync(ctx, "Mention a user or provide their user ID."); + return; + } + + User user = await ctx.Client.UserService.FetchUserAsync(userId.Value); + if (user is null) { await MessageHelper.ReplyAsync(ctx, "Could not find that user."); return; } + + switch (ctx.Args[0]) + { + case "add": + await handleAdd(ctx, user); + break; + case "remove": + await handleRemove(ctx, user); + break; + default: + await MessageHelper.ReplyAsync(ctx, "Usage: blacklist <@user|userid>"); + break; + } + } + + private static async Task handleAdd(CommandContext ctx, User user) + { + var result = await BlacklistService.Blacklist(user.Id); + switch (result) + { + case BlacklistService.BlacklistResult.AlreadyBlacklisted: + await MessageHelper.ReplyAsync(ctx, "User is already blacklisted"); + break; + case BlacklistService.BlacklistResult.Ok: + await MessageHelper.ReplyAsync(ctx, $"Blacklisted {user.Name} successfully."); + break; + } + } + + private static async Task handleRemove(CommandContext ctx, User user) + { + var result = await BlacklistService.UnBlacklist(user.Id); + switch (result) + { + case BlacklistService.UnBlacklistResult.NotBlacklisted: + await MessageHelper.ReplyAsync(ctx, "User is not blacklisted."); + break; + case BlacklistService.UnBlacklistResult.Ok: + await MessageHelper.ReplyAsync(ctx, $"Removed {user.Name} from the blacklist."); + break; + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Dev/React.cs b/SkyBot/Commands/Dev/React.cs index dae8f1f..082dc83 100644 --- a/SkyBot/Commands/Dev/React.cs +++ b/SkyBot/Commands/Dev/React.cs @@ -9,7 +9,7 @@ namespace SkyBot.Commands public string[] Aliases => []; public string Description => "Adds a reaction to a replied message."; public string Category => "Dev"; - public string Usage => "dev [amount]"; + public string Usage => "react [amount]"; public string[] SubCommands => []; public async Task Execute(CommandContext ctx) diff --git a/SkyBot/Commands/Info/Ping.cs b/SkyBot/Commands/Info/Ping.cs new file mode 100644 index 0000000..80e77a4 --- /dev/null +++ b/SkyBot/Commands/Info/Ping.cs @@ -0,0 +1,26 @@ +using SkyBot.Helpers; +using SkyBot.Models; +using Valour.Sdk.Models; +using Valour.Shared; + +namespace SkyBot.Commands +{ + public class Ping : ICommand + { + public string Name => "ping"; + public string[] Aliases => []; + public string Description => "Displays the bots response time."; + public string Category => "Info"; + public string Usage => "ping"; + public string[] SubCommands => []; + + public async Task Execute(CommandContext ctx) + { + DateTime start = DateTime.UtcNow; + TaskResult message = await MessageHelper.ReplyAsync(ctx, "🏓 Pinging..."); + double elapsed = (DateTime.UtcNow - start).TotalMilliseconds; + + await MessageHelper.EditAsync(ctx.Channel, message.Data, $"🏓 Ping! `{elapsed:F0}ms`"); + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Info/Uptime.cs b/SkyBot/Commands/Info/Uptime.cs new file mode 100644 index 0000000..ceb4486 --- /dev/null +++ b/SkyBot/Commands/Info/Uptime.cs @@ -0,0 +1,28 @@ +using SkyBot.Helpers; +using SkyBot.Models; + +namespace SkyBot.Commands +{ + public class Uptime : ICommand + { + public string Name => "uptime"; + public string[] Aliases => []; + public string Description => "Displays the uptime of the bot"; + public string Category => "Info"; + public string Usage => "uptime"; + public string[] SubCommands => []; + + public async Task Execute(CommandContext ctx) + { + TimeSpan uptime = DateTime.UtcNow - Program.StartTime; + string formatted = uptime.TotalDays >= 1 + ? $"{(int)uptime.TotalDays}d {uptime.Hours}h {uptime.Minutes}m {uptime.Seconds}s" + : uptime.TotalHours >= 1 + ? $"{uptime.Hours}h {uptime.Minutes}m {uptime.Seconds}s" + : uptime.TotalMinutes >= 1 + ? $"{uptime.Minutes}m {uptime.Seconds}s" + : $"{uptime.Seconds}s"; + await MessageHelper.ReplyAsync(ctx, $"⏱️ SkyBot Uptime: {formatted}"); + } + } +} \ No newline at end of file diff --git a/SkyBot/Services/BlacklistService.cs b/SkyBot/Services/BlacklistService.cs new file mode 100644 index 0000000..b642eac --- /dev/null +++ b/SkyBot/Services/BlacklistService.cs @@ -0,0 +1,71 @@ +using System.Collections.Concurrent; +using Microsoft.Data.Sqlite; + +namespace SkyBot.Services +{ + public static class BlacklistService + { + private static readonly ConcurrentDictionary blacklist = new(); + public static async Task InitialiseAsync() + { + using SqliteConnection connection = DatabaseService.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT UserId FROM Blacklist"; + using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); + while ( await reader.ReadAsync()) + { + long userId = (long)reader["UserId"]; + blacklist[userId] = true; + } + Console.WriteLine($"BlacklistService initialised. Loaded {blacklist.Count} blacklisted users"); + } + + public static bool GetBlacklisted(long userId) + { + return blacklist.TryGetValue(userId, out var blacklisted) ? blacklisted : false; + } + + public enum BlacklistResult + { + Ok, + AlreadyBlacklisted + } + + public static async Task Blacklist(long userId) + { + if (GetBlacklisted(userId)) return BlacklistResult.AlreadyBlacklisted; + + using SqliteConnection connection = DatabaseService.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = "INSERT OR IGNORE INTO Blacklist (UserId, Bool) VALUES ($u, $b)"; + cmd.Parameters.AddWithValue("$u", userId); + cmd.Parameters.AddWithValue("$b", true); + await cmd.ExecuteNonQueryAsync(); + + blacklist[userId] = true; + + return BlacklistResult.Ok; + } + + public enum UnBlacklistResult + { + Ok, + NotBlacklisted + } + + public static async Task UnBlacklist(long userId) + { + if (!GetBlacklisted(userId)) return UnBlacklistResult.NotBlacklisted; + + using SqliteConnection connection = DatabaseService.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = "DELETE FROM Blacklist WHERE UserId = $u"; + cmd.Parameters.AddWithValue("$u", userId); + await cmd.ExecuteNonQueryAsync(); + + blacklist.TryRemove(userId, out _); + + return UnBlacklistResult.Ok; + } + } +} \ No newline at end of file diff --git a/SkyBot/Services/DatabaseService.cs b/SkyBot/Services/DatabaseService.cs index a1af32e..e194490 100644 --- a/SkyBot/Services/DatabaseService.cs +++ b/SkyBot/Services/DatabaseService.cs @@ -30,6 +30,18 @@ namespace SkyBot.Services await cmd.ExecuteNonQueryAsync(); } + using (SqliteCommand cmd = connection.CreateCommand()) + { + cmd.CommandText = @" + CREATE TABLE IF NOT EXISTS Blacklist ( + UserId INTEGER NOT NULL, + Bool BOOLEAN NOT NULL, + PRIMARY KEY (UserId, Bool) + ); + "; + await cmd.ExecuteNonQueryAsync(); + } + Console.WriteLine("Database initialised."); } } diff --git a/SkyBot/Services/MessageService.cs b/SkyBot/Services/MessageService.cs index 9688ee1..998499b 100644 --- a/SkyBot/Services/MessageService.cs +++ b/SkyBot/Services/MessageService.cs @@ -21,11 +21,12 @@ namespace SkyBot.Services string prefix = Config.Prefix; string content = message.Content; - PlanetMember member = await message.FetchAuthorMemberAsync(); if (!content.ToLower().StartsWith(prefix)) return; if (!channelCache.TryGetValue(message.ChannelId, out var channel)) return; if (string.IsNullOrWhiteSpace(content)) return; + + if (BlacklistService.GetBlacklisted(message.AuthorUserId)) return; string[] parts = content.Substring(prefix.Length).Split(' ', StringSplitOptions.RemoveEmptyEntries); @@ -34,6 +35,8 @@ namespace SkyBot.Services string command = parts[0].ToLower(); string[] args = parts[1..]; + PlanetMember member = await message.FetchAuthorMemberAsync(); + CommandContext ctx = new CommandContext { Client = client, diff --git a/SkyBot/SkyBot.cs b/SkyBot/SkyBot.cs index 593e329..61dadc9 100644 --- a/SkyBot/SkyBot.cs +++ b/SkyBot/SkyBot.cs @@ -10,14 +10,17 @@ namespace SkyBot private static readonly ValourClient client = new("https://api.valour.gg/"); private static readonly ConcurrentDictionary channelCache = new(); private static readonly ConcurrentDictionary initialisedPlanets = new(); - + public static DateTime StartTime; + public static async Task Main() { client.SetupHttpClient(); try { + StartTime = DateTime.UtcNow; await DatabaseService.InitialiseAsync(); await MarriageService.InitialiseAsync(); + await BlacklistService.InitialiseAsync(); await BotService.InitialiseBotAsync(client, channelCache, initialisedPlanets); diff --git a/SkyBot/SkyBot.csproj b/SkyBot/SkyBot.csproj index 351a360..daa48be 100644 --- a/SkyBot/SkyBot.csproj +++ b/SkyBot/SkyBot.csproj @@ -5,7 +5,7 @@ net10.0 enable enable - 0.3.3.2 + 0.3.4.0