v3.0.0 - Initial Commit
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,6 @@
|
||||
.gitignore
|
||||
**/bin/
|
||||
**/obj/
|
||||
**/SkyBot.sln
|
||||
**.sln
|
||||
**/database.db
|
||||
**/Config.cs
|
||||
|
||||
33
SkyBot/Commands/CommandRegistry.cs
Normal file
33
SkyBot/Commands/CommandRegistry.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using SkyBot.Models;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public static class CommandRegistry
|
||||
{
|
||||
public static readonly Dictionary<string, ICommand> Commands = new();
|
||||
public static readonly Dictionary<string, List<ICommand>> Categories = new();
|
||||
|
||||
static CommandRegistry()
|
||||
{
|
||||
var commands = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.Where(t => typeof(ICommand).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
|
||||
.Select(t => (ICommand?)Activator.CreateInstance(t))
|
||||
.Select(c => c!);
|
||||
|
||||
foreach (var cmd in commands)
|
||||
{
|
||||
Commands[cmd.Name.ToLower()] = cmd;
|
||||
foreach (var alias in cmd.Aliases)
|
||||
{
|
||||
Commands[alias.ToLower()] = cmd;
|
||||
}
|
||||
|
||||
Categories = Commands.Values
|
||||
.Distinct()
|
||||
.GroupBy(c => c.Category.ToLower())
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
SkyBot/Commands/CommandTemplate.cs
Normal file
19
SkyBot/Commands/CommandTemplate.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using SkyBot.Models;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public class CommandTemplate : ICommand
|
||||
{
|
||||
public string Name => "template";
|
||||
public string[] Aliases => [];
|
||||
public string Description => "";
|
||||
public string Category => "template";
|
||||
public string Usage => "";
|
||||
public string[] SubCommands => [];
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
if (!ctx.ChannelCache.TryGetValue(ctx.Channel.Id, out var channel)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
SkyBot/Commands/Dev/Delete.cs
Normal file
55
SkyBot/Commands/Dev/Delete.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using SkyBot.Helpers;
|
||||
using SkyBot.Models;
|
||||
using Valour.Shared.Authorization;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public class Delete : ICommand
|
||||
{
|
||||
public string Name => "delete";
|
||||
public string[] Aliases => [];
|
||||
public string Description => "Deletes a message by the bot";
|
||||
public string Category => "Dev";
|
||||
public string Usage => "delete";
|
||||
public string[] SubCommands => [];
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
if (!ctx.ChannelCache.TryGetValue(ctx.Channel.Id, out var channel)) return;
|
||||
|
||||
if (!PermissionHelper.IsOwner(ctx.Member))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "You do not have permission to execute this command.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (ctx.Message.ReplyToId is null)
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Please reply to the message you would like to delete");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.Message.ReplyToId is not long replyToId)
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Please reply to the message you want to delete.");
|
||||
return;
|
||||
}
|
||||
if (!ctx.Client.Cache.Messages.TryGet(replyToId, out var replyMsg))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Could not find the replied message in cache.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (replyMsg!.AuthorUserId != ctx.Client.Me.Id && !await PermissionHelper.HasPermAsync(ctx.Planet.MyMember, [ChatChannelPermissions.ManageMessages], channel))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "I do not have permission to delete other members' messages in this channel.");
|
||||
return;
|
||||
}
|
||||
|
||||
await replyMsg!.DeleteAsync();
|
||||
await ctx.Message.AddReactionAsync("👍");
|
||||
}
|
||||
}
|
||||
}
|
||||
139
SkyBot/Commands/Dev/Planet.cs
Normal file
139
SkyBot/Commands/Dev/Planet.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using SkyBot.Helpers;
|
||||
using SkyBot.Models;
|
||||
using Valour.Sdk.Models;
|
||||
using Valour.Sdk.Models.Messages.Embeds;
|
||||
using Valour.Shared;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public class PlanetCmds : ICommand
|
||||
{
|
||||
public string Name => "planet";
|
||||
public string[] Aliases => [];
|
||||
public string Description => "Planet Commands";
|
||||
public string Category => "Dev";
|
||||
public string Usage => "planet <join|leave|list>";
|
||||
public string[] SubCommands => ["join", "leave", "list"];
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
if (ctx.Message.AuthorUserId != Config.OwnerId) return;
|
||||
|
||||
string sub = ctx.Args.Length > 0 ? ctx.Args[0].ToLower() : "";
|
||||
|
||||
switch (sub)
|
||||
{
|
||||
case "join":
|
||||
await HandleJoin(ctx);
|
||||
break;
|
||||
|
||||
case "leave":
|
||||
await HandleLeave(ctx);
|
||||
break;
|
||||
|
||||
case "list":
|
||||
await HandleList(ctx);
|
||||
break;
|
||||
|
||||
default:
|
||||
await MessageHelper.ReplyAsync(ctx, $"Usage: {Config.Prefix}planet <join|leave|list>");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleJoin(CommandContext ctx)
|
||||
{
|
||||
if (!long.TryParse(ctx.Args.Length > 1 ? ctx.Args[1] : null, out long planetId))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Please provide a valid planet ID.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.Client.PlanetService.JoinedPlanets.Any(p => p.Id == planetId))
|
||||
{
|
||||
Planet planet = await ctx.Client.PlanetService.FetchPlanetAsync(planetId);
|
||||
await MessageHelper.ReplyAsync(ctx, $"Bot is already a member of {planet.Name} (ID: {planet.Id})");
|
||||
return;
|
||||
}
|
||||
|
||||
string? inviteCode = ctx.Args.Length > 2 ? ctx.Args[2] : null;
|
||||
|
||||
TaskResult<PlanetMember> joinResult = inviteCode is null
|
||||
? await ctx.Client.PlanetService.JoinPlanetAsync(planetId)
|
||||
: await ctx.Client.PlanetService.JoinPlanetAsync(planetId, inviteCode);
|
||||
|
||||
if (!joinResult.Success)
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, $"Failed to join planet: {joinResult.Message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Planet planet = await ctx.Client.PlanetService.FetchPlanetAsync(planetId);
|
||||
await MessageHelper.ReplyAsync(ctx, $"Successfully joined planet: {planet.Name} (ID {planet.Id})");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleLeave(CommandContext ctx)
|
||||
{
|
||||
if (!long.TryParse(ctx.Args.Length > 1 ? ctx.Args[1] : ctx.Planet.Id.ToString(), out long planetId))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Please provide a valid planet ID or no planet ID to leave this planet.");
|
||||
return;
|
||||
}
|
||||
|
||||
Planet planet = await ctx.Client.PlanetService.FetchPlanetAsync(planetId);
|
||||
|
||||
await MessageHelper.ReplyAsync(ctx, $"Are you sure you want to leave planet: {planet.Name}? Type `{Config.Prefix}confirm within 30 seconds to confirm.");
|
||||
bool confirmed = await PendingConfirmations.WaitAsync(ctx.Member.UserId, TimeSpan.FromSeconds(30));
|
||||
|
||||
if (!confirmed)
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Confirmation timed out.");
|
||||
return;
|
||||
}
|
||||
|
||||
await MessageHelper.ReplyAsync(ctx, $"Leaving planet: {planet.Name}...");
|
||||
await ctx.Client.PlanetService.LeavePlanetAsync(planet);
|
||||
}
|
||||
|
||||
private static async Task HandleList(CommandContext ctx)
|
||||
{
|
||||
const int PageSize = 10;
|
||||
|
||||
var planets = ctx.Client.PlanetService.JoinedPlanets.ToList();
|
||||
|
||||
var chunks = planets
|
||||
.Select((p, i) => (p, i))
|
||||
.GroupBy(x => x.i / PageSize)
|
||||
.Select(g => g.Select(x => x.p).ToList())
|
||||
.ToList();
|
||||
|
||||
if (chunks.Count == 0)
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Bot is not a member of any planets.");
|
||||
return;
|
||||
}
|
||||
|
||||
var builder = new EmbedBuilder();
|
||||
builder.embed.HideChangePageArrows = true;
|
||||
|
||||
for (int chunkIndex = 0; chunkIndex < chunks.Count; chunkIndex++)
|
||||
{
|
||||
int embedPage = chunkIndex;
|
||||
string? footer = chunks.Count > 1 ? $"Page {chunkIndex + 1}/{chunks.Count}" : null;
|
||||
|
||||
builder.AddPage($"Planets ({planets.Count} total)", footer);
|
||||
|
||||
foreach (var planet in chunks[chunkIndex])
|
||||
builder.AddText(planet.Name, $"ID: {planet.Id}");
|
||||
|
||||
if (chunkIndex > 0)
|
||||
builder.AddButton("← Prev").OnClickGoToEmbedPage(embedPage - 1);
|
||||
if (chunkIndex < chunks.Count - 1)
|
||||
builder.AddButton("Next →").OnClickGoToEmbedPage(embedPage + 1);
|
||||
}
|
||||
|
||||
await MessageHelper.ReplyAsync(ctx, null, builder.embed);
|
||||
}
|
||||
};
|
||||
};
|
||||
58
SkyBot/Commands/Dev/React.cs
Normal file
58
SkyBot/Commands/Dev/React.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using SkyBot.Helpers;
|
||||
using SkyBot.Models;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public class React : ICommand
|
||||
{
|
||||
public string Name => "react";
|
||||
public string[] Aliases => [];
|
||||
public string Description => "Adds a reaction to a replied message.";
|
||||
public string Category => "Dev";
|
||||
public string Usage => "dev <emoji> [amount]";
|
||||
public string[] SubCommands => [];
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
if (!ctx.ChannelCache.TryGetValue(ctx.Channel.Id, out var channel)) return;
|
||||
|
||||
if (!PermissionHelper.IsOwner(ctx.Member))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "You do not have permission to execute this command.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.Args.Length < 1)
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, $"Usage: `{Config.Prefix}react <emoji> [amount]");
|
||||
return;
|
||||
}
|
||||
|
||||
string emoji = ctx.Args[0];
|
||||
|
||||
int amount = 1;
|
||||
if (ctx.Args.Length >= 2 && !int.TryParse(ctx.Args[1], out amount))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, $"`{ctx.Args[1]}` is not a valid number. Defaulting to `1`");
|
||||
amount = 1;
|
||||
}
|
||||
|
||||
if (ctx.Message.ReplyToId is not long replyToId)
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Please reply to the message you want to add the reaction to.");
|
||||
return;
|
||||
}
|
||||
if (!ctx.Client.Cache.Messages.TryGet(replyToId, out var replyMsg))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Could not find the replied message in cache.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
await replyMsg!.AddReactionAsync(emoji);
|
||||
}
|
||||
await ctx.Message.AddReactionAsync("👍");
|
||||
}
|
||||
}
|
||||
}
|
||||
46
SkyBot/Commands/Fun/Echo.cs
Normal file
46
SkyBot/Commands/Fun/Echo.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Concurrent;
|
||||
using SkyBot.Helpers;
|
||||
using SkyBot.Models;
|
||||
using Valour.Sdk.Models;
|
||||
using Valour.Shared;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public class Echo : ICommand
|
||||
{
|
||||
public string Name => "echo";
|
||||
public string[] Aliases => ["say"];
|
||||
public string Description => "Echos your message as the bot";
|
||||
public string Category => "Fun";
|
||||
public string Usage => "echo <text>";
|
||||
public string[] SubCommands => [];
|
||||
|
||||
public static readonly ConcurrentDictionary<long, long> EchoMap = new();
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
if (!ctx.ChannelCache.TryGetValue(ctx.Channel.Id, out var channel)) return;
|
||||
|
||||
string reply = string.Join(' ', ctx.Args);
|
||||
if (string.IsNullOrWhiteSpace(reply))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, "Please enter a message to echo.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply.Length > 2048)
|
||||
{
|
||||
reply = reply.Substring(0, 2048);
|
||||
}
|
||||
|
||||
TaskResult<Message> echoedMsg = await MessageHelper.ReplyAsync(ctx, reply);
|
||||
|
||||
if (echoedMsg.Success && echoedMsg.Data is not null)
|
||||
{
|
||||
EchoMap[ctx.Message.Id] = echoedMsg.Data.Id;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
160
SkyBot/Commands/Info/Help.cs
Normal file
160
SkyBot/Commands/Info/Help.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using SkyBot.Helpers;
|
||||
using SkyBot.Models;
|
||||
using Valour.Sdk.Models.Messages.Embeds;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public class Help : ICommand
|
||||
{
|
||||
public string Name => "help";
|
||||
public string[] Aliases => ["cmds"];
|
||||
public string Description => "Shows all commands by category.";
|
||||
public string Category => "Info";
|
||||
public string Usage => "help";
|
||||
public string[] SubCommands => [];
|
||||
|
||||
private const int PageSize = 10;
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
if (!ctx.ChannelCache.TryGetValue(ctx.Channel.Id, out var channel)) return;
|
||||
|
||||
var categories = CommandRegistry.Categories
|
||||
.Where(c => c.Key != "template")
|
||||
.Where(c => c.Key != "dev" || PermissionHelper.IsOwner(ctx.Member))
|
||||
.OrderBy(c => c.Key)
|
||||
.ToList();
|
||||
|
||||
var categoryFirstPage = new Dictionary<string, int>();
|
||||
var allChunks = new List<(string CategoryName, List<ICommand> Cmds, int ChunkIndex, int TotalChunks)>();
|
||||
|
||||
int nextPageIndex = 1;
|
||||
foreach (var (categoryName, cmds) in categories)
|
||||
{
|
||||
var ordered = cmds.OrderBy(c => c.Name).ToList();
|
||||
var chunks = ordered
|
||||
.Select((cmd, i) => (cmd, i))
|
||||
.GroupBy(x => x.i / PageSize)
|
||||
.Select(g => g.Select(x => x.cmd).ToList())
|
||||
.ToList();
|
||||
|
||||
categoryFirstPage[categoryName] = nextPageIndex;
|
||||
for (int c = 0; c < chunks.Count; c++)
|
||||
allChunks.Add((categoryName, chunks[c], c, chunks.Count));
|
||||
|
||||
nextPageIndex += chunks.Count;
|
||||
}
|
||||
|
||||
var allCmds = allChunks
|
||||
.SelectMany(chunk => chunk.Cmds)
|
||||
.DistinctBy(cmd => cmd.Name)
|
||||
.ToList();
|
||||
|
||||
var cmdDetailPage = new Dictionary<string, int>();
|
||||
int detailPageIndex = nextPageIndex;
|
||||
foreach (var cmd in allCmds)
|
||||
cmdDetailPage[cmd.Name] = detailPageIndex++;
|
||||
|
||||
var builder = new EmbedBuilder();
|
||||
builder.embed.HideChangePageArrows = true;
|
||||
|
||||
// Home page
|
||||
builder.AddPage("✦ Help Menu", $"Prefix: {Config.Prefix}");
|
||||
builder.AddRow()
|
||||
.AddText("Select a Category")
|
||||
.WithStyles(EmbedStyles.LabelText)
|
||||
.CloseRow();
|
||||
|
||||
builder.AddRow();
|
||||
foreach (var (categoryName, cmds) in categories)
|
||||
{
|
||||
builder.AddButton($"{categoryName.ToTitleCase()} ({cmds.Count})")
|
||||
.WithStyles(EmbedStyles.CategoryBtn)
|
||||
.OnClickGoToEmbedPage(categoryFirstPage[categoryName]);
|
||||
}
|
||||
builder.CloseRow();
|
||||
|
||||
// Category pages
|
||||
int embedPage = 1;
|
||||
foreach (var (categoryName, cmds, chunkIndex, totalChunks) in allChunks)
|
||||
{
|
||||
string? footer = totalChunks > 1
|
||||
? $"Page {chunkIndex + 1}/{totalChunks} | Prefix: {Config.Prefix}"
|
||||
: $"Prefix: {Config.Prefix}";
|
||||
|
||||
builder.AddPage($"✦ {categoryName.ToTitleCase()} Commands", footer);
|
||||
|
||||
foreach (var cmd in cmds)
|
||||
{
|
||||
builder.AddRow()
|
||||
.AddButton(cmd.Name)
|
||||
.WithStyles(EmbedStyles.CommandBtn)
|
||||
.OnClickGoToEmbedPage(cmdDetailPage[cmd.Name])
|
||||
.CloseRow();
|
||||
}
|
||||
|
||||
builder.AddRow()
|
||||
.AddButton("← Back")
|
||||
.WithStyles(EmbedStyles.BackBtn)
|
||||
.OnClickGoToEmbedPage(0);
|
||||
|
||||
if (chunkIndex > 0)
|
||||
{
|
||||
builder.AddButton("← Prev")
|
||||
.WithStyles(EmbedStyles.NavBtn)
|
||||
.OnClickGoToEmbedPage(embedPage - 1);
|
||||
}
|
||||
|
||||
if (chunkIndex < totalChunks - 1)
|
||||
{
|
||||
builder.AddButton("Next →")
|
||||
.WithStyles(EmbedStyles.NavBtn)
|
||||
.OnClickGoToEmbedPage(embedPage + 1);
|
||||
}
|
||||
|
||||
builder.CloseRow();
|
||||
|
||||
embedPage++;
|
||||
}
|
||||
|
||||
// Command detail pages
|
||||
foreach (var cmd in allCmds)
|
||||
{
|
||||
builder.AddPage($"✦ {cmd.Name.ToTitleCase()}", $"Prefix: {Config.Prefix}");
|
||||
|
||||
builder.AddRow()
|
||||
.AddText("Description", cmd.Description)
|
||||
.CloseRow();
|
||||
|
||||
if (cmd.Aliases.Length > 0)
|
||||
{
|
||||
builder.AddRow()
|
||||
.AddText("Aliases", string.Join(", ", cmd.Aliases))
|
||||
.CloseRow();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cmd.Usage))
|
||||
{
|
||||
builder.AddRow()
|
||||
.AddText("Usage", $"{Config.Prefix}{cmd.Usage}")
|
||||
.CloseRow();
|
||||
}
|
||||
|
||||
if (cmd.SubCommands.Length > 0)
|
||||
{
|
||||
builder.AddRow()
|
||||
.AddText("Sub-commands", string.Join(", ", cmd.SubCommands.Select(s => s.ToTitleCase())))
|
||||
.CloseRow();
|
||||
}
|
||||
|
||||
builder.AddRow()
|
||||
.AddButton("← Back")
|
||||
.WithStyles(EmbedStyles.BackBtn)
|
||||
.OnClickGoToEmbedPage(categoryFirstPage[cmd.Category.ToLower()])
|
||||
.CloseRow();
|
||||
}
|
||||
|
||||
await MessageHelper.ReplyAsync(ctx, null, builder.embed);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
SkyBot/Helpers/EmbedStyles.cs
Normal file
51
SkyBot/Helpers/EmbedStyles.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Valour.Sdk.Models.Messages.Embeds.Styles;
|
||||
using Valour.Sdk.Models.Messages.Embeds.Styles.Basic;
|
||||
|
||||
namespace SkyBot.Helpers
|
||||
{
|
||||
public static class EmbedStyles
|
||||
{
|
||||
private static readonly Size Radius = new(Unit.Pixels, 8);
|
||||
private static readonly Size PadV = new(Unit.Pixels, 2);
|
||||
private static readonly Size PadH = new(Unit.Pixels, 6);
|
||||
private static readonly Size FitContent = new(Unit.FitContent);
|
||||
|
||||
public static StyleBase[] LabelText => [
|
||||
new TextColor("#a0a0b8"),
|
||||
new FontWeight(600),
|
||||
];
|
||||
|
||||
public static StyleBase[] CategoryBtn => [
|
||||
new BackgroundColor("#5865F2"),
|
||||
new TextColor("#ffffff"),
|
||||
new BorderRadius(Radius),
|
||||
new Padding(left: PadH, right: PadH, top: PadV, bottom: PadV),
|
||||
new FontWeight(600),
|
||||
];
|
||||
|
||||
public static StyleBase[] CommandBtn => [
|
||||
new BackgroundColor("#2b2d3e"),
|
||||
new TextColor("#e0e0f0"),
|
||||
new BorderRadius(Radius),
|
||||
new Padding(left: PadH, right: PadH, top: PadV, bottom: PadV),
|
||||
new Width(FitContent),
|
||||
];
|
||||
|
||||
public static StyleBase[] NavBtn => [
|
||||
new BackgroundColor("#1e1f2e"),
|
||||
new TextColor("#a0a0b8"),
|
||||
new BorderRadius(Radius),
|
||||
new Padding(left: PadH, right: PadH, top: PadV, bottom: PadV),
|
||||
new Width(FitContent),
|
||||
];
|
||||
|
||||
public static StyleBase[] BackBtn => [
|
||||
new BackgroundColor("#5865F2"),
|
||||
new TextColor("#ffffff"),
|
||||
new BorderRadius(Radius),
|
||||
new Padding(left: PadH, right: PadH, top: PadV, bottom: PadV),
|
||||
new FontWeight(600),
|
||||
new Width(FitContent),
|
||||
];
|
||||
}
|
||||
}
|
||||
64
SkyBot/Helpers/MessageHelper.cs
Normal file
64
SkyBot/Helpers/MessageHelper.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using SkyBot.Models;
|
||||
using Valour.Sdk.Client;
|
||||
using Valour.Sdk.Models;
|
||||
using Valour.Sdk.Models.Messages.Embeds;
|
||||
using Valour.Shared;
|
||||
|
||||
namespace SkyBot.Helpers
|
||||
{
|
||||
public static class MessageHelper
|
||||
{
|
||||
public static string Mention(this PlanetMember member) => $"«@m-{member.Id}»";
|
||||
public static string Mention(this User user) => $"«@u-{user.Id}»";
|
||||
public static string ToTitleCase(this string str) => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str);
|
||||
|
||||
public static async Task<TaskResult<Message>> ReplyAsync(CommandContext ctx, string? content, Embed? embed = null, bool reply = false)
|
||||
{
|
||||
long? replyToId;
|
||||
|
||||
if (reply)
|
||||
{
|
||||
replyToId = ctx.Message.ReplyToId;
|
||||
}
|
||||
else
|
||||
{
|
||||
replyToId = ctx.Message.Id;
|
||||
}
|
||||
|
||||
string? embedData;
|
||||
if (embed is not null)
|
||||
{
|
||||
embedData = JsonSerializer.Serialize(embed);
|
||||
}
|
||||
else
|
||||
{
|
||||
embedData = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Message msg = new Message(ctx.Client)
|
||||
{
|
||||
Content = content,
|
||||
EmbedData = embedData,
|
||||
ChannelId = ctx.Channel.Id,
|
||||
PlanetId = ctx.Planet.Id,
|
||||
AuthorUserId = ctx.Client.Me.Id,
|
||||
AuthorMemberId = ctx.Channel.Planet?.MyMember.Id,
|
||||
ReplyToId = replyToId,
|
||||
Fingerprint = Guid.NewGuid().ToString()
|
||||
};
|
||||
return await ctx.Client.MessageService.SendMessage(msg);
|
||||
}
|
||||
|
||||
|
||||
public static async Task<TaskResult<Message>> EditAsync(Channel channel, Message message, string content)
|
||||
{
|
||||
message.Content = content;
|
||||
return await channel.Planet.Node.PutAsyncWithResponse<Message>($"api/messages/{message.Id}", message);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
29
SkyBot/Helpers/PendingConfirmations.cs
Normal file
29
SkyBot/Helpers/PendingConfirmations.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace SkyBot.Helpers
|
||||
{
|
||||
public static class PendingConfirmations
|
||||
{
|
||||
private static readonly ConcurrentDictionary<long, TaskCompletionSource<bool>> pending = new();
|
||||
public static bool IsPending(long userId) => pending.ContainsKey(userId);
|
||||
|
||||
public static Task<bool> WaitAsync(long userId, TimeSpan timeout)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
pending[userId] = tcs;
|
||||
_ = Task.Delay(timeout).ContinueWith(_ => tcs.TrySetResult(false));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public static bool TryComplete(long userId, bool confirmed)
|
||||
{
|
||||
if (pending.TryRemove(userId, out var tcs))
|
||||
{
|
||||
return tcs.TrySetResult(confirmed);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
42
SkyBot/Helpers/PermissionHelper.cs
Normal file
42
SkyBot/Helpers/PermissionHelper.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Valour.Sdk.Models;
|
||||
using Valour.Shared.Authorization;
|
||||
|
||||
namespace SkyBot.Helpers
|
||||
{
|
||||
public static class PermissionHelper
|
||||
{
|
||||
public static async Task<bool> HasPermAsync(PlanetMember member, Permission[] permissions, Channel? channel = null, bool requireAll = false)
|
||||
{
|
||||
if (member is null) return false;
|
||||
if (member.HasPermission(PlanetPermissions.FullControl)) return true;
|
||||
if (member.Roles.Any(r => r.IsAdmin)) return true;
|
||||
|
||||
var results = new List<bool>();
|
||||
|
||||
foreach (var perm in permissions)
|
||||
{
|
||||
bool result = perm switch
|
||||
{
|
||||
PlanetPermission p => member.HasPermission(p),
|
||||
ChatChannelPermission p => channel is not null
|
||||
? await channel.HasPermissionAsync(member, p)
|
||||
: throw new ArgumentNullException(nameof(channel), $"Channel is required for ChatChannelPermission '{p.Name}'"),
|
||||
VoiceChannelPermission p => channel is not null
|
||||
? await channel.HasPermissionAsync(member, p)
|
||||
: throw new ArgumentNullException(nameof(channel), $"Channel is required for VoiceChannelPermission '{p.Name}'"),
|
||||
_ => throw new ArgumentException($"Unsupported permission type: {perm.GetType().Name}")
|
||||
};
|
||||
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
return requireAll ? results.All(r => r) : results.Any(r => r);
|
||||
}
|
||||
|
||||
public static bool IsOwner(PlanetMember member)
|
||||
{
|
||||
if (member is null) return false;
|
||||
return member.UserId == Config.OwnerId;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
SkyBot/Models/CommandContext.cs
Normal file
17
SkyBot/Models/CommandContext.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Valour.Sdk.Client;
|
||||
using Valour.Sdk.Models;
|
||||
|
||||
namespace SkyBot.Models
|
||||
{
|
||||
public class CommandContext
|
||||
{
|
||||
public required ValourClient Client { get; set; }
|
||||
public required ConcurrentDictionary<long, Channel> ChannelCache { get; set; }
|
||||
public required PlanetMember Member { get; set; }
|
||||
public required Message Message { get; set; }
|
||||
public required Planet Planet { get; set; }
|
||||
public required Channel Channel { get; set; }
|
||||
public required string[] Args { get; set; }
|
||||
};
|
||||
};
|
||||
13
SkyBot/Models/ICommand.cs
Normal file
13
SkyBot/Models/ICommand.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace SkyBot.Models
|
||||
{
|
||||
public interface ICommand
|
||||
{
|
||||
string Name { get; }
|
||||
string[] Aliases { get; }
|
||||
string Description { get; }
|
||||
string Category { get; }
|
||||
string Usage { get; }
|
||||
string[] SubCommands { get; }
|
||||
Task Execute(CommandContext ctx);
|
||||
}
|
||||
}
|
||||
32
SkyBot/Services/BotService.cs
Normal file
32
SkyBot/Services/BotService.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Concurrent;
|
||||
using DotNetEnv;
|
||||
using Valour.Sdk.Client;
|
||||
using Valour.Sdk.Models;
|
||||
|
||||
namespace SkyBot.Services
|
||||
{
|
||||
public static class BotService
|
||||
{
|
||||
public static async Task InitialiseBotAsync(
|
||||
ValourClient client,
|
||||
ConcurrentDictionary<long, Channel> channelCache,
|
||||
ConcurrentDictionary<long, bool> initialisedPlanets
|
||||
)
|
||||
{
|
||||
Env.Load();
|
||||
var token = Environment.GetEnvironmentVariable("TOKEN");
|
||||
if (string.IsNullOrWhiteSpace(token)) {Console.WriteLine($"TOKEN not set in .env"); return;}
|
||||
|
||||
var loginResult = await client.InitializeUser(token);
|
||||
if (!loginResult.Success) {Console.WriteLine($"Login Failed: {loginResult.Message}"); return;}
|
||||
Console.WriteLine($"Logged in as {client.Me.NameAndTag} (ID: {client.Me.Id})");
|
||||
|
||||
await PlanetService.InitialisePlanetsAsync(client, channelCache, initialisedPlanets);
|
||||
client.PlanetService.JoinedPlanetsUpdated += async () => { await PlanetService.InitialisePlanetsAsync(client, channelCache, initialisedPlanets); };
|
||||
|
||||
client.MessageService.MessageReceived += async message => { await MessageService.Create(client, channelCache, message); };
|
||||
client.MessageService.MessageDeleted += async message => { await MessageService.Delete(client, message); };
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
47
SkyBot/Services/ChannelService.cs
Normal file
47
SkyBot/Services/ChannelService.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Valour.Sdk.Models;
|
||||
using Valour.Shared.Models;
|
||||
|
||||
namespace SkyBot.Services
|
||||
{
|
||||
public static class ChannelService
|
||||
{
|
||||
public static async Task InitialiseChannelsAsync(
|
||||
ConcurrentDictionary<long, Channel> channelCache,
|
||||
Planet planet
|
||||
)
|
||||
{
|
||||
foreach (var channel in planet.Channels)
|
||||
{
|
||||
channelCache[channel.Id] = channel;
|
||||
}
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
foreach (var channel in planet.Channels.Where(c => c.ChannelType == ChannelTypeEnum.PlanetChat))
|
||||
{
|
||||
try
|
||||
{
|
||||
await channel.OpenWithResult("SkyBot");
|
||||
Console.WriteLine($"Realtime opened for: {planet.Name} (ID: {planet.Id}) -> {channel.Name} (ID: {channel.Id})");
|
||||
} catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error opening realtime for {channel.Id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"All channels opened for {planet.Name}");
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task InitialiseChannelAsync(
|
||||
ConcurrentDictionary<long, Channel> channelCache,
|
||||
Channel channel
|
||||
)
|
||||
{
|
||||
channelCache[channel.Id] = channel;
|
||||
await channel.OpenWithResult("SkyBot");
|
||||
Console.WriteLine($"Realtime opened for: {channel.Planet.Name} (ID: {channel.Planet.Id}) -> {channel.Name} (ID: {channel.Id})");
|
||||
}
|
||||
}
|
||||
}
|
||||
86
SkyBot/Services/MessageService.cs
Normal file
86
SkyBot/Services/MessageService.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.Collections.Concurrent;
|
||||
using SkyBot.Commands;
|
||||
using SkyBot.Helpers;
|
||||
using SkyBot.Models;
|
||||
using Valour.Sdk.Client;
|
||||
using Valour.Sdk.Models;
|
||||
|
||||
namespace SkyBot.Services
|
||||
{
|
||||
public static class MessageService
|
||||
{
|
||||
private static readonly ConcurrentDictionary<long, DateTime> _cooldowns = new();
|
||||
private static readonly TimeSpan _cooldown = TimeSpan.FromSeconds(2);
|
||||
public static async Task Create(
|
||||
ValourClient client,
|
||||
ConcurrentDictionary<long, Channel> channelCache,
|
||||
Message message
|
||||
)
|
||||
{
|
||||
if (message.AuthorUserId == client.Me.Id) return;
|
||||
|
||||
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;
|
||||
|
||||
string[] parts = content.Substring(prefix.Length).Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length == 0) return;
|
||||
|
||||
string command = parts[0].ToLower();
|
||||
string[] args = parts[1..];
|
||||
|
||||
CommandContext ctx = new CommandContext
|
||||
{
|
||||
Client = client,
|
||||
ChannelCache = channelCache,
|
||||
Member = member,
|
||||
Message = message,
|
||||
Planet = message.Planet,
|
||||
Channel = channel,
|
||||
Args = args
|
||||
};
|
||||
|
||||
|
||||
if (PendingConfirmations.IsPending(member.UserId))
|
||||
{
|
||||
PendingConfirmations.TryComplete(member.UserId, content.Trim().ToLower() == $"{prefix}confirm");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cooldowns.TryGetValue(member.Id, out var lastUsed) && DateTime.UtcNow - lastUsed < _cooldown) return;
|
||||
|
||||
_cooldowns[member.Id] = DateTime.UtcNow;
|
||||
|
||||
if (CommandRegistry.Commands.TryGetValue(command, out var handler))
|
||||
{
|
||||
await handler.Execute(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, $"Unknown command `{command}`.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static async Task Delete(
|
||||
ValourClient client,
|
||||
Message message
|
||||
)
|
||||
{
|
||||
if (Echo.EchoMap.TryRemove(message.Id, out var echoId))
|
||||
{
|
||||
if (client.Cache.Messages.TryGet(echoId, out var echoMsg))
|
||||
{
|
||||
await echoMsg!.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
SkyBot/Services/PlanetService.cs
Normal file
37
SkyBot/Services/PlanetService.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Valour.Sdk.Client;
|
||||
using Valour.Sdk.ModelLogic;
|
||||
using Valour.Sdk.Models;
|
||||
|
||||
namespace SkyBot.Services
|
||||
{
|
||||
public static class PlanetService
|
||||
{
|
||||
public static async Task InitialisePlanetsAsync(
|
||||
ValourClient client,
|
||||
ConcurrentDictionary<long, Channel> channelCache,
|
||||
ConcurrentDictionary<long, bool> initialisedPlanets
|
||||
)
|
||||
{
|
||||
foreach (var planet in client.PlanetService.JoinedPlanets.Where(p => !initialisedPlanets.ContainsKey(p.Id)))
|
||||
{
|
||||
Console.WriteLine($"Initialising Planet: {planet.Name}");
|
||||
|
||||
await planet.EnsureReadyAsync();
|
||||
await planet.FetchInitialDataAsync();
|
||||
await ChannelService.InitialiseChannelsAsync(channelCache, planet);
|
||||
|
||||
planet.Channels.Changed += async channelEvent =>
|
||||
{
|
||||
if (channelEvent is ModelAddedEvent<Channel> addedEvent)
|
||||
{
|
||||
await ChannelService.InitialiseChannelAsync(channelCache, addedEvent.Model);
|
||||
}
|
||||
await ChannelService.InitialiseChannelsAsync(channelCache, planet);
|
||||
};
|
||||
|
||||
initialisedPlanets.TryAdd(planet.Id, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
29
SkyBot/SkyBot.cs
Normal file
29
SkyBot/SkyBot.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Concurrent;
|
||||
using SkyBot.Services;
|
||||
using Valour.Sdk.Client;
|
||||
using Valour.Sdk.Models;
|
||||
|
||||
namespace SkyBot
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
private static readonly ValourClient client = new("https://api.valour.gg/");
|
||||
private static readonly ConcurrentDictionary<long, Channel> channelCache = new();
|
||||
private static readonly ConcurrentDictionary<long, bool> initialisedPlanets = new();
|
||||
|
||||
public static async Task Main()
|
||||
{
|
||||
client.SetupHttpClient();
|
||||
try
|
||||
{
|
||||
await BotService.InitialiseBotAsync(client, channelCache, initialisedPlanets);
|
||||
|
||||
Console.WriteLine("Ready and Listening...");
|
||||
await Task.Delay(Timeout.Infinite);
|
||||
} catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Fatal Error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
SkyBot/SkyBot.csproj
Normal file
16
SkyBot/SkyBot.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.3.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||
<PackageReference Include="Valour.Sdk" Version="0.5.21" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user