0.3.5.0 - Added few commands & Channel restrictions
This commit is contained in:
@@ -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. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
56
SkyBot/Commands/Fun/8Ball.cs
Normal file
56
SkyBot/Commands/Fun/8Ball.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
44
SkyBot/Commands/Fun/Mock.cs
Normal file
44
SkyBot/Commands/Fun/Mock.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
99
SkyBot/Commands/Mods/Restrict.cs
Normal file
99
SkyBot/Commands/Mods/Restrict.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
SkyBot/Services/ChannelRestrictionService.cs
Normal file
72
SkyBot/Services/ChannelRestrictionService.cs
Normal 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()}";
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace SkyBot
|
||||
await DatabaseService.InitialiseAsync();
|
||||
await MarriageService.InitialiseAsync();
|
||||
await BlacklistService.InitialiseAsync();
|
||||
await ChannelRestrictionService.InitialiseAsync();
|
||||
await BotService.InitialiseBotAsync(client, channelCache, initialisedPlanets);
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user