0.3.4.0 - Blacklist system, Ping/Uptime commands & fixes

This commit is contained in:
2026-05-06 23:58:03 +01:00
parent 51fbd0524e
commit 9f5131346f
12 changed files with 248 additions and 13 deletions

View File

@@ -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 | | `help` | `cmds` | Shows all commands organised by category with an interactive embed |
| `info <planet\|user> [@user]` | — | Shows detailed info about the current planet or a user | | `info <planet\|user> [@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 | | `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) | | `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 | | Command | Aliases | Description |
|---|---|---| |---|---|---|
| `blacklist <add\|remove> <@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. | | `delete` | — | Deletes a replied-to message. Deletes bot messages directly; requires `ManageMessages` for other members' messages. Reply to a message to use. |
| `planet <join\|leave\|list>` | — | Manage the planets the bot is a member of | | `planet <join\|leave\|list>` | — | Manage the planets the bot is a member of |
| `react <emoji> [amount]` | — | Add a reaction to a replied-to message a given number of times | | `react <emoji> [amount]` | — | Add a reaction to a replied-to message a given number of times |

View File

@@ -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: 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 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 ### Information Never Stored
@@ -41,6 +42,7 @@ Temporarily held information is used exclusively to:
2. Enable core bot functionality during the current session 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 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) 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. 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 ## 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. The Bot does not sell, rent, trade, or otherwise share any data with third parties.

View File

@@ -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 ### Command Categories
- **Fun** — echo and similar utilities - **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 - **Moderation** — kick and ban with reason and duration support
- **RP** — emotes (35 actions via nekos.best) and a marriage system - **RP** — emotes (35 actions via nekos.best) and a marriage system
- **Utils** — accept, decline, confirm, cancel for pending actions - **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) Full command list: [COMMANDS.md](COMMANDS.md)
@@ -38,13 +34,13 @@ Full command list: [COMMANDS.md](COMMANDS.md)
## Data & Privacy ## 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: SkyBot does **not** store:
- Message content - Message content
- Direct messages - 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) Full privacy policy: [PRIVACY.md](PRIVACY.md)

View File

@@ -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 <add|remove> <@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 <add|remove> <@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;
}
}
}
}

View File

@@ -9,7 +9,7 @@ namespace SkyBot.Commands
public string[] Aliases => []; public string[] Aliases => [];
public string Description => "Adds a reaction to a replied message."; public string Description => "Adds a reaction to a replied message.";
public string Category => "Dev"; public string Category => "Dev";
public string Usage => "dev <emoji> [amount]"; public string Usage => "react <emoji> [amount]";
public string[] SubCommands => []; public string[] SubCommands => [];
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)

View File

@@ -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> message = await MessageHelper.ReplyAsync(ctx, "🏓 Pinging...");
double elapsed = (DateTime.UtcNow - start).TotalMilliseconds;
await MessageHelper.EditAsync(ctx.Channel, message.Data, $"🏓 Ping! `{elapsed:F0}ms`");
}
}
}

View File

@@ -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}");
}
}
}

View File

@@ -0,0 +1,71 @@
using System.Collections.Concurrent;
using Microsoft.Data.Sqlite;
namespace SkyBot.Services
{
public static class BlacklistService
{
private static readonly ConcurrentDictionary<long, bool> 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<BlacklistResult> 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<UnBlacklistResult> 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;
}
}
}

View File

@@ -30,6 +30,18 @@ namespace SkyBot.Services
await cmd.ExecuteNonQueryAsync(); 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."); Console.WriteLine("Database initialised.");
} }
} }

View File

@@ -21,11 +21,12 @@ namespace SkyBot.Services
string prefix = Config.Prefix; string prefix = Config.Prefix;
string content = message.Content; string content = message.Content;
PlanetMember member = await message.FetchAuthorMemberAsync();
if (!content.ToLower().StartsWith(prefix)) return; if (!content.ToLower().StartsWith(prefix)) return;
if (!channelCache.TryGetValue(message.ChannelId, out var channel)) return; if (!channelCache.TryGetValue(message.ChannelId, out var channel)) return;
if (string.IsNullOrWhiteSpace(content)) return; if (string.IsNullOrWhiteSpace(content)) return;
if (BlacklistService.GetBlacklisted(message.AuthorUserId)) return;
string[] parts = content.Substring(prefix.Length).Split(' ', StringSplitOptions.RemoveEmptyEntries); string[] parts = content.Substring(prefix.Length).Split(' ', StringSplitOptions.RemoveEmptyEntries);
@@ -34,6 +35,8 @@ namespace SkyBot.Services
string command = parts[0].ToLower(); string command = parts[0].ToLower();
string[] args = parts[1..]; string[] args = parts[1..];
PlanetMember member = await message.FetchAuthorMemberAsync();
CommandContext ctx = new CommandContext CommandContext ctx = new CommandContext
{ {
Client = client, Client = client,

View File

@@ -10,14 +10,17 @@ namespace SkyBot
private static readonly ValourClient client = new("https://api.valour.gg/"); private static readonly ValourClient client = new("https://api.valour.gg/");
private static readonly ConcurrentDictionary<long, Channel> channelCache = new(); private static readonly ConcurrentDictionary<long, Channel> channelCache = new();
private static readonly ConcurrentDictionary<long, bool> initialisedPlanets = new(); private static readonly ConcurrentDictionary<long, bool> initialisedPlanets = new();
public static DateTime StartTime;
public static async Task Main() public static async Task Main()
{ {
client.SetupHttpClient(); client.SetupHttpClient();
try try
{ {
StartTime = DateTime.UtcNow;
await DatabaseService.InitialiseAsync(); await DatabaseService.InitialiseAsync();
await MarriageService.InitialiseAsync(); await MarriageService.InitialiseAsync();
await BlacklistService.InitialiseAsync();
await BotService.InitialiseBotAsync(client, channelCache, initialisedPlanets); await BotService.InitialiseBotAsync(client, channelCache, initialisedPlanets);

View File

@@ -5,7 +5,7 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.3.3.2</Version> <Version>0.3.4.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>