0.3.5.0 - Added few commands & Channel restrictions

This commit is contained in:
2026-05-07 22:57:20 +01:00
parent 9f5131346f
commit 7e5ad5ece2
11 changed files with 299 additions and 6 deletions

View File

@@ -8,7 +8,9 @@ All commands are prefixed with your configured prefix (e.g. `sd/`). Arguments in
| Command | Aliases | Description |
|---|---|---|
| `8ball <question>` | — | Ask the magic 8ball a question. Replies with a random response after a short delay. |
| `echo <text>` | `say` | Repeat text through the bot. If the command message is deleted, the echoed message is also deleted. |
| `mock <text\|reply>` | — | Mocks text by alternating upper and lower case. Provide text or reply to a message. |
---
@@ -31,8 +33,9 @@ All commands are prefixed with your configured prefix (e.g. `sd/`). Arguments in
| Command | Aliases | Description |
|---|---|---|
| `kick <@member\|id> [reason]` | — | Kicks a member from the planet. Sends them a DM embed before kicking. |
| `ban <@member\|id> [duration] [reason]` | — | Bans a member from the planet. Duration is optional (e.g. `7d`, `2h`, `30m`). Permanent if omitted. |
| `kick <@member\|id> [reason]` | — | Kicks a member from the planet. Sends them a DM embed before kicking. |
| `restrict <disable\|enable\|list> <category> [all\|command]` | `cr`, `channelrestrict` | Disable or enable command categories or individual commands in the current channel. Requires Manage Channel permission. |
---

View File

@@ -25,6 +25,7 @@ The following data is written to a local SQLite database (`database.db`) and is
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
3. Channel restrictions — channel IDs and the categories or commands disabled within them
### Information Never Stored
@@ -50,7 +51,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 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.
In-memory data is automatically cleared when the Bot restarts. Marriage records, blacklist entries, and channel restrictions 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.

View File

@@ -21,9 +21,9 @@ SkyBot is a Valour.gg bot built with .NET 10.
### Command Categories
- **Fun** — echo and similar utilities
- **Fun** — echo, 8ball, mock and similar utilities
- **Info** — bot info, ping, uptime, command listing, planet/user info
- **Moderation** — kick and ban with reason and duration support
- **Moderation** — kick, ban, and per-channel command restrictions
- **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
@@ -34,7 +34,7 @@ Full command list: [COMMANDS.md](COMMANDS.md)
## Data & Privacy
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 stores only the minimum data required for operation. Marriage records, blacklist entries, and channel restrictions are persisted to a local SQLite database. All other data is stored in-memory and is lost on restart.
SkyBot does **not** store:

View File

@@ -0,0 +1,56 @@
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared;
namespace SkyBot.Commands
{
public class EightBall : ICommand
{
public string Name => "8ball";
public string[] Aliases => [];
public string Description => "Ask the magic 8ball a question.";
public string Category => "Fun";
public string Usage => "8ball <question>";
public string[] SubCommands => [];
private static readonly string[] Responses =
[
"It is certain.",
"It is decidedly so.",
"Without a doubt.",
"Yes, definitely.",
"You may rely on it.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
"Reply hazy, try again.",
"Ask again later.",
"Better not tell you now.",
"Cannot predict now.",
"Concentrate and ask again.",
"Don't count on it.",
"My reply is no.",
"My sources say no.",
"Outlook not so good.",
"Very doubtful."
];
public async Task Execute(CommandContext ctx)
{
if (ctx.Args.Length == 0)
{
await MessageHelper.ReplyAsync(ctx, "Please ask a question.");
return;
}
TaskResult<Message> result = await MessageHelper.ReplyAsync(ctx, $"🎱 Thinking...", reply: true);
await ctx.Channel.SendIsTyping();
await Task.Delay(2000);
string response = Responses[Random.Shared.Next(Responses.Length)];
await MessageHelper.EditAsync(ctx.Channel, result.Data, $"🎱 {response}");
}
}
}

View File

@@ -0,0 +1,44 @@
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Mock : ICommand
{
public string Name => "mock";
public string[] Aliases => [];
public string Description => "Mocks the text a user has sent or entered text";
public string Category => "Fun";
public string Usage => "mock <text|reply>";
public string[] SubCommands => [];
public async Task Execute(CommandContext ctx)
{
string text;
if (ctx.Message.ReplyToId.HasValue)
{
var replyMessage = await ctx.Message.FetchReplyMessageAsync();
text = replyMessage?.Content ?? "";
} else
{
if (ctx.Args.Length == 0)
{
await MessageHelper.ReplyAsync(ctx, "Please provide some text to mock or reply to a message.");
return;
}
text = string.Join(" ", ctx.Args);
}
if (string.IsNullOrWhiteSpace(text))
{
await MessageHelper.ReplyAsync(ctx, "No text to mock.");
return;
}
string mocked = new string(text.Select((c, i) => i % 2 == 0 ? char.ToLower(c) : char.ToUpper(c)).ToArray());
await MessageHelper.ReplyAsync(ctx, mocked);
}
}
}

View File

@@ -0,0 +1,99 @@
using SkyBot.Helpers;
using SkyBot.Models;
using SkyBot.Services;
using Valour.Shared.Authorization;
namespace SkyBot.Commands
{
public class Restrict : ICommand
{
public string Name => "restrict";
public string[] Aliases => ["channelrestrict", "cr"];
public string Description => "Enable or disable command categories or specific commands in this channel.";
public string Category => "Mod";
public string Usage => "restrict <disable|enable|list> <category> <all|commandname>";
public string[] SubCommands => ["disable", "enable", "list"];
public async Task Execute(CommandContext ctx)
{
if (!PermissionHelper.IsOwner(ctx.Member) && !await PermissionHelper.HasPermAsync(ctx.Member, [ChatChannelPermissions.ManageChannel], ctx.Channel))
{
await MessageHelper.ReplyAsync(ctx, "You need the **Manage Channel** permission to use this command.");
return;
}
string sub = ctx.Args.ElementAtOrDefault(0)?.ToLower() ?? "";
if (sub == "list")
{
var restrictions = ChannelRestrictionService.GetRestrictions(ctx.Channel.Id);
if (restrictions.Count == 0)
{
await MessageHelper.ReplyAsync(ctx, "No restrictions set for this channel.");
return;
}
var lines = restrictions.Select(k =>
k.StartsWith("cat:") ? $"Category: **{k[4..].ToTitleCase()}**" :
k.StartsWith("cmd:") ? $"Command: `{k[4..]}`" : k);
await MessageHelper.ReplyAsync(ctx, $"Restrictions in this channel:\n{string.Join("\n", lines)}");
return;
}
if (sub is not ("disable" or "enable"))
{
await MessageHelper.ReplyAsync(ctx, $"Usage: `{Config.Prefix}{Usage}`");
return;
}
string? category = ctx.Args.ElementAtOrDefault(1)?.ToLower();
string? target = ctx.Args.ElementAtOrDefault(2)?.ToLower();
if (category is null)
{
await MessageHelper.ReplyAsync(ctx, $"Please specify a category. Available: {string.Join(", ", CommandRegistry.Categories.Keys)}");
return;
}
if (!CommandRegistry.Categories.ContainsKey(category))
{
await MessageHelper.ReplyAsync(ctx, $"Unknown category `{category}`. Available: {string.Join(", ", CommandRegistry.Categories.Keys)}");
return;
}
bool disable = sub == "disable";
if (target is null or "all")
{
string key = ChannelRestrictionService.CategoryKey(category);
bool changed = disable
? await ChannelRestrictionService.DisableAsync(ctx.Channel.Id, key)
: await ChannelRestrictionService.EnableAsync(ctx.Channel.Id, key);
if (!changed)
await MessageHelper.ReplyAsync(ctx, disable ? $"**{category.ToTitleCase()}** commands are already disabled here." : $"**{category.ToTitleCase()}** commands are already enabled here.");
else
await MessageHelper.ReplyAsync(ctx, disable ? $"Disabled all **{category.ToTitleCase()}** commands in this channel." : $"Enabled all **{category.ToTitleCase()}** commands in this channel.");
}
else
{
if (!CommandRegistry.Commands.ContainsKey(target))
{
await MessageHelper.ReplyAsync(ctx, $"Unknown command `{target}`.");
return;
}
string key = ChannelRestrictionService.CommandKey(target);
bool changed = disable
? await ChannelRestrictionService.DisableAsync(ctx.Channel.Id, key)
: await ChannelRestrictionService.EnableAsync(ctx.Channel.Id, key);
if (!changed)
await MessageHelper.ReplyAsync(ctx, disable ? $"`{target}` is already disabled here." : $"`{target}` is already enabled here.");
else
await MessageHelper.ReplyAsync(ctx, disable ? $"Disabled `{target}` in this channel." : $"Enabled `{target}` in this channel.");
}
}
}
}

View File

@@ -0,0 +1,72 @@
using System.Collections.Concurrent;
using Microsoft.Data.Sqlite;
namespace SkyBot.Services
{
public static class ChannelRestrictionService
{
// channelId -> set of disabled keys ("cat:rp" or "cmd:emote")
private static readonly ConcurrentDictionary<long, HashSet<string>> _restrictions = new();
public static async Task InitialiseAsync()
{
using SqliteConnection connection = DatabaseService.GetConnection();
using SqliteCommand cmd = connection.CreateCommand();
cmd.CommandText = "SELECT ChannelId, DisabledKey FROM ChannelRestrictions";
using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
long channelId = (long)reader["ChannelId"];
string key = (string)reader["DisabledKey"];
_restrictions.GetOrAdd(channelId, _ => new HashSet<string>()).Add(key);
}
Console.WriteLine($"ChannelRestrictionService initialised. Loaded {_restrictions.Values.Sum(s => s.Count)} restrictions");
}
public static bool IsRestricted(long channelId, Models.ICommand command)
{
if (!_restrictions.TryGetValue(channelId, out var keys)) return false;
return keys.Contains(CategoryKey(command.Category)) || keys.Contains(CommandKey(command.Name));
}
public static async Task<bool> DisableAsync(long channelId, string key)
{
var set = _restrictions.GetOrAdd(channelId, _ => new HashSet<string>());
lock (set)
{
if (!set.Add(key)) return false;
}
using SqliteConnection connection = DatabaseService.GetConnection();
using SqliteCommand cmd = connection.CreateCommand();
cmd.CommandText = "INSERT OR IGNORE INTO ChannelRestrictions (ChannelId, DisabledKey) VALUES ($c, $k)";
cmd.Parameters.AddWithValue("$c", channelId);
cmd.Parameters.AddWithValue("$k", key);
await cmd.ExecuteNonQueryAsync();
return true;
}
public static async Task<bool> EnableAsync(long channelId, string key)
{
if (!_restrictions.TryGetValue(channelId, out var set)) return false;
lock (set)
{
if (!set.Remove(key)) return false;
}
using SqliteConnection connection = DatabaseService.GetConnection();
using SqliteCommand cmd = connection.CreateCommand();
cmd.CommandText = "DELETE FROM ChannelRestrictions WHERE ChannelId = $c AND DisabledKey = $k";
cmd.Parameters.AddWithValue("$c", channelId);
cmd.Parameters.AddWithValue("$k", key);
await cmd.ExecuteNonQueryAsync();
return true;
}
public static HashSet<string> GetRestrictions(long channelId) =>
_restrictions.TryGetValue(channelId, out var set) ? set : new HashSet<string>();
public static string CategoryKey(string category) => $"cat:{category.ToLower()}";
public static string CommandKey(string command) => $"cmd:{command.ToLower()}";
}
}

View File

@@ -42,6 +42,18 @@ namespace SkyBot.Services
await cmd.ExecuteNonQueryAsync();
}
using (SqliteCommand cmd = connection.CreateCommand())
{
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS ChannelRestrictions (
ChannelId INTEGER NOT NULL,
DisabledKey TEXT NOT NULL,
PRIMARY KEY (ChannelId, DisabledKey)
);
";
await cmd.ExecuteNonQueryAsync();
}
Console.WriteLine("Database initialised.");
}
}

View File

@@ -61,6 +61,11 @@ namespace SkyBot.Services
if (CommandRegistry.Commands.TryGetValue(command, out var handler))
{
if (!PermissionHelper.IsOwner(member) && ChannelRestrictionService.IsRestricted(message.ChannelId, handler))
{
await MessageHelper.ReplyAsync(ctx, $"`{command}` is disabled in this channel.");
return;
}
await handler.Execute(ctx);
}
else

View File

@@ -21,6 +21,7 @@ namespace SkyBot
await DatabaseService.InitialiseAsync();
await MarriageService.InitialiseAsync();
await BlacklistService.InitialiseAsync();
await ChannelRestrictionService.InitialiseAsync();
await BotService.InitialiseBotAsync(client, channelCache, initialisedPlanets);

View File

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