big boy update fr

big boy update
This commit is contained in:
SkyJoshua
2026-03-16 13:43:06 +00:00
committed by GitHub
31 changed files with 965 additions and 86 deletions

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;
@@ -16,14 +17,9 @@ namespace SkyBot.Commands
{ {
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache; ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId; long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = $"";
if (channelCache.TryGetValue(channelId, out var channel)) if (!channelCache.TryGetValue(channelId, out var channel)) return;
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
} }
} }
} }

View File

@@ -3,29 +3,45 @@ using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Client; using Valour.Sdk.Client;
using Valour.Sdk.Models; using Valour.Sdk.Models;
using Valour.Shared;
namespace SkyBot.Commands namespace SkyBot.Commands
{ {
public class Test : ICommand public class Test : ICommand
{ {
public string Name => "test"; public string Name => "edit";
public string[] Aliases => []; public string[] Aliases => [];
public string Description => "Just a test command"; public string Description => "Edit the bots message";
public string Section => "Dev"; public string Section => "Dev";
public string Usage => "test"; public string Usage => "reply -> edit <message>";
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)
{ {
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache; ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
ValourClient client = ctx.Client; ValourClient client = ctx.Client;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member; PlanetMember member = ctx.Member;
Message message = ctx.Message; Message message = ctx.Message;
Planet planet = ctx.Planet; string[] args = ctx.Args;
if (channelCache.TryGetValue(channelId, out var channel)) if (channelCache.TryGetValue(channelId, out var channel))
{ {
await MessageHelper.ReplyAsync(ctx, channel, "This is a test message"); if(!PermissionHelper.IsOwner(member))
{
await MessageHelper.ReplyAsync(ctx, channel, "This is a Dev only command.");
}
if (message.ReplyToId == null)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please reply to a message.");
return;
}
if (client.Cache.Messages.TryGet(message.ReplyToId.Value, out var msg))
{
await MessageHelper.EditAsync(channel, msg, string.Join(" ", args));
return;
}
} }
} }
} }

View File

@@ -0,0 +1,64 @@
using System.Collections.Concurrent;
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 Section => "Fun";
public string Usage => "8ball <question>";
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)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
string[] args = ctx.Args;
if (channelCache.TryGetValue(channelId, out var channel))
{
if (args.Length == 0)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please ask a question.");
return;
}
TaskResult<Message> result = await MessageHelper.ReplyAsync(ctx, channel, $"🎱 Thinking...");
await channel.SendIsTyping();
await Task.Delay(2000);
string response = Responses[Random.Shared.Next(Responses.Length)];
await MessageHelper.EditAsync(channel, result.Data, $"🎱 {response}");
}
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared;
namespace SkyBot.Commands
{
public class Choose : ICommand
{
public string Name => "choose";
public string[] Aliases => ["pick"];
public string Description => "Picks one of the given options.";
public string Section => "Fun";
public string Usage => "choose <option1> <option2> ...";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
string[] args = ctx.Args;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
if (ctx.Args.Length < 2)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please provide at least two options.");
return;
}
TaskResult<Message> result = await MessageHelper.ReplyAsync(ctx, channel, "🤔 Choosing...");
await Task.Delay(1000);
string choice = args[Random.Shared.Next(args.Length)];
await MessageHelper.EditAsync(channel, result.Data, $"I choose **{choice}**!");
}
}
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared;
namespace SkyBot.Commands
{
public class CoinFlip : ICommand
{
public string Name => "coinflip";
public string[] Aliases => ["cf"];
public string Description => "Flips a coin.";
public string Section => "Fun";
public string Usage => "coinflip";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
TaskResult<Message> result = await MessageHelper.ReplyAsync(ctx, channel, "🪙 Flipping...");
await channel.SendIsTyping();
await Task.Delay(3000);
string outcome = Random.Shared.Next(2) == 0 ? "Heads" : "Tails";
await MessageHelper.EditAsync(channel, result.Data, $"🪙 {outcome}");
}
}
}

View File

@@ -0,0 +1,60 @@
using System.Collections.Concurrent;
using System.Runtime.InteropServices.Marshalling;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared;
namespace SkyBot.Commands
{
public class Dice : ICommand
{
public string Name => "dice";
public string[] Aliases => ["roll"];
public string Description => "Rolls dice.";
public string Section => "Fun";
public string Usage => "roll <dice> (e.g. 2d6, d20, 3d8)";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string[] args = ctx.Args;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
string input = args.Length > 0 ? args[0].ToLower() : "1d6";
string[] parts = input.Split('d');
if (parts.Length != 2 || !int.TryParse(parts[1], out int sides) || sides < 2)
{
await MessageHelper.ReplyAsync(ctx, channel, "Invalid dice format. Use something like `2d6` or `d20`.");
return;
}
int count = 1;
if (!string.IsNullOrWhiteSpace(parts[0]) && !int.TryParse(parts[0], out count))
{
await MessageHelper.ReplyAsync(ctx, channel, "Invalid dice format. Use something like `2d6` or `d20`.");
return;
}
if (count < 1 || count > 100)
{
await MessageHelper.ReplyAsync(ctx, channel, "You can only roll between 1 and 100 dice at a time.");
return;
}
IEnumerable<int> rolls = Enumerable.Range(0, count).Select(_ => Random.Shared.Next(1, sides+1)).ToList();
int total = rolls.Sum();
TaskResult<Message> rolling = await MessageHelper.ReplyAsync(ctx, channel, "🎲 Rolling...");
await channel.SendIsTyping();
await Task.Delay(2000);
string rollDisplay = count > 1 ? $"({string.Join(" + ", rolls)}) = **{total}**" : $"**{total}**";
await MessageHelper.EditAsync(channel, rolling.Data, $"🎲 Rolled {input}: {rollDisplay}");
}
}
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Net.NetworkInformation;
using SkyBot.Helpers; using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;
@@ -19,24 +18,21 @@ namespace SkyBot.Commands
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache; ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId; long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member; PlanetMember member = ctx.Member;
String[] args = ctx.Args; string[] args = ctx.Args;
Message message = ctx.Message; Message message = ctx.Message;
string reply = string.Join(" ", args); string reply = string.Join(" ", args);
if (channelCache.TryGetValue(channelId, out var channel)) if (!channelCache.TryGetValue(channelId, out var channel)) return;
if (string.IsNullOrWhiteSpace(reply)) await MessageHelper.ReplyAsync(ctx, channel, $"Enter a message to echo.");
reply = $"{member.Name} » {reply}";
if (reply.Length > 2048)
{ {
if (string.IsNullOrWhiteSpace(reply)) await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Enter a message to echo."); reply = reply.Substring(0, 2048);
reply = $"{member.Name} » {reply}";
if (reply.Length > 2048)
{
reply = reply.Substring(0, 2048);
}
await MessageHelper.ReplyAsync(ctx, channel, reply);
} }
await MessageHelper.ReplyAsync(ctx, channel, reply);
} }
} }
} }

View File

@@ -0,0 +1,51 @@
using System.Collections.Concurrent;
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 => "Mock text";
public string Section => "Fun";
public string Usage => "mock [text] (Or reply to a message)";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
string[] args = ctx.Args;
Message message = ctx.Message;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
string text;
if (message.ReplyToId.HasValue)
{
var replyMessage = await message.FetchReplyMessageAsync();
text = replyMessage?.Content ?? "";
} else
{
if (args.Length == 0)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please provide some text to mock or reply to a message.");
return;
}
text = string.Join(" ", args);
}
if (string.IsNullOrWhiteSpace(text))
{
await MessageHelper.ReplyAsync(ctx, channel, "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, channel, mocked);
}
}
}

View File

@@ -0,0 +1,51 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Reverse : ICommand
{
public string Name => "reverse";
public string[] Aliases => [];
public string Description => "Reverses yours or a replied text.";
public string Section => "Fun";
public string Usage => "reverse [text] (Or reply to a message)";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
string[] args = ctx.Args;
Message message = ctx.Message;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
string text;
if (message.ReplyToId.HasValue)
{
var replyMessage = await message.FetchReplyMessageAsync();
text = replyMessage?.Content ?? "";
} else
{
if (args.Length == 0)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please provide some text to reverse or reply to a message.");
return;
}
text = string.Join(" ", args);
}
if (string.IsNullOrWhiteSpace(text))
{
await MessageHelper.ReplyAsync(ctx, channel, "No text to reverse.");
return;
}
string reversed = new string(text.Reverse().ToArray());
await MessageHelper.ReplyAsync(ctx, channel, $"Reversed: {reversed}");
}
}
}

View File

@@ -0,0 +1,61 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared;
namespace SkyBot.Commands
{
public class RockPaperScissors : ICommand
{
public string Name => "rps";
public string[] Aliases => [];
public string Description => "Play Rock Paper Scissors against the bot.";
public string Section => "Fun";
public string Usage => "rps <rock|paper|scissors>";
private static readonly string[] Choices = ["rock", "paper", "scissors"];
private static readonly string[] Emojis = ["🪨", "📄", "✂️"];
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
string[] args = ctx.Args;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
if (args.Length == 0)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please choose `Rock`, `Paper`, or `Scissors`.");
return;
}
string input = args[0].ToLower();
int playerIndex = Array.IndexOf(Choices, input);
if (playerIndex == -1)
{
await MessageHelper.ReplyAsync(ctx, channel, "Invalid choice. Please choose `Rock`, `Paper`, or `Scissors`.");
return;
}
TaskResult<Message> result = await MessageHelper.ReplyAsync(ctx, channel, "🤔 Thinking...");
await Task.Delay(1000);
int botIndex = Random.Shared.Next(3);
string playerChoice = $"{Emojis[playerIndex]} {Choices[playerIndex].ToTitleCase()}";
string botChoice = $"{Emojis[botIndex]} {Choices[botIndex].ToTitleCase()}";
string outcome;
if (playerIndex == botIndex) outcome = "It's a tie!";
else if ((playerIndex == 0 && botIndex == 2) ||
(playerIndex == 1 && botIndex == 0) ||
(playerIndex == 2 && botIndex == 1)) outcome = "You win! 🎉";
else outcome = "You Lose! 🥲";
await MessageHelper.EditAsync(channel, result.Data, $"**You**: {playerChoice}\nvs\n**Bot**: {botChoice}\n[]()\n{outcome}");
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;
@@ -10,7 +11,7 @@ namespace SkyBot.Commands
public string[] Aliases => ["dev"]; public string[] Aliases => ["dev"];
public string Description => "Sends an invite link to the Dev Central Planet."; public string Description => "Sends an invite link to the Dev Central Planet.";
public string Section => "Info"; public string Section => "Info";
public string Usage => "devcentral|dev"; public string Usage => "devcentral";
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)
{ {

View File

@@ -13,7 +13,7 @@ namespace SkyBot.Commands
public string[] Aliases => ["h"]; public string[] Aliases => ["h"];
public string Description => "Shows all the commands and their descriptions."; public string Description => "Shows all the commands and their descriptions.";
public string Section => "Info"; public string Section => "Info";
public string Usage => "help|h [section] [page]"; public string Usage => "help [section] [page]";
private const int PageSize = 5; private const int PageSize = 5;
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)
@@ -23,8 +23,6 @@ namespace SkyBot.Commands
string[] args = ctx.Args; string[] args = ctx.Args;
PlanetMember member = ctx.Member; PlanetMember member = ctx.Member;
bool isOwner = await PermissionHelper.IsOwner(member);
if (!channelCache.TryGetValue(channelId, out var channel)) return; if (!channelCache.TryGetValue(channelId, out var channel)) return;
// Show all sections. // Show all sections.
@@ -35,8 +33,8 @@ namespace SkyBot.Commands
foreach (var section in CommandRegistry.Sections.Keys) foreach (var section in CommandRegistry.Sections.Keys)
{ {
if (section == "template") continue; if (section == "template") continue;
if (section == "dev" && !isOwner) continue; if (section == "dev" && !PermissionHelper.IsOwner(member)) continue;
if (section == "mod" && !PermissionHelper.HasPermAsync(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles]).Result) continue; if (section == "mod" && !PermissionHelper.HasPerm(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles])) continue;
sb.AppendLine($"- `{section.ToTitleCase()}` ({CommandRegistry.Sections[section].Count})"); sb.AppendLine($"- `{section.ToTitleCase()}` ({CommandRegistry.Sections[section].Count})");
} }
sb.AppendLine($"\nUse `{Config.Prefix}help <category>` to see commands in a category."); sb.AppendLine($"\nUse `{Config.Prefix}help <category>` to see commands in a category.");
@@ -52,13 +50,13 @@ namespace SkyBot.Commands
return; return;
} }
if (sectionName == "dev" && !isOwner) if (sectionName == "dev" && !PermissionHelper.IsOwner(member))
{ {
await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`."); await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`.");
return; return;
} }
if (sectionName == "mod" && !PermissionHelper.HasPermAsync(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles]).Result) if (sectionName == "mod" && !PermissionHelper.HasPerm(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles]))
{ {
await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`."); await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`.");
return; return;

View File

@@ -0,0 +1,109 @@
using System.Collections.Concurrent;
using System.Text;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Info : ICommand
{
public string Name => "info";
public string[] Aliases => [];
public string Description => "Shows the info about a User or the Planet.";
public string Section => "Info";
public string Usage => "info <user|planet> [mention|memberid]";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
string[] args = ctx.Args;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
if (args.Length == 0)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please specify `user` or `planet`.");
return;
}
switch (args[0].ToLower())
{
case "user":
case "u":
await HandleUserInfo(ctx, channel);
break;
case "planet":
case "p":
await HandlePlanetInfo(ctx, channel);
break;
default:
await MessageHelper.ReplyAsync(ctx, channel, "invalid option. Use `user` or `planet`.");
break;
}
}
private async Task HandleUserInfo(CommandContext ctx, Channel channel)
{
Message message = ctx.Message;
Planet planet = ctx.Planet;
string[] args = ctx.Args;
PlanetMember? target;
try
{
if (message.Mentions != null && message.Mentions.Any())
{
target = await planet.FetchMemberAsync(message.Mentions.First().TargetId);
}
else if (args.Length > 1 && long.TryParse(args[1], out long memberid))
{
target = await planet.FetchMemberAsync(memberid);
}
else
{
target = ctx.Member;
}
}
catch (Exception ex)
{
Console.WriteLine($"Exception: {ex.Message}");
target = ctx.Member;
}
if (target == null)
{
await MessageHelper.ReplyAsync(ctx, channel, "Could not find that member.");
return;
}
var sb = new StringBuilder();
sb.AppendLine($"**{target.Name}**");
sb.AppendLine($"User ID: `{target.UserId}`");
sb.AppendLine($"Member ID: `{target.Id}`");
sb.AppendLine($"Nickname: `{(string.IsNullOrWhiteSpace(target.Nickname) ? "None" : target.Nickname)}`");
sb.AppendLine($"Status: `{(string.IsNullOrWhiteSpace(target.Status) ? "None" : target.Status)}`");
sb.AppendLine($"Primary Role: `{target.PrimaryRole?.Name ?? "None"}`");
sb.AppendLine($"Roles: `{string.Join(", ", target.Roles.Select(r => r.Name))}`");
await MessageHelper.ReplyAsync(ctx, channel, sb.ToString());
}
private async Task HandlePlanetInfo(CommandContext ctx, Channel channel)
{
var planet = ctx.Planet;
var sb = new StringBuilder();
sb.AppendLine($"**{planet.Name}**");
sb.AppendLine($"Planet ID: `{planet.Id}`");
sb.AppendLine($"Owner ID: `{planet.OwnerId}`");
sb.AppendLine($"Member Count: `{planet.Members?.Count ?? 0}`");
sb.AppendLine($"Channel Count: `{planet.Channels?.Count ?? 0}`");
sb.AppendLine($"Role Count: `{planet.Roles?.Count ?? 0}`");
sb.AppendLine($"Description: `{(string.IsNullOrWhiteSpace(planet.Description) ? "None" : planet.Description)}`");
await MessageHelper.ReplyAsync(ctx, channel, sb.ToString());
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;
@@ -10,7 +11,7 @@ namespace SkyBot.Commands
public string[] Aliases => ["mc"]; public string[] Aliases => ["mc"];
public string Description => "Sends the Unofficial ValourSMP IPs"; public string Description => "Sends the Unofficial ValourSMP IPs";
public string Section => "Info"; public string Section => "Info";
public string Usage => "minecraft|mc"; public string Usage => "minecraft";
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)
{ {

View File

@@ -0,0 +1,31 @@
using System.Collections.Concurrent;
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 => "Shows the bot's response time.";
public string Section => "Info";
public string Usage => "ping";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
DateTime start = DateTime.UtcNow;
TaskResult<Message> message = await MessageHelper.ReplyAsync(ctx, channel, "🏓 Pinging...");
double elapsed = (DateTime.UtcNow - start).TotalMilliseconds;
await MessageHelper.EditAsync(channel, message.Data, $"🏓 Pong! `{elapsed:F0}ms`");
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;
@@ -10,7 +11,7 @@ namespace SkyBot.Commands
public string[] Aliases => ["api"]; public string[] Aliases => ["api"];
public string Description => "Sends a link to the Valour.gg Swagger API."; public string Description => "Sends a link to the Valour.gg Swagger API.";
public string Section => "Info"; public string Section => "Info";
public string Usage => "swagger|api"; public string Usage => "swagger";
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)
{ {

View File

@@ -0,0 +1,30 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared;
namespace SkyBot.Commands
{
public class Uptime : ICommand
{
public string Name => "uptime";
public string[] Aliases => ["up"];
public string Description => "Shows how long the bot has been running.";
public string Section => "Info";
public string Usage => "uptime";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
TimeSpan uptime = DateTime.UtcNow - SkyBot.StartTime;
string formatted = $"{(int)uptime.TotalDays}d {uptime.Hours}h {uptime.Minutes}m {uptime.Seconds}s";
await MessageHelper.ReplyAsync(ctx, channel, $"⏱️ Uptime: `{formatted}`");
}
}
}

View File

@@ -11,13 +11,12 @@ namespace SkyBot.Commands
public string[] Aliases => ["users"]; public string[] Aliases => ["users"];
public string Description => "Shows the user count of Valour."; public string Description => "Shows the user count of Valour.";
public string Section => "Info"; public string Section => "Info";
public string Usage => "usercount|users"; public string Usage => "usercount";
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)
{ {
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache; ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId; long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = @$"Current Valour user count is: {ValourUsercountHelper.ValourUsercount:N0} string message = @$"Current Valour user count is: {ValourUsercountHelper.ValourUsercount:N0}
You can see a graph of the user count here: /meow"; You can see a graph of the user count here: /meow";

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;
@@ -10,7 +11,7 @@ namespace SkyBot.Commands
public string[] Aliases => []; public string[] Aliases => [];
public string Description => "Shows the current version of the Bot and Valour."; public string Description => "Shows the current version of the Bot and Valour.";
public string Section => "Info"; public string Section => "Info";
public string Usage => ""; public string Usage => "version";
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)
{ {

View File

@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using SkyBot.Helpers; using SkyBot.Helpers;
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;
using Valour.Shared;
using Valour.Shared.Authorization; using Valour.Shared.Authorization;
namespace SkyBot.Commands namespace SkyBot.Commands
@@ -11,26 +12,106 @@ namespace SkyBot.Commands
public string Name => "ban"; public string Name => "ban";
public string[] Aliases => []; public string[] Aliases => [];
public string Description => "Bans a user from the planet."; public string Description => "Bans a user from the planet.";
public string Section => "mod"; public string Section => "Mod";
public string Usage => "ban <user> [reason]"; public string Usage => "ban <mention|memberid> [reason] [length (y=year, M=month, w=week, d=day, h=hour, m=minute)]";
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)
{ {
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache; ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId; long channelId = ctx.ChannelId;
Planet planet = ctx.Planet;
Message message = ctx.Message;
string[] args = ctx.Args;
PlanetMember member = ctx.Member; PlanetMember member = ctx.Member;
PlanetMember bot = await planet.FetchMemberByUserAsync(ctx.Client.Me.Id);
if (channelCache.TryGetValue(channelId, out var channel)) if (channelCache.TryGetValue(channelId, out var channel))
{ {
if (!PermissionHelper.HasPermAsync(member, [PlanetPermissions.Ban]).Result) if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Ban]))
{ {
await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command."); await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command.");
return; return;
} }
string message = $"Work in progress..."; if (!PermissionHelper.HasPerm(bot, [PlanetPermissions.Ban]))
await MessageHelper.ReplyAsync(ctx, channel, message); {
await MessageHelper.ReplyAsync(ctx, channel, $"I don't have permission to ban members.");
return;
}
if (!message.Mentions.Any() && args.Length < 1)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please mention someone or user their id.");
return;
}
long targetId;
if (message.Mentions.Any())
{
targetId = message.Mentions.First().TargetId;
}
else if (!long.TryParse(args[0], out targetId))
{
await MessageHelper.ReplyAsync(ctx, channel, "Invalid member ID.");
return;
}
PlanetMember victim = await planet.FetchMemberAsync(targetId);
if (victim == null)
{
await MessageHelper.ReplyAsync(ctx, channel, "Could not find that member.");
return;
}
DateTime? expires = null;
List<string> remainingArgs = args[1..].ToList();
for (int i = 0; i < remainingArgs.Count; i++)
{
var parsed = MessageHelper.ParseDuration(remainingArgs[i]);
if (parsed != null)
{
expires = parsed;
remainingArgs.RemoveAt(i);
break;
}
}
string reason = remainingArgs.Count > 0 ? string.Join(" ", remainingArgs) : "No reason provided.";
PlanetBan ban = new PlanetBan(ctx.Client)
{
PlanetId = planet.Id,
TargetId = victim.UserId,
IssuerId = ctx.Client.Me.Id,
Reason = reason,
TimeCreated = DateTime.UtcNow,
TimeExpires = expires
};
TaskResult<PlanetBan> result = await ban.CreateAsync();
if (!result.Success)
{
await MessageHelper.ReplyAsync(ctx, channel, $"Failed to ban {victim.Name}: {result.Message}");
return;
}
if (expires == null)
{
await MessageHelper.ReplyAsync(ctx, channel, $"Permanently Banned `{victim.Name}`. Reason: `{reason}`");
} else
{
await MessageHelper.ReplyAsync(ctx, channel, $"Banned `{victim.Name}` until `{expires}`. Reason: `{reason}`");
}
} }
} }
} }
} }

View File

@@ -0,0 +1,88 @@
using System.Collections.Concurrent;
using System.Text;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared.Authorization;
using Valour.Shared.Models;
namespace SkyBot.Commands
{
public class GetBans : ICommand
{
public string Name => "bans";
public string[] Aliases => [""];
public string Description => "Lists all bans in the planet.";
public string Section => "Mod";
public string Usage => "bans [page]";
private const int PageSize = 10;
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
Planet planet = ctx.Planet;
string[] args = ctx.Args;
PlanetMember member = ctx.Member;
if (channelCache.TryGetValue(channelId, out var channel))
{
if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Ban]))
{
await MessageHelper.ReplyAsync(ctx, channel, "You don't have permission to use this command.");
return;
}
int page = 1;
if (args.Length > 0 && int.TryParse(args[0], out int parsedPage))
page = parsedPage;
int skip = (page - 1) * PageSize;
var queryResult = await planet.Node.PostAsyncWithResponse<QueryResponse<PlanetBan>>(
$"api/planets/{planet.Id}/bans/query",
new { skip, take = PageSize, options = new { } }
);
if (!queryResult.Success || queryResult.Data?.Items == null)
{
await MessageHelper.ReplyAsync(ctx, channel, "Failed to fetch bans.");
return;
}
if (!queryResult.Data.Items.Any())
{
await MessageHelper.ReplyAsync(ctx, channel, "No bans found.");
return;
}
int totalPages = (int)Math.Ceiling(queryResult.Data.TotalCount / (double)PageSize);
var sb = new StringBuilder();
sb.AppendLine($"**Bans** (Page {page}/{totalPages}):");
IEnumerable<PlanetBan> activeBans = queryResult.Data.Items.Where(b => b.Permanent || b.TimeExpires > DateTime.UtcNow);
if (!activeBans.Any())
{
await MessageHelper.ReplyAsync(ctx, channel, "No active bans found.");
return;
}
foreach (var ban in activeBans)
{
var user = await ctx.Client.UserService.FetchUserAsync(ban.TargetId);
string username = user?.NameAndTag ?? "Unknown";
string expires = ban.TimeExpires.HasValue
? $"{ban.TimeExpires}"
: "Never";
sb.AppendLine($"**{username}** `{user?.Id}` - {ban.Reason} (Expires: `{expires}`)");
}
sb.AppendLine($"\nUse `{Config.Prefix}bans <page>` to see more.");
await MessageHelper.ReplyAsync(ctx, channel, sb.ToString());
}
}
}
}

View File

@@ -11,26 +11,66 @@ namespace SkyBot.Commands
public string Name => "kick"; public string Name => "kick";
public string[] Aliases => []; public string[] Aliases => [];
public string Description => "Kicks a user from the planet."; public string Description => "Kicks a user from the planet.";
public string Section => "mod"; public string Section => "Mod";
public string Usage => "kick <user> [reason]"; public string Usage => "kick <user> [reason]";
public async Task Execute(CommandContext ctx) public async Task Execute(CommandContext ctx)
{ {
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache; ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId; long channelId = ctx.ChannelId;
Planet planet = ctx.Planet;
Message message = ctx.Message;
string[] args = ctx.Args;
PlanetMember member = ctx.Member; PlanetMember member = ctx.Member;
PlanetMember bot = await planet.FetchMemberByUserAsync(ctx.Client.Me.Id);
if (channelCache.TryGetValue(channelId, out var channel)) if (channelCache.TryGetValue(channelId, out var channel))
{ {
if (!PermissionHelper.HasPermAsync(member, [PlanetPermissions.Kick]).Result) if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Kick]))
{ {
await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command."); await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command.");
return; return;
} }
string message = $"Work in progress..."; if (!PermissionHelper.HasPerm(bot, [PlanetPermissions.Kick]))
{
await MessageHelper.ReplyAsync(ctx, channel, $"I don't have permission to kick members.");
return;
}
await MessageHelper.ReplyAsync(ctx, channel, message); if (!message.Mentions.Any() && args.Length < 2)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please mention someone or user their id.");
return;
}
long targetId;
if (message.Mentions.Any())
{
targetId = message.Mentions.First().TargetId;
}
else if (!long.TryParse(args[1], out targetId))
{
await MessageHelper.ReplyAsync(ctx, channel, "Invalid member ID.");
return;
}
PlanetMember victim = await planet.FetchMemberAsync(targetId);
if (victim == null)
{
await MessageHelper.ReplyAsync(ctx, channel, "Could not find that member.");
return;
}
string reason = args.Length > 1 && !message.Mentions.Any()
? string.Join(" ", args[2..])
: args.Length > 1
? string.Join(" ", args[1..])
: "No reason provided.";
await victim.DeleteAsync();
await MessageHelper.ReplyAsync(ctx, channel, $"Kicked {victim.Name}. Reason: {reason}");
} }
} }
} }

View File

@@ -0,0 +1,107 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared;
using Valour.Shared.Authorization;
using Valour.Shared.Models;
namespace SkyBot.Commands
{
public class Unban : ICommand
{
public string Name => "unban";
public string[] Aliases => [];
public string Description => "Unbans a user from the planet.";
public string Section => "Mod";
public string Usage => "unban <id>";
// public async Task Execute(CommandContext ctx)
// {
// ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
// long channelId = ctx.ChannelId;
// if (channelCache.TryGetValue(channelId, out var channel))
// {
// await MessageHelper.ReplyAsync(ctx, channel, "Unbanning is currently unavailable due to a Valour server bug.");
// }
// }
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
Planet planet = ctx.Planet;
Message message = ctx.Message;
string[] args = ctx.Args;
PlanetMember member = ctx.Member;
PlanetMember bot = await planet.FetchMemberByUserAsync(ctx.Client.Me.Id);
if (channelCache.TryGetValue(channelId, out var channel))
{
if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Ban]))
{
await MessageHelper.ReplyAsync(ctx, channel, "You don't have permission to use this command.");
return;
}
if (!PermissionHelper.HasPerm(bot, [PlanetPermissions.Ban]))
{
await MessageHelper.ReplyAsync(ctx, channel, "I don't have permission to unban members.");
return;
}
if (args.Length < 1)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a user ID.");
return;
}
if (!long.TryParse(args[0], out long targetUserId))
{
await MessageHelper.ReplyAsync(ctx, channel, "Invalid user ID.");
return;
}
PlanetBan? ban = null;
int skip = 0;
int take = 50;
while (ban == null)
{
var queryResult = await planet.Node.PostAsyncWithResponse<QueryResponse<PlanetBan>>(
$"api/planets/{planet.Id}/bans/query",
new {skip, take, options = new { }}
);
if (!queryResult.Success || queryResult.Data?.Items == null || !queryResult.Data.Items.Any())
break;
ban = queryResult.Data.Items.FirstOrDefault(b => b.TargetId == targetUserId && (b.Permanent || b.TimeExpires > DateTime.UtcNow));
if (queryResult.Data.Items.Count < take)
break;
skip += take;
}
if (ban == null)
{
await MessageHelper.ReplyAsync(ctx, channel, "Could not find a ban for that user.");
return;
}
ban.TimeExpires = DateTime.UtcNow.AddSeconds(-1);
TaskResult<PlanetBan> result = await planet.Node.PutAsyncWithResponse<PlanetBan>($"api/bans/{ban.Id}", ban);
if (!result.Success)
{
await MessageHelper.ReplyAsync(ctx, channel, "Failed to unban.");
return;
}
User user = await ctx.Client.UserService.FetchUserAsync(targetUserId);
await MessageHelper.ReplyAsync(ctx, channel, $"Unbanned `{user?.NameAndTag ?? targetUserId.ToString()}`.");
}
}
}
}

View File

@@ -1,10 +0,0 @@
using Valour.Sdk.Models;
namespace SkyBot.Helpers
{
public static class MentionHelper
{
public static string Mention(this PlanetMember member) => $"«@m-{member.Id}»";
public static string Mention(this User user) => $"«@u-{user.Id}»";
}
}

View File

@@ -1,24 +1,53 @@
using SkyBot.Models; using SkyBot.Models;
using Valour.Sdk.Models; using Valour.Sdk.Models;
using Valour.Shared;
public static class MessageHelper namespace SkyBot.Helpers
{ {
public static async Task ReplyAsync(CommandContext ctx, Channel channel, string content) public static class MessageHelper
{ {
long? replyToId = ctx.Message.ReplyToId.HasValue ? ctx.Message.ReplyToId : ctx.Message.Id;
var msg = new Message(ctx.Client) 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) => System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str);
public static async Task<TaskResult<Message>> ReplyAsync(CommandContext ctx, Channel channel, string content)
{ {
Content = content, long? replyToId = ctx.Message.ReplyToId.HasValue ? ctx.Message.ReplyToId : ctx.Message.Id;
ChannelId = channel.Id,
PlanetId = ctx.Planet.Id,
AuthorUserId = ctx.Client.Me.Id,
AuthorMemberId = channel.Planet?.MyMember.Id,
ReplyToId = replyToId,
Fingerprint = Guid.NewGuid().ToString()
};
await ctx.Client.MessageService.SendMessage(msg);
}
public static string ToTitleCase(this string str) => System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str); var msg = new Message(ctx.Client)
{
Content = content,
ChannelId = channel.Id,
PlanetId = ctx.Planet.Id,
AuthorUserId = ctx.Client.Me.Id,
AuthorMemberId = 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);
}
public static DateTime? ParseDuration(string input)
{
if (string.IsNullOrWhiteSpace(input)) return null;
var unit = input[^1];
if (!int.TryParse(input[..^1], out int value)) return null;
return unit switch
{
'm' => DateTime.UtcNow.AddMinutes(value),
'h' => DateTime.UtcNow.AddHours(value),
'd' => DateTime.UtcNow.AddDays(value),
'w' => DateTime.UtcNow.AddDays(value * 7),
'M' => DateTime.UtcNow.AddMonths(value),
'y' => DateTime.UtcNow.AddYears(value),
_ => null
};
}
}
} }

View File

@@ -5,7 +5,7 @@ namespace SkyBot.Helpers
{ {
public static class PermissionHelper public static class PermissionHelper
{ {
public static async Task<bool> HasPermAsync(PlanetMember member, PlanetPermission[] permissions, bool requireAll = false) public static bool HasPerm(PlanetMember member, PlanetPermission[] permissions, bool requireAll = false)
{ {
if (member == null) return false; if (member == null) return false;
if (member.HasPermission(PlanetPermissions.FullControl)) return true; if (member.HasPermission(PlanetPermissions.FullControl)) return true;
@@ -16,7 +16,7 @@ namespace SkyBot.Helpers
: permissions.Any(permission => member.HasPermission(permission)); : permissions.Any(permission => member.HasPermission(permission));
} }
public static async Task<bool> IsOwner(PlanetMember member) public static bool IsOwner(PlanetMember member)
{ {
if (member == null) return false; if (member == null) return false;

View File

@@ -29,23 +29,25 @@ namespace SkyBot.Services.Messages
string command = parts[0].ToLower(); string command = parts[0].ToLower();
string[] args = parts[1..]; string[] args = parts[1..];
CommandContext ctx = new CommandContext
{
ChannelCache = channelCache,
ChannelId = channelId,
Member = member,
Planet = message.Planet,
Args = args,
Message = message,
Client = client
};
if (CommandRegistry.Commands.TryGetValue(command, out var handler)) if (CommandRegistry.Commands.TryGetValue(command, out var handler))
{ {
await handler.Execute(new CommandContext await handler.Execute(ctx);
{
ChannelCache = channelCache,
ChannelId = channelId,
Member = member,
Planet = message.Planet,
Args = args,
Message = message,
Client = client
});
} else } else
{ {
if (channelCache.TryGetValue(channelId, out var channel)) if (channelCache.TryGetValue(channelId, out var channel))
{ {
await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Unknown command."); await MessageHelper.ReplyAsync(ctx, channel, $"Unknown command `{command}`.");
} }
} }
} }

View File

@@ -10,6 +10,7 @@ namespace SkyBot
private readonly ValourClient _client; private readonly ValourClient _client;
private readonly ConcurrentDictionary<long, Channel> _channelCache = new(); private readonly ConcurrentDictionary<long, Channel> _channelCache = new();
private readonly ConcurrentDictionary<long, bool> _initializedPlanets = new(); private readonly ConcurrentDictionary<long, bool> _initializedPlanets = new();
public static DateTime StartTime;
public SkyBot() public SkyBot()
{ {
@@ -19,6 +20,7 @@ namespace SkyBot
public async Task StartAsync() public async Task StartAsync()
{ {
StartTime = DateTime.UtcNow;
await BotService.InitializeBotAsync(_client, _channelCache, _initializedPlanets); await BotService.InitializeBotAsync(_client, _channelCache, _initializedPlanets);
} }
} }
@@ -32,6 +34,7 @@ namespace SkyBot
try try
{ {
await new SkyBot().StartAsync(); await new SkyBot().StartAsync();
Console.WriteLine("Ready and listening..."); Console.WriteLine("Ready and listening...");
await Task.Delay(Timeout.Infinite); await Task.Delay(Timeout.Infinite);