diff --git a/SkyBot/Commands/Dev/Test.cs b/SkyBot/Commands/Dev/Edit.cs similarity index 96% rename from SkyBot/Commands/Dev/Test.cs rename to SkyBot/Commands/Dev/Edit.cs index 85a9239..02afe7c 100644 --- a/SkyBot/Commands/Dev/Test.cs +++ b/SkyBot/Commands/Dev/Edit.cs @@ -7,7 +7,7 @@ using Valour.Shared; namespace SkyBot.Commands { - public class Test : ICommand + public class Edit : ICommand { public string Name => "edit"; public string[] Aliases => []; @@ -29,6 +29,7 @@ namespace SkyBot.Commands if(!PermissionHelper.IsOwner(member)) { await MessageHelper.ReplyAsync(ctx, channel, "This is a Dev only command."); + return; } if (message.ReplyToId == null) diff --git a/SkyBot/Commands/Mod/SetWelcome.cs b/SkyBot/Commands/Mod/SetWelcome.cs new file mode 100644 index 0000000..c36b2ee --- /dev/null +++ b/SkyBot/Commands/Mod/SetWelcome.cs @@ -0,0 +1,103 @@ +using System.Collections.Concurrent; +using SkyBot.Helpers; +using SkyBot.Models; +using SkyBot.Services; +using Valour.Sdk.Models; +using Valour.Shared.Authorization; +using Valour.Shared.Models; + +namespace SkyBot.Commands +{ + public class SetWelcome : ICommand + { + public string Name => "setwelcome"; + public string[] Aliases => []; + public string Description => "Sets the welcome channel, message or active."; + public string Section => "Mod"; + public string Usage => "set channelCache = ctx.ChannelCache; + long channelId = ctx.ChannelId; + Message message = ctx.Message; + PlanetMember member = ctx.Member; + Planet planet = ctx.Planet; + string[] args = ctx.Args; + + if (!channelCache.TryGetValue(channelId, out var channel)) return; + + if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Manage]) && !PermissionHelper.IsOwner(member)) + { + await MessageHelper.ReplyAsync(ctx, channel, "You don't have permission to use this command."); + return; + } + + if (args.Length == 0) + { + await MessageHelper.ReplyAsync(ctx, channel, "Please specify `channel` or `message`."); + return; + } + + switch (args[0].ToLower()) + { + case "channel": + case "c": + long targetChannelId; + if (message.Mentions != null && message.Mentions.Any(m => m.Type == MentionType.Channel)) {targetChannelId = message.Mentions.First(m => m.Type == MentionType.Channel).TargetId;} + else if (args.Length > 1 && long.TryParse(args[1], out long parsedChannelId)) {targetChannelId = parsedChannelId;} + else {targetChannelId = channelId;} + + if (!channelCache.ContainsKey(targetChannelId)) {await MessageHelper.ReplyAsync(ctx, channel, "Could not find that channel."); return;} + + await WelcomeService.SetWelcomeChannel(planet.Id, targetChannelId); + await MessageHelper.ReplyAsync(ctx, channel, $"Welcome channel set to «@c-{targetChannelId}»."); + break; + + case "message": + case "m": + if (args.Length < 2) + { + await MessageHelper.ReplyAsync(ctx, channel, "Please provide a message. Valid variables: {username} {nickname} {fulluser} {mention} {id}"); + return; + } + string msg = string.Join(" ", args[1..]); + await WelcomeService.SetWelcomeMessage( planet.Id, msg); + await MessageHelper.ReplyAsync(ctx, channel, $"Welcome message set to: `{msg}`"); + break; + + case "active": + case "a": + if (args.Length < 2) + { + await MessageHelper.ReplyAsync(ctx, channel, "Please provide a value. Use `true`, `false`, or `toggle`."); + return; + } + string value = args[1].ToLower(); + if (value != "toggle" && value != "true" && value != "false") + { + await MessageHelper.ReplyAsync(ctx, channel, "Invalid value. Use `true`, `false`, `toggle`"); + return; + } + + if (value == "toggle") + { + var toggle = await WelcomeService.SetActive(planet.Id); + await MessageHelper.ReplyAsync(ctx, channel, toggle.Value ? "Welcome messages enabled." : "Welcome messages disabled."); + return; + } + + bool.TryParse(value, out var active); + + await WelcomeService.SetActive(planet.Id, active); + await MessageHelper.ReplyAsync(ctx, channel, active ? "Welcome messages enabled." : "Welcome messages disabled."); + break; + + default: + await MessageHelper.ReplyAsync(ctx, channel, "Invalid option. Use `channel`, `message` or `active`."); + break; + } + + } + } +} \ No newline at end of file diff --git a/SkyBot/Models/DatabaseHelper.cs b/SkyBot/Models/DatabaseHelper.cs new file mode 100644 index 0000000..32864ba --- /dev/null +++ b/SkyBot/Models/DatabaseHelper.cs @@ -0,0 +1,32 @@ +using Microsoft.Data.Sqlite; + +namespace SkyBot.Helpers +{ + public static class DatabaseHelper + { + private const string ConnectionString = "Data Source=database.db"; + + public static SqliteConnection GetConnection() + { + SqliteConnection connection = new SqliteConnection(ConnectionString); + connection.Open(); + return connection; + } + + public static async Task InitializeAsync() + { + using SqliteConnection connection = GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = @" + CREATE TABLE IF NOT EXISTS WelcomeConfigs ( + PlanetId INTEGER PRIMARY KEY, + ChannelId INTEGER NOT NULL DEFAULT 0, + Message TEXT NOT NULL DEFAULT 'Welcome to the planet, {username}!', + Active INTEGER NOT NULL DEFAULT 0 + ); + "; + await cmd.ExecuteNonQueryAsync(); + Console.WriteLine("Database initialized."); + } + } +} \ No newline at end of file diff --git a/SkyBot/Models/WelcomeConfig.cs b/SkyBot/Models/WelcomeConfig.cs new file mode 100644 index 0000000..b31cb78 --- /dev/null +++ b/SkyBot/Models/WelcomeConfig.cs @@ -0,0 +1,7 @@ +public class WelcomeConfig +{ + public long PlanetId { get; set; } + public long ChannelId { get; set; } + public string Message { get; set; } = "Welcome to the planet, {username}!"; + public bool Active { get; set; } = false; +} \ No newline at end of file diff --git a/SkyBot/Services/Messages/Create.cs b/SkyBot/Services/Messages/Create.cs index fe549cc..4fed14d 100644 --- a/SkyBot/Services/Messages/Create.cs +++ b/SkyBot/Services/Messages/Create.cs @@ -5,10 +5,14 @@ using SkyBot.Models; using Valour.Sdk.Client; using Valour.Sdk.Models; + + namespace SkyBot.Services.Messages { public static class Create { + private static readonly ConcurrentDictionary _cooldowns = new(); + private static readonly TimeSpan _cooldown = TimeSpan.FromSeconds(2); public static async Task MessageAsync( ValourClient client, ConcurrentDictionary channelCache, @@ -40,6 +44,11 @@ namespace SkyBot.Services.Messages Client = client }; + if (_cooldowns.TryGetValue(message.AuthorUserId, out var lastUsed) && DateTime.UtcNow - lastUsed < _cooldown) + return; + + _cooldowns[message.AuthorUserId] = DateTime.UtcNow; + if (CommandRegistry.Commands.TryGetValue(command, out var handler)) { await handler.Execute(ctx); diff --git a/SkyBot/Services/PlanetService.cs b/SkyBot/Services/PlanetService.cs index 911fccf..74c3b7d 100644 --- a/SkyBot/Services/PlanetService.cs +++ b/SkyBot/Services/PlanetService.cs @@ -1,14 +1,13 @@ using System.Collections.Concurrent; -using SkyBot.Services; using Valour.Sdk.Client; +using Valour.Sdk.ModelLogic; using Valour.Sdk.Models; -using Valour.Sdk.Models.Messages.Embeds; - namespace SkyBot.Services { public static class PlanetService { + private static readonly DateTime _startTime = DateTime.UtcNow; public static async Task InitializePlanetsAsync( ValourClient client, ConcurrentDictionary channelCache, @@ -19,15 +18,27 @@ namespace SkyBot.Services .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) => { + planet.Channels.Changed += async _ => + { await ChannelService.InitializeChannelsAsync(channelCache, planet); }; - }); + planet.Members.Changed += async memberEvent => + { + if ((DateTime.UtcNow - _startTime).TotalSeconds < 10) return; + if (memberEvent is ModelAddedEvent addedEvent) + { + await WelcomeService.OnMemberJoin(addedEvent.Model, channelCache); + } + }; + + initializedPlanets.TryAdd(planet.Id, true); + }); await Task.WhenAll(tasks); } } diff --git a/SkyBot/Services/WelcomeService.cs b/SkyBot/Services/WelcomeService.cs new file mode 100644 index 0000000..db8df7e --- /dev/null +++ b/SkyBot/Services/WelcomeService.cs @@ -0,0 +1,137 @@ +using System.Collections.Concurrent; +using Microsoft.Data.Sqlite; +using SkyBot.Helpers; +using Valour.Sdk.Models; + +namespace SkyBot.Services +{ + public static class WelcomeService + { + private static readonly ConcurrentDictionary _cache = new(); + + public static async Task InitializeAsync() + { + using SqliteConnection connection = DatabaseHelper.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM WelcomeConfigs"; + using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + var config = new WelcomeConfig + { + PlanetId = (long)reader["PlanetId"], + ChannelId = (long)reader["ChannelId"], + Message = (string)reader["Message"], + Active = (long)reader["Active"] == 1 + }; + _cache[config.PlanetId] = config; + } + Console.WriteLine("WelcomeService initialized."); + Console.WriteLine($"Loaded {_cache.Count} welcome configs from database."); + } + + public static async Task OnMemberJoin(PlanetMember member, ConcurrentDictionary channelCache) + { + if (!_cache.TryGetValue(member.PlanetId, out var config)) { Console.WriteLine("No config found"); return; } + if (!config.Active) { Console.WriteLine("Not active"); return; } + + Channel? channel = null; + + if (config.ChannelId != 0 && channelCache.TryGetValue(config.ChannelId, out var configChannel)) + { + channel = configChannel; + } + else + { + channel = channelCache.Values.FirstOrDefault(c => c.PlanetId == member.PlanetId && c.IsDefault); + } + + if (channel == null) { Console.WriteLine("No channel found"); return; } + + + string message = config.Message + .Replace("{username}", member.Name) + .Replace("{fulluser}", member.User.NameAndTag) + .Replace("{nickname}", string.IsNullOrWhiteSpace(member.Nickname) ? member.Name : member.Nickname) + .Replace("{mention}", MessageHelper.Mention(member)) + .Replace("{id}", $"{member.Id}"); + + await channel.SendMessageAsync(message); + } + + public static async Task SetWelcomeChannel(long planetId, long channelId) + { + using SqliteConnection connection = DatabaseHelper.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = @" + INSERT INTO WelcomeConfigs (PlanetId, ChannelId) VALUES ($planetId, $channelId) + ON CONFLICT(PlanetId) DO UPDATE SET ChannelId = $channelId; + "; + cmd.Parameters.AddWithValue("$planetId", planetId); + cmd.Parameters.AddWithValue("$channelId", channelId); + await cmd.ExecuteNonQueryAsync(); + + if (_cache.TryGetValue(planetId, out var config)) + { + config.ChannelId = channelId; + } + else + { + _cache[planetId] = new WelcomeConfig{PlanetId = planetId, ChannelId = channelId}; + } + } + + public static async Task SetWelcomeMessage(long planetId, string message) + { + using SqliteConnection connection = DatabaseHelper.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = @" + INSERT INTO WelcomeConfigs (PlanetId, Message) VALUES ($planetId, $message) + ON CONFLICT(PlanetId) DO UPDATE SET Message = $message; + "; + cmd.Parameters.AddWithValue("$planetId", planetId); + cmd.Parameters.AddWithValue("$message", message); + await cmd.ExecuteNonQueryAsync(); + + if (_cache.TryGetValue(planetId, out var config)) + { + config.Message = message; + } + else + { + _cache[planetId] = new WelcomeConfig{PlanetId = planetId, Message = message}; + } + } + + public static async Task SetActive(long planetId, bool active) + { + using SqliteConnection connection = DatabaseHelper.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = @" + INSERT INTO WelcomeConfigs (PlanetId, Active) VALUES ($planetId, $active) + ON CONFLICT(PlanetId) DO UPDATE SET Active = $active; + "; + cmd.Parameters.AddWithValue("$planetId", planetId); + cmd.Parameters.AddWithValue("$active", active ? 1 : 0); + await cmd.ExecuteNonQueryAsync(); + + if (_cache.TryGetValue(planetId, out var config)) + { + config.Active = active; + } + else + { + _cache[planetId] = new WelcomeConfig{PlanetId = planetId, Active = active}; + } + } + + public static async Task SetActive(long planetId) + { + if (!_cache.TryGetValue(planetId, out var config)) return null; + + bool newActive = !config.Active; + await SetActive(planetId, newActive); + return newActive; + } + } +} \ No newline at end of file diff --git a/SkyBot/SkyBot.cs b/SkyBot/SkyBot.cs index 0d9138e..58fd5b7 100644 --- a/SkyBot/SkyBot.cs +++ b/SkyBot/SkyBot.cs @@ -2,6 +2,7 @@ using Valour.Sdk.Client; using Valour.Sdk.Models; using SkyBot.Services; using System.Collections.Concurrent; +using SkyBot.Helpers; namespace SkyBot { @@ -21,6 +22,8 @@ namespace SkyBot public async Task StartAsync() { StartTime = DateTime.UtcNow; + await DatabaseHelper.InitializeAsync(); + await WelcomeService.InitializeAsync(); await BotService.InitializeBotAsync(_client, _channelCache, _initializedPlanets); } } @@ -34,7 +37,6 @@ namespace SkyBot try { await new SkyBot().StartAsync(); - Console.WriteLine("Ready and listening..."); await Task.Delay(Timeout.Infinite); diff --git a/SkyBot/SkyBot.csproj b/SkyBot/SkyBot.csproj index eb93c8e..734b5d3 100644 --- a/SkyBot/SkyBot.csproj +++ b/SkyBot/SkyBot.csproj @@ -5,11 +5,12 @@ net10.0 enable enable - 0.2.0.0 + 0.2.1.0 +