From 87a459aa6bc958afe40e5ae5bd75639bdf2e6081 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Thu, 12 Mar 2026 20:22:41 +0000 Subject: [PATCH 1/6] funny react --- Program.cs | 26 ++++++++++++++++++++++++++ utils.cs | 29 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/Program.cs b/Program.cs index da31b5e..0e6a9cd 100644 --- a/Program.cs +++ b/Program.cs @@ -66,6 +66,32 @@ client.MessageService.MessageReceived += async (message) => } }; + if (Utils.ContainsAny(content, $"{prefix}react")) + { + if (message.AuthorUserId != ownerId) return; + + string[] args = content.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (args.Length < 2) return; + string emoji = args[1]; + + var interceptor = new Utils.ReactionInterceptor(Console.Out); + Console.SetOut(interceptor); + + while (true) + { + interceptor.Reset(); + await message.AddReactionAsync(emoji); + if (interceptor.DetectedAlreadyExists) + { + Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }); + Console.WriteLine("Reaction already exists, stopping."); + break; + } + } + + Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }); + } + var echoprefixes = new[] { $"{prefix}echo"}; if (Utils.ContainsAny(content, echoprefixes)) { diff --git a/utils.cs b/utils.cs index 5412a4e..f6d3776 100644 --- a/utils.cs +++ b/utils.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Text.Json; using Valour.Sdk.Models; using Valour.Sdk.Client; +using System.Text; namespace SkyBot { @@ -105,5 +106,33 @@ namespace SkyBot initializedPlanets.Add(planet.Id); } } + + public class ReactionInterceptor : TextWriter + { + private readonly TextWriter _original; + public bool DetectedAlreadyExists { get; private set; } + public override Encoding Encoding => _original.Encoding; + + public ReactionInterceptor(TextWriter original) + { + _original = original; + } + + public void Reset() => DetectedAlreadyExists = false; + + public override void WriteLine(string value) + { + if (value?.Contains("Reaction already exists") == true) + DetectedAlreadyExists = true; + _original.WriteLine(value); + } + + public override void Write(string value) + { + if (value?.Contains("Reaction already exists") == true) + DetectedAlreadyExists = true; + _original.Write(value); + } + } }; }; \ No newline at end of file From f20c65bfca8b26b1cb128298a732cd0b463fd169 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Thu, 12 Mar 2026 20:24:24 +0000 Subject: [PATCH 2/6] prefix in env --- Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Program.cs b/Program.cs index 0e6a9cd..4f28428 100644 --- a/Program.cs +++ b/Program.cs @@ -8,7 +8,7 @@ Env.Load(); var token = Environment.GetEnvironmentVariable("TOKEN"); var allowedUserIds = new List { 15652354820931584 }; var ownerId = 15652354820931584; -var prefix = "s/"; +var prefix = Environment.GetEnvironmentVariable("PREFIX"); var client = new ValourClient("https://api.valour.gg/"); client.SetupHttpClient(); From 21ddaf4c0bc60e9236d50cdfb63b7e61a20bccaf Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Thu, 12 Mar 2026 20:26:56 +0000 Subject: [PATCH 3/6] yes. --- Program.cs | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Program.cs b/Program.cs index 4f28428..3203ac8 100644 --- a/Program.cs +++ b/Program.cs @@ -8,7 +8,7 @@ Env.Load(); var token = Environment.GetEnvironmentVariable("TOKEN"); var allowedUserIds = new List { 15652354820931584 }; var ownerId = 15652354820931584; -var prefix = Environment.GetEnvironmentVariable("PREFIX"); +var prefix = Environment.GetEnvironmentVariable("DEVPREFIX"); var client = new ValourClient("https://api.valour.gg/"); client.SetupHttpClient(); diff --git a/README.md b/README.md index bae0693..9b18363 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ provided SkyBot.csproj file.

Before running the bot, create a .env file in the root directory of the project with the following content:

TOKEN=your-bot-token-here
+DEVPREFIX=your-prefix-here
 
    From 177cb12b59c59f65584750c92a191e04bc44e33b Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Thu, 12 Mar 2026 20:27:40 +0000 Subject: [PATCH 4/6] yes. --- Program.cs | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Program.cs b/Program.cs index 3203ac8..4f28428 100644 --- a/Program.cs +++ b/Program.cs @@ -8,7 +8,7 @@ Env.Load(); var token = Environment.GetEnvironmentVariable("TOKEN"); var allowedUserIds = new List { 15652354820931584 }; var ownerId = 15652354820931584; -var prefix = Environment.GetEnvironmentVariable("DEVPREFIX"); +var prefix = Environment.GetEnvironmentVariable("PREFIX"); var client = new ValourClient("https://api.valour.gg/"); client.SetupHttpClient(); diff --git a/README.md b/README.md index 9b18363..a8ab730 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ provided SkyBot.csproj file.

    Before running the bot, create a .env file in the root directory of the project with the following content:

    TOKEN=your-bot-token-here
    -DEVPREFIX=your-prefix-here
    +PREFIX=your-prefix-here
     
      From ab9c7223caf396e4b1fa59ae36239697ab04d7b1 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sun, 15 Mar 2026 01:29:16 +0000 Subject: [PATCH 5/6] first build of v2 --- .gitignore | 8 +- Program.cs | 248 -------------------------- SkyBot/Commands/HelpCommand.cs | 30 ++++ SkyBot/Config.cs | 9 + SkyBot/Helpers/MentionHelper.cs | 10 ++ SkyBot/Helpers/PermissionHelper.cs | 19 ++ SkyBot/Services/BotService.cs | 36 ++++ SkyBot/Services/ChannelService.cs | 37 ++++ SkyBot/Services/Messages/Create.cs | 51 ++++++ SkyBot/Services/PlanetService.cs | 34 ++++ SkyBot/SkyBot.cs | 51 ++++++ SkyBot.csproj => SkyBot/SkyBot.csproj | 0 utils.cs | 138 -------------- 13 files changed, 281 insertions(+), 390 deletions(-) delete mode 100644 Program.cs create mode 100644 SkyBot/Commands/HelpCommand.cs create mode 100644 SkyBot/Config.cs create mode 100644 SkyBot/Helpers/MentionHelper.cs create mode 100644 SkyBot/Helpers/PermissionHelper.cs create mode 100644 SkyBot/Services/BotService.cs create mode 100644 SkyBot/Services/ChannelService.cs create mode 100644 SkyBot/Services/Messages/Create.cs create mode 100644 SkyBot/Services/PlanetService.cs create mode 100644 SkyBot/SkyBot.cs rename SkyBot.csproj => SkyBot/SkyBot.csproj (100%) delete mode 100644 utils.cs diff --git a/.gitignore b/.gitignore index 304f7ba..c9b1561 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -bin/ -obj/ -SkyBot.sln .env -Program.cs.old +.gitignore +SkyBot/bin/ +SkyBot/obj/ +SkyBot/SkyBot.sln diff --git a/Program.cs b/Program.cs deleted file mode 100644 index 4f28428..0000000 --- a/Program.cs +++ /dev/null @@ -1,248 +0,0 @@ -using Valour.Sdk.Client; -using Valour.Sdk.Models; -using DotNetEnv; -using SkyBot; - -Env.Load(); - -var token = Environment.GetEnvironmentVariable("TOKEN"); -var allowedUserIds = new List { 15652354820931584 }; -var ownerId = 15652354820931584; -var prefix = Environment.GetEnvironmentVariable("PREFIX"); - -var client = new ValourClient("https://api.valour.gg/"); -client.SetupHttpClient(); - -if (string.IsNullOrWhiteSpace(token)) -{ - Console.WriteLine("TOKEN environment variable not set."); - return; -} - -var loginResult = await client.InitializeUser(token); -if (!loginResult.Success) -{ - Console.WriteLine($"Login Failed: {loginResult.Message}"); - return; -} -Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})"); - -await Utils.UpdateValourUserCountAsync(); -Utils.StartValourUserUpdater(); - - -//Dictionaries -var channelCache = new Dictionary(); -var InitializedPlanets = new HashSet(); - - - - -await Utils.InitializePlanetsAsync(client, channelCache, InitializedPlanets); - -client.PlanetService.JoinedPlanetsUpdated += async () => -{ - await Utils.InitializePlanetsAsync(client, channelCache, InitializedPlanets); -}; - - -client.MessageService.MessageReceived += async (message) => -{ - string content = message.Content ?? ""; - long channelId = message.ChannelId; - var member = await message.FetchAuthorMemberAsync(); - var pingMember = $"«@m-{member.Id}»"; - - if (content is null) return; - - if (message.AuthorUserId == client.Me.Id) return; - - - if (allowedUserIds.Contains(message.AuthorUserId)) - { - if (Utils.IsSingleEmoji(content)) - { - await message.AddReactionAsync(content); - } - }; - - if (Utils.ContainsAny(content, $"{prefix}react")) - { - if (message.AuthorUserId != ownerId) return; - - string[] args = content.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (args.Length < 2) return; - string emoji = args[1]; - - var interceptor = new Utils.ReactionInterceptor(Console.Out); - Console.SetOut(interceptor); - - while (true) - { - interceptor.Reset(); - await message.AddReactionAsync(emoji); - if (interceptor.DetectedAlreadyExists) - { - Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }); - Console.WriteLine("Reaction already exists, stopping."); - break; - } - } - - Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }); - } - - var echoprefixes = new[] { $"{prefix}echo"}; - if (Utils.ContainsAny(content, echoprefixes)) - { - - var matchedPrefix = echoprefixes.First(p => content.StartsWith(p, StringComparison.OrdinalIgnoreCase)); - - var reply = content.Substring(matchedPrefix.Length).TrimStart(); - - if (string.IsNullOrWhiteSpace(reply)) await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} Enter a message to echo."); - - reply = $"{pingMember} {reply}"; - - if (reply.Length > 2048) - { - reply = reply.Substring(0, 2048); - } - - await Utils.SendReplyAsync(channelCache, channelId, reply); - }; - - var echorawprefixes = new[] { $"{prefix}rawecho"}; - if (Utils.ContainsAny(content, echorawprefixes)) - { - - if (message.AuthorUserId != ownerId) - { - await Utils.SendReplyAsync(channelCache, channelId, "You do not have permission to execute this command."); - return; - } - - var matchedPrefix = echorawprefixes.First(p => content.StartsWith(p, StringComparison.OrdinalIgnoreCase)); - - var reply = content.Substring(matchedPrefix.Length).TrimStart(); - - if (string.IsNullOrWhiteSpace(reply)) - { - await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} Enter a message to echo."); - return; - } - - reply = $"{reply}"; - - if (reply.Length > 2048) - { - reply = reply.Substring(0, 2048); - } - - await Utils.SendReplyAsync(channelCache, channelId, reply); - }; - - if (Utils.ContainsAny(content, $"{prefix}suggest")) - { - await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} You can suggest a command to be added here: https://docs.google.com/spreadsheets/d/1CzcpLAuMiPL_RODrZ5x25cPj8yE-rR3mEnqrd_2Fbmk"); - }; - - if (Utils.ContainsAny(content, $"{prefix}source")) - { - await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} You can see my source code here: https://github.com/SkyJoshua/SkyBot"); - }; - - if (Utils.ContainsAny(content, $"{prefix}joincode")) - { - await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} You can use this to join a planet: https://github.com/SkyJoshua/JoinPlanet"); - }; - - if (Utils.ContainsAny(content, $"{prefix}joinsite")) - { - await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} You can use this website to easily add your bot to a planet: https://skyjoshua.xyz/planetjoiner"); - }; - - if (Utils.ContainsAny(content, $"{prefix}api", $"{prefix}swagger")) - { - await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} Here is a link to the Swagger API: https://api.valour.gg/swagger"); - }; - - if (Utils.ContainsAny(content, $"{prefix}cmds", $"{prefix}help")) - { - await Utils.SendReplyAsync(channelCache, channelId, @$"{pingMember} Here is a list of my commands: - - `s/echo - Echos text into the chat` - - `s/suggest - Shares the suggestions link` - - `s/source - Sends link for the source code` - - `s/joincode - Sends a link to a github that you can use to make your bot join your planet.` - - `s/joinsite - Sends a link to a website that you can use to make yout bot join your planet.` - - `s/api|swagger - Sends a link to the Swagger API` - - `s/cmds|help - Shows this list` - - `s/usercount - Shows the user count of Valour` - - `s/devcentral - Sends the invite link to the Dev Central Planet` - - `s/mc - Sends Unofficial ValourSMP IP` - "); - }; - - if (Utils.ContainsAny(content, $"{prefix}usercount")) - { - await Utils.SendReplyAsync(channelCache, channelId, @$"{pingMember} - Current Valour user count is: {Utils.ValourUserCount:N0} - You can see a graph of the user count here: /meow"); - }; - - if (Utils.ContainsAny(content, $"{prefix}devcentral")) - { - await Utils.SendReplyAsync(channelCache, channelId, @$"{pingMember} you can join the Dev Central (ID: 42439954653511681) planet here: https://app.valour.gg/I/k2tz9c4i"); - } - - if (Utils.ContainsAny(content, $"{prefix}mc")) - { - await Utils.SendReplyAsync(channelCache, channelId, @$"{pingMember} you can join the Unofficial ValourSMP Minecraft Server by using this ip: - Java: `valour.sxsc.xyz`, Bedrock: `valourbr.sxsc.xyz` Both with the default ports. - Cool features can be found here: https://sxsc.xyz/servers/valour/"); - } - - if (Utils.ContainsAny(content, $"{prefix}invite")) - { - if(message.AuthorUserId != ownerId) return; - - string[] args = content.Split(' ', StringSplitOptions.RemoveEmptyEntries); - - if (args.Length < 2) - { - await Utils.SendReplyAsync(channelCache, channelId, "Usage: s/invite [inviteCode]"); - } - - if (!long.TryParse(args[1], out long planetId)) - { - await Utils.SendReplyAsync(channelCache, channelId, "Planet ID is not valid."); - return; - } - - string inviteCode = args.Length > 2 ? args[2] : ""; - - var joinResult = await client.PlanetService.JoinPlanetAsync(planetId, inviteCode); - - if (joinResult.Success && joinResult.Data != null) - { - await Task.Delay(200); - - if (client.Cache.Planets.TryGet(planetId, out var planet)) - { - if (planet is null) return; - await Utils.SendReplyAsync(channelCache, channelId, $"Joined planet: {planet.Name}"); - } - else - { - await Utils.SendReplyAsync(channelCache, channelId, "Joined planet, but could not retrieve its name."); - } - } - else - { - await Utils.SendReplyAsync(channelCache, channelId, $"Failed to join planet: {joinResult.Message}"); - } - }; -}; - -Console.WriteLine("Listening for messages..."); -await Task.Delay(Timeout.Infinite); \ No newline at end of file diff --git a/SkyBot/Commands/HelpCommand.cs b/SkyBot/Commands/HelpCommand.cs new file mode 100644 index 0000000..48cc518 --- /dev/null +++ b/SkyBot/Commands/HelpCommand.cs @@ -0,0 +1,30 @@ +using System.Collections.Concurrent; +using SkyBot.Helpers; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public static class HelpCommand + { + public static async Task Execute(ConcurrentDictionary channelCache, long channelId, String prefix, PlanetMember member) + { + string helpMessage = $@"**Skybot Commands**: + - `s/echo ` - Echos text into the chat + - `s/suggest` - Shares the suggestions link + - `s/source` - Sends link for the source code + - `s/joincode` - Sends a link to a github that you can use to make your bot join your planet. + - `s/joinsite` - Sends a link to a website that you can use to make yout bot join your planet. + - `s/api|swagger` - Sends a link to the Swagger API + - `s/cmds|help` - Shows this list + - `s/usercount` - Shows the user count of Valour + - `s/devcentral` - Sends the invite link to the Dev Central Planet + - `s/mc` - Sends Unofficial ValourSMP IPs + "; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await channel.SendMessageAsync($"{MentionHelper.Mention(member)}\n{helpMessage}"); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Config.cs b/SkyBot/Config.cs new file mode 100644 index 0000000..b2b203b --- /dev/null +++ b/SkyBot/Config.cs @@ -0,0 +1,9 @@ +namespace Skybot +{ + + public static class Config { + public static readonly long OwnerId = 15652354820931584; + public static readonly string Prefix = "sd/"; + + } +} \ No newline at end of file diff --git a/SkyBot/Helpers/MentionHelper.cs b/SkyBot/Helpers/MentionHelper.cs new file mode 100644 index 0000000..2c6544b --- /dev/null +++ b/SkyBot/Helpers/MentionHelper.cs @@ -0,0 +1,10 @@ +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}»"; + } +} \ No newline at end of file diff --git a/SkyBot/Helpers/PermissionHelper.cs b/SkyBot/Helpers/PermissionHelper.cs new file mode 100644 index 0000000..5adb73f --- /dev/null +++ b/SkyBot/Helpers/PermissionHelper.cs @@ -0,0 +1,19 @@ +using Valour.Sdk.Models; +using Valour.Shared.Authorization; + +namespace SkyBot.Helpers +{ + public static class PermissionHelper + { + public static async Task HasPermAsync(PlanetMember member, PlanetPermission[] permissions, bool requireAll = false) + { + if (member == null) return false; + if (member.HasPermission(PlanetPermissions.FullControl)) return true; + if (member.Roles.Any(r => r.IsAdmin)) return true; + + return requireAll + ? permissions.All(permission => member.HasPermission(permission)) + : permissions.Any(permission => member.HasPermission(permission)); + } + } +} \ No newline at end of file diff --git a/SkyBot/Services/BotService.cs b/SkyBot/Services/BotService.cs new file mode 100644 index 0000000..279cdc4 --- /dev/null +++ b/SkyBot/Services/BotService.cs @@ -0,0 +1,36 @@ +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 InitializeBotAsync( + ValourClient client, + ConcurrentDictionary channelCache, + ConcurrentDictionary initalizedPlanets) + { + Env.Load(); + + var token = Environment.GetEnvironmentVariable("TOKEN"); + if (string.IsNullOrWhiteSpace(token)) {Console.WriteLine("TOKEN not set."); return;} + + var loginResult = await client.InitializeUser(token); + if (!loginResult.Success) {Console.WriteLine($"Login Failed: {loginResult.Message}"); return;} + Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})"); + + await PlanetService.InitializePlanetsAsync(client, channelCache, initalizedPlanets); + client.PlanetService.JoinedPlanetsUpdated += async () => + { + await PlanetService.InitializePlanetsAsync(client, channelCache, initalizedPlanets); + }; + + client.MessageService.MessageReceived += async (message) => + { + await Messages.Create.MessageAsync(client, channelCache, message); + }; + } + } +} \ No newline at end of file diff --git a/SkyBot/Services/ChannelService.cs b/SkyBot/Services/ChannelService.cs new file mode 100644 index 0000000..630df4e --- /dev/null +++ b/SkyBot/Services/ChannelService.cs @@ -0,0 +1,37 @@ +using System.Collections.Concurrent; +using Valour.Sdk.Models; +using Valour.Shared.Models; + +namespace SkyBot.Services +{ + public static class ChannelService + { + private static readonly SemaphoreSlim _channelSemaphore = new SemaphoreSlim(3, 3); + public static async Task InitializeChannelsAsync( + ConcurrentDictionary channelCache, + Planet planet) + { + var tasks = planet.Channels.Select(async channel => + { + channelCache[channel.Id] = channel; + if (channel.ChannelType == ChannelTypeEnum.PlanetChat) + { + await _channelSemaphore.WaitAsync(); + try + { + await channel.OpenWithResult("SkyBot"); + Console.WriteLine($"Realtime opened for: {planet.Name} (ID: {planet.Id}) -> {channel.Name} (ID: {channel.Id})"); + await Task.Delay(250); + } + finally + { + _channelSemaphore.Release(); + } + + } + }); + + await Task.WhenAll(tasks); + } + } +} \ No newline at end of file diff --git a/SkyBot/Services/Messages/Create.cs b/SkyBot/Services/Messages/Create.cs new file mode 100644 index 0000000..2345934 --- /dev/null +++ b/SkyBot/Services/Messages/Create.cs @@ -0,0 +1,51 @@ +using System.Collections.Concurrent; +using Skybot; +using SkyBot.Commands; +using SkyBot.Helpers; +using Valour.Sdk.Client; +using Valour.Sdk.Models; + +namespace SkyBot.Services.Messages +{ + public static class Create + { + public static async Task MessageAsync( + ValourClient client, + ConcurrentDictionary channelCache, + Message message + ) + { + string prefix = Config.Prefix; + + if (message.AuthorUserId == client.Me.Id) return; + + string content = message.Content ?? ""; + if (string.IsNullOrWhiteSpace(content)) return; + if (!content.StartsWith(prefix)) return; + + long channelId = message.ChannelId; + + PlanetMember member = await message.FetchAuthorMemberAsync(); + + var parts = content.Substring(prefix.Length).Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) return; + + string command = parts[0].ToLower(); + string[] args = parts[1..]; + + switch (command) + { + case "help": + await HelpCommand.Execute(channelCache, channelId, prefix, member); + break; + + default: + if (channelCache.TryGetValue(channelId, out var channel)) + { + await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Unknown command."); + } + break; + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Services/PlanetService.cs b/SkyBot/Services/PlanetService.cs new file mode 100644 index 0000000..911fccf --- /dev/null +++ b/SkyBot/Services/PlanetService.cs @@ -0,0 +1,34 @@ +using System.Collections.Concurrent; +using SkyBot.Services; +using Valour.Sdk.Client; +using Valour.Sdk.Models; +using Valour.Sdk.Models.Messages.Embeds; + + +namespace SkyBot.Services +{ + public static class PlanetService + { + public static async Task InitializePlanetsAsync( + ValourClient client, + ConcurrentDictionary channelCache, + ConcurrentDictionary initializedPlanets) + { + var tasks = client.PlanetService.JoinedPlanets + .Where(planet => !initializedPlanets.ContainsKey(planet.Id)) + .Select(async planet => + { + Console.WriteLine($"Initializing Planet: {planet.Name}"); + await planet.EnsureReadyAsync(); + await planet.FetchInitialDataAsync(); + await ChannelService.InitializeChannelsAsync(channelCache, planet); + + planet.Channels.Changed += async (channelEvent) => { + await ChannelService.InitializeChannelsAsync(channelCache, planet); + }; + }); + + await Task.WhenAll(tasks); + } + } +} \ No newline at end of file diff --git a/SkyBot/SkyBot.cs b/SkyBot/SkyBot.cs new file mode 100644 index 0000000..4326c29 --- /dev/null +++ b/SkyBot/SkyBot.cs @@ -0,0 +1,51 @@ +using Valour.Sdk.Client; +using Valour.Sdk.Models; +using SkyBot.Services; +using System.Collections.Concurrent; + +namespace SkyBot +{ + public class SkyBot + { + private readonly ValourClient _client; + private readonly ConcurrentDictionary _channelCache = new(); + private readonly ConcurrentDictionary _initializedPlanets = new(); + + public SkyBot() + { + _client = new ValourClient("https://api.valour.gg/"); + _client.SetupHttpClient(); + } + + public async Task StartAsync() + { + await BotService.InitializeBotAsync(_client, _channelCache, _initializedPlanets); + } + } + + public class Program + { + public static async Task Main(string[] args) + { + while (true) + { + try + { + await new SkyBot().StartAsync(); + + Console.WriteLine("Ready and listening..."); + await Task.Delay(Timeout.Infinite); + } catch (InvalidOperationException ex) when (ex.Message.Contains("concurrent update")) + { + Console.WriteLine("Concurrent update detected, restarting..."); + await Task.Delay(1000); + } catch (Exception ex) + { + Console.WriteLine($"Fatal error: {ex.Message}"); + break; + } + } + + } + } +} diff --git a/SkyBot.csproj b/SkyBot/SkyBot.csproj similarity index 100% rename from SkyBot.csproj rename to SkyBot/SkyBot.csproj diff --git a/utils.cs b/utils.cs deleted file mode 100644 index f6d3776..0000000 --- a/utils.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Valour.Sdk.Models; -using Valour.Sdk.Client; -using System.Text; - -namespace SkyBot -{ - public static class Utils - { - - private static readonly HttpClient _http = new HttpClient(); - private static long _valourUserCount; - public static long ValourUserCount => _valourUserCount; - - - - public static bool IsSingleEmoji(string input) - { - if (string.IsNullOrWhiteSpace(input)) - return false; - - input = input.Trim(); - - var enumerator = StringInfo.GetTextElementEnumerator(input); - int count = 0; - - while (enumerator.MoveNext()) - count++; - - return count == 1; - } - - public static bool ContainsAny(string input, params string[] values) - { - var lower = input.ToLower(); - - foreach (var value in values) - { - if (lower.Contains(value.ToLower())) - return true; - }; - - return false; - } - - public static async Task SendReplyAsync(Dictionary channelCache, long channel, string reply) - { - if (channelCache.TryGetValue(channel, out var chan)) - { - await chan.SendMessageAsync(reply); - } - else - { - Console.WriteLine($"Channel {channel} was not found in the cache."); - }; - } - - public static async Task UpdateValourUserCountAsync() - { - try - { - var response = await _http.GetStringAsync("https://api.valour.gg/api/users/count"); - - _valourUserCount = JsonSerializer.Deserialize(response); - - Console.WriteLine($"Valour user count updated: {_valourUserCount}"); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to update Valour user count: {ex.Message}"); - } - } - - public static void StartValourUserUpdater() - { - var timer = new System.Timers.Timer(300_000); - timer.Elapsed += async (_, _) => await UpdateValourUserCountAsync(); - timer.AutoReset = true; - timer.Start(); - } - - public static async Task InitializePlanetsAsync(ValourClient client, Dictionary channelCache, HashSet initializedPlanets) - { - foreach (var planet in client.PlanetService.JoinedPlanets) - { - if (initializedPlanets.Contains(planet.Id)) - continue; - - Console.WriteLine($"Initializing Planet: {planet.Name}"); - - await planet.EnsureReadyAsync(); - await planet.FetchInitialDataAsync(); - - foreach (var channel in planet.Channels) - { - channelCache[channel.Id] = channel; - - if (channel.ChannelType == Valour.Shared.Models.ChannelTypeEnum.PlanetChat) - { - await channel.OpenWithResult("SkyBot"); - Console.WriteLine($"Realtime opened for: {planet.Name} -> {channel.Name}"); - } - } - - initializedPlanets.Add(planet.Id); - } - } - - public class ReactionInterceptor : TextWriter - { - private readonly TextWriter _original; - public bool DetectedAlreadyExists { get; private set; } - public override Encoding Encoding => _original.Encoding; - - public ReactionInterceptor(TextWriter original) - { - _original = original; - } - - public void Reset() => DetectedAlreadyExists = false; - - public override void WriteLine(string value) - { - if (value?.Contains("Reaction already exists") == true) - DetectedAlreadyExists = true; - _original.WriteLine(value); - } - - public override void Write(string value) - { - if (value?.Contains("Reaction already exists") == true) - DetectedAlreadyExists = true; - _original.Write(value); - } - } - }; -}; \ No newline at end of file From eb06fc810285df052615ab06bbf0394ab569b6be Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Sun, 15 Mar 2026 05:44:32 +0000 Subject: [PATCH 6/6] Holy Big Commit! --- SkyBot/Commands/CommandRegistry.cs | 33 +++++++++ SkyBot/Commands/CommandTemplate.cs | 29 ++++++++ SkyBot/Commands/Dev/Test.cs | 32 +++++++++ SkyBot/Commands/Fun/Echo.cs | 42 +++++++++++ SkyBot/Commands/HelpCommand.cs | 30 -------- SkyBot/Commands/Info/Devcentral.cs | 29 ++++++++ SkyBot/Commands/Info/Help.cs | 92 +++++++++++++++++++++++++ SkyBot/Commands/Info/JoinSite.cs | 29 ++++++++ SkyBot/Commands/Info/Minecraft.cs | 31 +++++++++ SkyBot/Commands/Info/Source.cs | 29 ++++++++ SkyBot/Commands/Info/Suggest.cs | 29 ++++++++ SkyBot/Commands/Info/SwaggerAPI.cs | 29 ++++++++ SkyBot/Commands/Info/UserCount.cs | 31 +++++++++ SkyBot/Commands/Info/Version.cs | 30 ++++++++ SkyBot/Commands/Mod/Ban.cs | 36 ++++++++++ SkyBot/Commands/Mod/Kick.cs | 37 ++++++++++ SkyBot/Config.cs | 3 +- SkyBot/Helpers/MessageHelper.cs | 24 +++++++ SkyBot/Helpers/PermissionHelper.cs | 8 +++ SkyBot/Helpers/ValourUsercountHelper.cs | 32 +++++++++ SkyBot/Models/CommandContext.cs | 18 +++++ SkyBot/Models/ICommand.cs | 14 ++++ SkyBot/Services/BotService.cs | 4 ++ SkyBot/Services/ChannelService.cs | 23 +++---- SkyBot/Services/Messages/Create.cs | 38 +++++----- SkyBot/SkyBot.cs | 4 -- SkyBot/SkyBot.csproj | 1 + 27 files changed, 672 insertions(+), 65 deletions(-) create mode 100644 SkyBot/Commands/CommandRegistry.cs create mode 100644 SkyBot/Commands/CommandTemplate.cs create mode 100644 SkyBot/Commands/Dev/Test.cs create mode 100644 SkyBot/Commands/Fun/Echo.cs delete mode 100644 SkyBot/Commands/HelpCommand.cs create mode 100644 SkyBot/Commands/Info/Devcentral.cs create mode 100644 SkyBot/Commands/Info/Help.cs create mode 100644 SkyBot/Commands/Info/JoinSite.cs create mode 100644 SkyBot/Commands/Info/Minecraft.cs create mode 100644 SkyBot/Commands/Info/Source.cs create mode 100644 SkyBot/Commands/Info/Suggest.cs create mode 100644 SkyBot/Commands/Info/SwaggerAPI.cs create mode 100644 SkyBot/Commands/Info/UserCount.cs create mode 100644 SkyBot/Commands/Info/Version.cs create mode 100644 SkyBot/Commands/Mod/Ban.cs create mode 100644 SkyBot/Commands/Mod/Kick.cs create mode 100644 SkyBot/Helpers/MessageHelper.cs create mode 100644 SkyBot/Helpers/ValourUsercountHelper.cs create mode 100644 SkyBot/Models/CommandContext.cs create mode 100644 SkyBot/Models/ICommand.cs diff --git a/SkyBot/Commands/CommandRegistry.cs b/SkyBot/Commands/CommandRegistry.cs new file mode 100644 index 0000000..59650ff --- /dev/null +++ b/SkyBot/Commands/CommandRegistry.cs @@ -0,0 +1,33 @@ +using SkyBot.Models; + +namespace SkyBot.Commands +{ + public static class CommandRegistry + { + public static readonly Dictionary Commands = new(); + public static readonly Dictionary> Sections = new(); + + static CommandRegistry() + { + var allCommands = 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 allCommands) + { + Commands[cmd.Name.ToLower()] = cmd; + foreach (var alias in cmd.Aliases) + { + Commands[alias.ToLower()] = cmd; + } + + Sections = Commands.Values + .Distinct() + .GroupBy(c => c.Section.ToLower()) + .ToDictionary(g => g.Key, g => g.ToList()); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/CommandTemplate.cs b/SkyBot/Commands/CommandTemplate.cs new file mode 100644 index 0000000..d725003 --- /dev/null +++ b/SkyBot/Commands/CommandTemplate.cs @@ -0,0 +1,29 @@ +using System.Collections.Concurrent; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class CommandTemplate : ICommand + { + public string Name => "template"; + public string[] Aliases => []; + public string Description => ""; + public string Section => "template"; + public string Usage => ""; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + string message = $""; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Dev/Test.cs b/SkyBot/Commands/Dev/Test.cs new file mode 100644 index 0000000..abf2287 --- /dev/null +++ b/SkyBot/Commands/Dev/Test.cs @@ -0,0 +1,32 @@ +using System.Collections.Concurrent; +using SkyBot.Helpers; +using SkyBot.Models; +using Valour.Sdk.Client; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class Test : ICommand + { + public string Name => "test"; + public string[] Aliases => []; + public string Description => "Just a test command"; + public string Section => "Dev"; + public string Usage => "test"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + ValourClient client = ctx.Client; + PlanetMember member = ctx.Member; + Message message = ctx.Message; + Planet planet = ctx.Planet; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, "This is a test message"); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Fun/Echo.cs b/SkyBot/Commands/Fun/Echo.cs new file mode 100644 index 0000000..c680f76 --- /dev/null +++ b/SkyBot/Commands/Fun/Echo.cs @@ -0,0 +1,42 @@ +using System.Collections.Concurrent; +using System.Net.NetworkInformation; +using SkyBot.Helpers; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class Echo : ICommand + { + public string Name => "echo"; + public string[] Aliases => []; + public string Description => "Echos what you said through the bot."; + public string Section => "Fun"; + public string Usage => "echo "; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + String[] args = ctx.Args; + Message message = ctx.Message; + + string reply = string.Join(" ", args); + + if (channelCache.TryGetValue(channelId, out var channel)) + { + if (string.IsNullOrWhiteSpace(reply)) await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Enter a message to echo."); + + reply = $"{member.Name} » {reply}"; + + if (reply.Length > 2048) + { + reply = reply.Substring(0, 2048); + } + + await MessageHelper.ReplyAsync(ctx, channel, reply); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/HelpCommand.cs b/SkyBot/Commands/HelpCommand.cs deleted file mode 100644 index 48cc518..0000000 --- a/SkyBot/Commands/HelpCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Concurrent; -using SkyBot.Helpers; -using Valour.Sdk.Models; - -namespace SkyBot.Commands -{ - public static class HelpCommand - { - public static async Task Execute(ConcurrentDictionary channelCache, long channelId, String prefix, PlanetMember member) - { - string helpMessage = $@"**Skybot Commands**: - - `s/echo ` - Echos text into the chat - - `s/suggest` - Shares the suggestions link - - `s/source` - Sends link for the source code - - `s/joincode` - Sends a link to a github that you can use to make your bot join your planet. - - `s/joinsite` - Sends a link to a website that you can use to make yout bot join your planet. - - `s/api|swagger` - Sends a link to the Swagger API - - `s/cmds|help` - Shows this list - - `s/usercount` - Shows the user count of Valour - - `s/devcentral` - Sends the invite link to the Dev Central Planet - - `s/mc` - Sends Unofficial ValourSMP IPs - "; - - if (channelCache.TryGetValue(channelId, out var channel)) - { - await channel.SendMessageAsync($"{MentionHelper.Mention(member)}\n{helpMessage}"); - } - } - } -} \ No newline at end of file diff --git a/SkyBot/Commands/Info/Devcentral.cs b/SkyBot/Commands/Info/Devcentral.cs new file mode 100644 index 0000000..d0c5556 --- /dev/null +++ b/SkyBot/Commands/Info/Devcentral.cs @@ -0,0 +1,29 @@ +using System.Collections.Concurrent; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class Devcentral : ICommand + { + public string Name => "devcentral"; + public string[] Aliases => ["dev"]; + public string Description => "Sends an invite link to the Dev Central Planet."; + public string Section => "Info"; + public string Usage => "devcentral|dev"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + string message = $"you can join the Dev Central (ID: 42439954653511681) planet here: https://app.valour.gg/I/k2tz9c4i"; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Info/Help.cs b/SkyBot/Commands/Info/Help.cs new file mode 100644 index 0000000..340ac63 --- /dev/null +++ b/SkyBot/Commands/Info/Help.cs @@ -0,0 +1,92 @@ +using System.Collections.Concurrent; +using System.Text; +using SkyBot.Helpers; +using SkyBot.Models; +using Valour.Sdk.Models; +using Valour.Shared.Authorization; + +namespace SkyBot.Commands +{ + public class Help : ICommand + { + public string Name => "help"; + public string[] Aliases => ["h"]; + public string Description => "Shows all the commands and their descriptions."; + public string Section => "Info"; + public string Usage => "help|h [section] [page]"; + private const int PageSize = 5; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + string[] args = ctx.Args; + PlanetMember member = ctx.Member; + + bool isOwner = await PermissionHelper.IsOwner(member); + + if (!channelCache.TryGetValue(channelId, out var channel)) return; + + // Show all sections. + if (args.Length == 0) + { + var sb = new StringBuilder(); + sb.AppendLine("**Available Categories**"); + foreach (var section in CommandRegistry.Sections.Keys) + { + if (section == "template") continue; + if (section == "dev" && !isOwner) continue; + if (section == "mod" && !PermissionHelper.HasPermAsync(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles]).Result) continue; + sb.AppendLine($"- `{section.ToTitleCase()}` ({CommandRegistry.Sections[section].Count})"); + } + sb.AppendLine($"\nUse `{Config.Prefix}help ` to see commands in a category."); + await MessageHelper.ReplyAsync(ctx, channel, sb.ToString()); + return; + } + + // section [page] + string sectionName = args[0].ToLower(); + if (!CommandRegistry.Sections.TryGetValue(sectionName, out var commands)) + { + await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`."); + return; + } + + if (sectionName == "dev" && !isOwner) + { + await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`."); + return; + } + + if (sectionName == "mod" && !PermissionHelper.HasPermAsync(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles]).Result) + { + await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`."); + return; + } + + int page = 1; + if (args.Length >= 2 && int.TryParse(args[1], out int parsedPage)) + { + page = parsedPage; + } + + int totalPages = (int)Math.Ceiling(commands.Count / (double)PageSize); + page = Math.Clamp(page, 1, totalPages); + + var pageCommands = commands.Skip((page - 1) * PageSize).Take(PageSize); + + var sb2 = new StringBuilder(); + sb2.AppendLine($"**{sectionName.ToTitleCase()} commands** (Page {page}/{totalPages}):"); + foreach (var cmd in pageCommands) + { + var name = cmd.Aliases.Length > 0 + ? $"{cmd.Name}|{string.Join("|", cmd.Aliases)}" + : cmd.Name; + sb2.AppendLine($"`{Config.Prefix}{name}` - {cmd.Description}"); + } + sb2.AppendLine($"\nUse `{Config.Prefix}help {sectionName} ` to see more."); + + await MessageHelper.ReplyAsync(ctx, channel, sb2.ToString()); + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Info/JoinSite.cs b/SkyBot/Commands/Info/JoinSite.cs new file mode 100644 index 0000000..70bc667 --- /dev/null +++ b/SkyBot/Commands/Info/JoinSite.cs @@ -0,0 +1,29 @@ +using System.Collections.Concurrent; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class JoinSite : ICommand + { + public string Name => "joinsite"; + public string[] Aliases => []; + public string Description => "Links to a site to help your bots join a planet."; + public string Section => "Info"; + public string Usage => "joinsite"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + string message = $"You can use this website to easily add your bot to a planet: https://skyjoshua.xyz/planetjoiner"; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Info/Minecraft.cs b/SkyBot/Commands/Info/Minecraft.cs new file mode 100644 index 0000000..af1b987 --- /dev/null +++ b/SkyBot/Commands/Info/Minecraft.cs @@ -0,0 +1,31 @@ +using System.Collections.Concurrent; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class Minecraft : ICommand + { + public string Name => "minecraft"; + public string[] Aliases => ["mc"]; + public string Description => "Sends the Unofficial ValourSMP IPs"; + public string Section => "Info"; + public string Usage => "minecraft|mc"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + string message = @$"you can join the Unofficial ValourSMP Minecraft Server by using this ip: + Java: `valour.sxsc.xyz`, Bedrock: `valourbr.sxsc.xyz` Both with the default ports. + Cool features can be found here: https://sxsc.xyz/servers/valour/"; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Info/Source.cs b/SkyBot/Commands/Info/Source.cs new file mode 100644 index 0000000..ca77c87 --- /dev/null +++ b/SkyBot/Commands/Info/Source.cs @@ -0,0 +1,29 @@ +using System.Collections.Concurrent; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class Source : ICommand + { + public string Name => "source"; + public string[] Aliases => ["src"]; + public string Description => "Shows the source code for this bot."; + public string Section => "Info"; + public string Usage => "source"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + string message = $"You can find my source code here: {Config.SourceLink}"; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Info/Suggest.cs b/SkyBot/Commands/Info/Suggest.cs new file mode 100644 index 0000000..e152666 --- /dev/null +++ b/SkyBot/Commands/Info/Suggest.cs @@ -0,0 +1,29 @@ +using System.Collections.Concurrent; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class Suggest : ICommand + { + public string Name => "suggest"; + public string[] Aliases => []; + public string Description => "Shows the source code for this bot."; + public string Section => "Info"; + public string Usage => "source"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + string message = $"You can suggest a command to be added here: https://docs.google.com/spreadsheets/d/1CzcpLAuMiPL_RODrZ5x25cPj8yE-rR3mEnqrd_2Fbmk"; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Info/SwaggerAPI.cs b/SkyBot/Commands/Info/SwaggerAPI.cs new file mode 100644 index 0000000..34ffa92 --- /dev/null +++ b/SkyBot/Commands/Info/SwaggerAPI.cs @@ -0,0 +1,29 @@ +using System.Collections.Concurrent; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class SwaggerAPI : ICommand + { + public string Name => "swagger"; + public string[] Aliases => ["api"]; + public string Description => "Sends a link to the Valour.gg Swagger API."; + public string Section => "Info"; + public string Usage => "swagger|api"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + string message = $"Here is a link to the Swagger API: https://api.valour.gg/swagger"; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Info/UserCount.cs b/SkyBot/Commands/Info/UserCount.cs new file mode 100644 index 0000000..92e0421 --- /dev/null +++ b/SkyBot/Commands/Info/UserCount.cs @@ -0,0 +1,31 @@ +using System.Collections.Concurrent; +using SkyBot.Helpers; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class UserCount : ICommand + { + public string Name => "usercount"; + public string[] Aliases => ["users"]; + public string Description => "Shows the user count of Valour."; + public string Section => "Info"; + public string Usage => "usercount|users"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + string message = @$"Current Valour user count is: {ValourUsercountHelper.ValourUsercount:N0} + You can see a graph of the user count here: /meow"; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Info/Version.cs b/SkyBot/Commands/Info/Version.cs new file mode 100644 index 0000000..f097293 --- /dev/null +++ b/SkyBot/Commands/Info/Version.cs @@ -0,0 +1,30 @@ +using System.Collections.Concurrent; +using SkyBot.Models; +using Valour.Sdk.Models; + +namespace SkyBot.Commands +{ + public class Version : ICommand + { + public string Name => "version"; + public string[] Aliases => []; + public string Description => "Shows the current version of the Bot and Valour."; + public string Section => "Info"; + public string Usage => ""; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + string message = @$"Bot Version: {typeof(Version).Assembly.GetName().Version} + Valour Version: {typeof(Channel).Assembly.GetName().Version}"; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Mod/Ban.cs b/SkyBot/Commands/Mod/Ban.cs new file mode 100644 index 0000000..a776b6c --- /dev/null +++ b/SkyBot/Commands/Mod/Ban.cs @@ -0,0 +1,36 @@ +using System.Collections.Concurrent; +using SkyBot.Helpers; +using SkyBot.Models; +using Valour.Sdk.Models; +using Valour.Shared.Authorization; + +namespace SkyBot.Commands +{ + public class Ban : ICommand + { + public string Name => "ban"; + public string[] Aliases => []; + public string Description => "Bans a user from the planet."; + public string Section => "mod"; + public string Usage => "ban [reason]"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + if (!PermissionHelper.HasPermAsync(member, [PlanetPermissions.Ban]).Result) + { + await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command."); + return; + } + + string message = $"Work in progress..."; + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Mod/Kick.cs b/SkyBot/Commands/Mod/Kick.cs new file mode 100644 index 0000000..67becc3 --- /dev/null +++ b/SkyBot/Commands/Mod/Kick.cs @@ -0,0 +1,37 @@ +using System.Collections.Concurrent; +using SkyBot.Helpers; +using SkyBot.Models; +using Valour.Sdk.Models; +using Valour.Shared.Authorization; + +namespace SkyBot.Commands +{ + public class Kick : ICommand + { + public string Name => "kick"; + public string[] Aliases => []; + public string Description => "Kicks a user from the planet."; + public string Section => "mod"; + public string Usage => "kick [reason]"; + + public async Task Execute(CommandContext ctx) + { + ConcurrentDictionary channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + PlanetMember member = ctx.Member; + + if (channelCache.TryGetValue(channelId, out var channel)) + { + if (!PermissionHelper.HasPermAsync(member, [PlanetPermissions.Kick]).Result) + { + await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command."); + return; + } + + string message = $"Work in progress..."; + + await MessageHelper.ReplyAsync(ctx, channel, message); + } + } + } +} \ No newline at end of file diff --git a/SkyBot/Config.cs b/SkyBot/Config.cs index b2b203b..74ffd81 100644 --- a/SkyBot/Config.cs +++ b/SkyBot/Config.cs @@ -1,9 +1,10 @@ -namespace Skybot +namespace SkyBot { public static class Config { public static readonly long OwnerId = 15652354820931584; public static readonly string Prefix = "sd/"; + public static readonly string SourceLink = "https://github.com/SkyJoshua/SkyBot"; } } \ No newline at end of file diff --git a/SkyBot/Helpers/MessageHelper.cs b/SkyBot/Helpers/MessageHelper.cs new file mode 100644 index 0000000..2d4de9c --- /dev/null +++ b/SkyBot/Helpers/MessageHelper.cs @@ -0,0 +1,24 @@ +using SkyBot.Models; +using Valour.Sdk.Models; + +public static class MessageHelper +{ + public static async Task ReplyAsync(CommandContext ctx, Channel channel, string content) + { + long? replyToId = ctx.Message.ReplyToId.HasValue ? ctx.Message.ReplyToId : ctx.Message.Id; + + 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() + }; + await ctx.Client.MessageService.SendMessage(msg); + } + + public static string ToTitleCase(this string str) => System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str); +} \ No newline at end of file diff --git a/SkyBot/Helpers/PermissionHelper.cs b/SkyBot/Helpers/PermissionHelper.cs index 5adb73f..a567b74 100644 --- a/SkyBot/Helpers/PermissionHelper.cs +++ b/SkyBot/Helpers/PermissionHelper.cs @@ -15,5 +15,13 @@ namespace SkyBot.Helpers ? permissions.All(permission => member.HasPermission(permission)) : permissions.Any(permission => member.HasPermission(permission)); } + + public static async Task IsOwner(PlanetMember member) + { + + if (member == null) return false; + if (member.UserId == Config.OwnerId) return true; + return false; + } } } \ No newline at end of file diff --git a/SkyBot/Helpers/ValourUsercountHelper.cs b/SkyBot/Helpers/ValourUsercountHelper.cs new file mode 100644 index 0000000..514ae6c --- /dev/null +++ b/SkyBot/Helpers/ValourUsercountHelper.cs @@ -0,0 +1,32 @@ +using System.Text.Json; + +namespace SkyBot.Helpers +{ + public static class ValourUsercountHelper { + private static readonly HttpClient _http = new HttpClient(); + private static long _valourUsercount; + public static long ValourUsercount => _valourUsercount; + public static async Task UpdateUsercount() + { + try + { + var response = await _http.GetStringAsync("https://api.valour.gg/api/users/count"); + + _valourUsercount = JsonSerializer.Deserialize(response); + + Console.WriteLine($"Valour user count updated: {_valourUsercount}"); + } catch (Exception ex) + { + Console.WriteLine($"Failed to update valour user count: {ex.Message}"); + } + } + + public static void StartUpdater() + { + var timer = new System.Timers.Timer(300_000); + timer.Elapsed += async (_, _) => await UpdateUsercount(); + timer.AutoReset = true; + timer.Start(); + } + } +} \ No newline at end of file diff --git a/SkyBot/Models/CommandContext.cs b/SkyBot/Models/CommandContext.cs new file mode 100644 index 0000000..a9cce9e --- /dev/null +++ b/SkyBot/Models/CommandContext.cs @@ -0,0 +1,18 @@ +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 ChannelCache { get; set; } + public required PlanetMember Member { get; set; } + public required Message Message { get; set; } + public required Planet Planet { get; set; } + public required long ChannelId { get; set; } + public required string[] Args { get; set; } + + } +} \ No newline at end of file diff --git a/SkyBot/Models/ICommand.cs b/SkyBot/Models/ICommand.cs new file mode 100644 index 0000000..1abeb7d --- /dev/null +++ b/SkyBot/Models/ICommand.cs @@ -0,0 +1,14 @@ +using System.Collections.Concurrent; + +namespace SkyBot.Models +{ + public interface ICommand + { + string Name { get; } + string[] Aliases { get; } + string Description { get; } + string Section { get; } + string Usage { get; } + Task Execute(CommandContext ctx); + } +} \ No newline at end of file diff --git a/SkyBot/Services/BotService.cs b/SkyBot/Services/BotService.cs index 279cdc4..3162ae0 100644 --- a/SkyBot/Services/BotService.cs +++ b/SkyBot/Services/BotService.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using DotNetEnv; +using SkyBot.Helpers; using Valour.Sdk.Client; using Valour.Sdk.Models; @@ -21,6 +22,9 @@ namespace SkyBot.Services if (!loginResult.Success) {Console.WriteLine($"Login Failed: {loginResult.Message}"); return;} Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})"); + await ValourUsercountHelper.UpdateUsercount(); + ValourUsercountHelper.StartUpdater(); + await PlanetService.InitializePlanetsAsync(client, channelCache, initalizedPlanets); client.PlanetService.JoinedPlanetsUpdated += async () => { diff --git a/SkyBot/Services/ChannelService.cs b/SkyBot/Services/ChannelService.cs index 630df4e..5547048 100644 --- a/SkyBot/Services/ChannelService.cs +++ b/SkyBot/Services/ChannelService.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Security.Cryptography.X509Certificates; using Valour.Sdk.Models; using Valour.Shared.Models; @@ -6,32 +7,30 @@ namespace SkyBot.Services { public static class ChannelService { - private static readonly SemaphoreSlim _channelSemaphore = new SemaphoreSlim(3, 3); public static async Task InitializeChannelsAsync( ConcurrentDictionary channelCache, Planet planet) { - var tasks = planet.Channels.Select(async channel => + foreach (var channel in planet.Channels) { channelCache[channel.Id] = channel; - if (channel.ChannelType == ChannelTypeEnum.PlanetChat) - { - await _channelSemaphore.WaitAsync(); + } + + _ = 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})"); - await Task.Delay(250); - } - finally + } catch (Exception ex) { - _channelSemaphore.Release(); + Console.WriteLine($"Error opening realtime for {channel.Id}: {ex.Message}"); } - } - }); - await Task.WhenAll(tasks); + Console.WriteLine($"All channels opened for {planet.Name}."); + }); } } } \ No newline at end of file diff --git a/SkyBot/Services/Messages/Create.cs b/SkyBot/Services/Messages/Create.cs index 2345934..e038458 100644 --- a/SkyBot/Services/Messages/Create.cs +++ b/SkyBot/Services/Messages/Create.cs @@ -1,7 +1,7 @@ using System.Collections.Concurrent; -using Skybot; using SkyBot.Commands; using SkyBot.Helpers; +using SkyBot.Models; using Valour.Sdk.Client; using Valour.Sdk.Models; @@ -15,36 +15,38 @@ namespace SkyBot.Services.Messages Message message ) { - string prefix = Config.Prefix; - if (message.AuthorUserId == client.Me.Id) return; - + string prefix = Config.Prefix; string content = message.Content ?? ""; if (string.IsNullOrWhiteSpace(content)) return; - if (!content.StartsWith(prefix)) return; + if (!content.ToLower().StartsWith(prefix)) return; long channelId = message.ChannelId; - PlanetMember member = await message.FetchAuthorMemberAsync(); - var parts = content.Substring(prefix.Length).Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) return; string command = parts[0].ToLower(); string[] args = parts[1..]; - switch (command) + if (CommandRegistry.Commands.TryGetValue(command, out var handler)) { - case "help": - await HelpCommand.Execute(channelCache, channelId, prefix, member); - break; - - default: - if (channelCache.TryGetValue(channelId, out var channel)) - { - await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Unknown command."); - } - break; + await handler.Execute(new CommandContext + { + ChannelCache = channelCache, + ChannelId = channelId, + Member = member, + Planet = message.Planet, + Args = args, + Message = message, + Client = client + }); + } else + { + if (channelCache.TryGetValue(channelId, out var channel)) + { + await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Unknown command."); + } } } } diff --git a/SkyBot/SkyBot.cs b/SkyBot/SkyBot.cs index 4326c29..890299e 100644 --- a/SkyBot/SkyBot.cs +++ b/SkyBot/SkyBot.cs @@ -35,10 +35,6 @@ namespace SkyBot Console.WriteLine("Ready and listening..."); await Task.Delay(Timeout.Infinite); - } catch (InvalidOperationException ex) when (ex.Message.Contains("concurrent update")) - { - Console.WriteLine("Concurrent update detected, restarting..."); - await Task.Delay(1000); } catch (Exception ex) { Console.WriteLine($"Fatal error: {ex.Message}"); diff --git a/SkyBot/SkyBot.csproj b/SkyBot/SkyBot.csproj index ca2272e..eb93c8e 100644 --- a/SkyBot/SkyBot.csproj +++ b/SkyBot/SkyBot.csproj @@ -5,6 +5,7 @@ net10.0 enable enable + 0.2.0.0