From 459973b40ca1b03801e4a4e8f4e5474bcb9bf8b8 Mon Sep 17 00:00:00 2001 From: skyjoshua Date: Wed, 29 Apr 2026 03:06:12 +0100 Subject: [PATCH] Minor Update --- SkyBot/Commands/Dev/Planet.cs | 2 +- SkyBot/Commands/RP/Marriage.cs | 222 ++++++++++++++++++++++++ SkyBot/Commands/Utils/Accept.cs | 23 +++ SkyBot/Commands/Utils/Cancel.cs | 23 +++ SkyBot/Commands/Utils/Confirm.cs | 23 +++ SkyBot/Commands/Utils/Decline.cs | 23 +++ SkyBot/Helpers/MessageHelper.cs | 47 +++-- SkyBot/Helpers/PendingActionRegistry.cs | 97 +++++++++++ SkyBot/Models/MarriageModel.cs | 5 + SkyBot/Services/DatabaseService.cs | 36 ++++ SkyBot/Services/MarriageService.cs | 219 +++++++++++++++++++++++ SkyBot/SkyBot.cs | 3 + SkyBot/SkyBot.csproj | 7 +- 13 files changed, 701 insertions(+), 29 deletions(-) create mode 100644 SkyBot/Commands/RP/Marriage.cs create mode 100644 SkyBot/Commands/Utils/Accept.cs create mode 100644 SkyBot/Commands/Utils/Cancel.cs create mode 100644 SkyBot/Commands/Utils/Confirm.cs create mode 100644 SkyBot/Commands/Utils/Decline.cs create mode 100644 SkyBot/Helpers/PendingActionRegistry.cs create mode 100644 SkyBot/Models/MarriageModel.cs create mode 100644 SkyBot/Services/DatabaseService.cs create mode 100644 SkyBot/Services/MarriageService.cs diff --git a/SkyBot/Commands/Dev/Planet.cs b/SkyBot/Commands/Dev/Planet.cs index 3448a5c..3590c3a 100644 --- a/SkyBot/Commands/Dev/Planet.cs +++ b/SkyBot/Commands/Dev/Planet.cs @@ -12,7 +12,7 @@ namespace SkyBot.Commands public string[] Aliases => []; public string Description => "Planet Commands"; public string Category => "Dev"; - public string Usage => "planet "; + public string Usage => "planet "; public string[] SubCommands => ["join", "leave", "list"]; public async Task Execute(CommandContext ctx) diff --git a/SkyBot/Commands/RP/Marriage.cs b/SkyBot/Commands/RP/Marriage.cs new file mode 100644 index 0000000..a9e3554 --- /dev/null +++ b/SkyBot/Commands/RP/Marriage.cs @@ -0,0 +1,222 @@ +using SkyBot.Helpers; +using SkyBot.Models; +using SkyBot.Services; +namespace SkyBot.Commands +{ + public class Marriage : ICommand + { + public string Name => "marriage"; + public string[] Aliases => ["marry"]; + public string Description => "Marriage system — propose, check status, or divorce."; + public string Category => "RP"; + public string Usage => "marriage "; + public string[] SubCommands => ["propose", "status", "divorce", "force"]; + + public async Task Execute(CommandContext ctx) + { + switch (ctx.Args.ElementAtOrDefault(0)?.ToLower()) + { + case "propose": + await HandlePropose(ctx); + break; + case "force": + await HandleForce(ctx); + break; + case "divorce": + await HandleDivorce(ctx); + break; + case "status": + await HandleStatus(ctx); + break; + default: + await MessageHelper.ReplyAsync(ctx, $"Usage: `{Config.Prefix}marriage `"); + break; + } + } + + private static async Task HandlePropose(CommandContext ctx) + { + long propserId = ctx.Member.UserId; + string sender = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown"; + + long? targetUserId = null; + string? targetMention = null; + + if (ctx.Message.ReplyToId is not null) + { + var replied = await ctx.Message.FetchReplyMessageAsync(); + if (replied is null) return; + var author = await replied.FetchAuthorAsync(); + if (author is null) return; + targetUserId = author.Id; + targetMention = $"«@u-{author.Id}»"; + } + + if (targetUserId is null && ctx.Message.Mentions is not null && ctx.Message.Mentions.Any()) + { + long memberId = ctx.Message.Mentions.First().TargetId; + var mentioned = await ctx.Planet.FetchMemberAsync(memberId); + if (mentioned is null) return; + targetUserId = mentioned.UserId; + targetMention = $"«@u-{mentioned.UserId}»"; + } + + if (targetUserId is null) + { + await MessageHelper.ReplyAsync(ctx, $"Please reply to someone's message or mention them. Usage: `{Config.Prefix}marriage propose "); + return; + } + + var result = MarriageService.Propose(propserId, targetUserId.Value); + + switch (result) + { + case MarriageService.ProposeResult.SelfPropose: + await MessageHelper.ReplyAsync(ctx, "You can't propose to yourself."); + break; + case MarriageService.ProposeResult.AlreadyMarried: + await MessageHelper.ReplyAsync(ctx, $"You are already married, {sender}. You'd need to `{Config.Prefix}marriage divorce` first."); + break; + case MarriageService.ProposeResult.TargetAlreadyMarried: + await MessageHelper.ReplyAsync(ctx, $"{targetMention} is already married."); + break; + case MarriageService.ProposeResult.AlreadyProposed: + await MessageHelper.ReplyAsync(ctx, $"You've already proposed to {targetMention}! Waiting for thsir reponse"); + break; + case MarriageService.ProposeResult.Ok: + await MessageHelper.ReplyAsync(ctx, $"💍 {sender} gets down on one knee and proposes to {targetMention}\n{targetMention}, type `{Config.Prefix}accept` or `{Config.Prefix}decline`. This proposal expires in 5 minutes."); + break; + } + } + + private static async Task HandleDivorce(CommandContext ctx) + { + long userId = ctx.Member.UserId; + string name = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown"; + + long? partnerId = MarriageService.GetMarriage(userId)?.SpouseId; + if (partnerId is null) + { + await MessageHelper.ReplyAsync(ctx, $"You're not married, {name}."); + return; + } + + MarriageService.RequestDivorceConfirmation(userId); + await MessageHelper.ReplyAsync(ctx, $"Are you sure you want to divorce «@u-{partnerId}»?\nType `{Config.Prefix}confirm` within 60 seconds to confirm, or `{Config.Prefix}cancel` to cancel."); + } + + private static async Task HandleStatus(CommandContext ctx) + { + long userId = ctx.Member.UserId; + string name = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown"; + + if (ctx.Message.Mentions is not null && ctx.Message.Mentions.Any()) + { + long memberId = ctx.Message.Mentions.First().TargetId; + var mentioned = await ctx.Planet.FetchMemberAsync(memberId); + if (mentioned is null) return; + userId = mentioned.UserId; + name = mentioned.Nickname ?? mentioned.User?.Name ?? "Unknown"; + } + + MarriageModel? marriage = MarriageService.GetMarriage(userId); + + if (marriage is null) + { + await MessageHelper.ReplyAsync(ctx, $"{name} is not currently married."); + } + else + { + long partnerId = marriage.SpouseId; + DateTimeOffset dt = DateTimeOffset.FromUnixTimeMilliseconds(marriage.MarriedAt); + string marriedAt = $"{dt.OrdinalDay()} {dt:MMMM yyyy HH:mm} UTC"; + await MessageHelper.ReplyAsync(ctx, $"{name} is married to «@u-{partnerId}»!\nThey got married: {marriedAt}"); + } + } + + private static async Task HandleForce(CommandContext ctx) + { + if (!PermissionHelper.IsOwner(ctx.Member)) + { + await MessageHelper.ReplyAsync(ctx, "You don't have permission to use this command."); + return; + } + + string action = ctx.Args.ElementAtOrDefault(1)?.ToLower() ?? ""; + var mentions = ctx.Message.Mentions ?? []; + + switch (action) + { + case "marry": + { + if (mentions.Count < 2) + { + await MessageHelper.ReplyAsync(ctx, $"Please mention two users. Usage: `{Config.Prefix}marriage force marry @user1 @user2`"); + return; + } + + var member1 = await ctx.Planet.FetchMemberAsync(mentions[0].TargetId); + var member2 = await ctx.Planet.FetchMemberAsync(mentions[1].TargetId); + + if (member1 is null || member2 is null) + { + await MessageHelper.ReplyAsync(ctx, "Could not find one or both of those members."); + return; + } + + var result = await MarriageService.ForceMarryAsync(member1.UserId, member2.UserId); + + switch (result) + { + case MarriageService.ForceMarryResult.SameUser: + await MessageHelper.ReplyAsync(ctx, "You can't marry someone to themselves!"); + break; + case MarriageService.ForceMarryResult.User1AlreadyMarried: + await MessageHelper.ReplyAsync(ctx, $"«@u-{member1.UserId}» is already married."); + break; + case MarriageService.ForceMarryResult.User2AlreadyMarried: + await MessageHelper.ReplyAsync(ctx, $"«@u-{member2.UserId}» is already married."); + break; + case MarriageService.ForceMarryResult.Ok: + await MessageHelper.ReplyAsync(ctx, $"💒 «@u-{member1.UserId}» and «@u-{member2.UserId}» are now married! 🎉"); + break; + } + break; + } + case "divorce": + { + if (mentions.Count < 1) + { + await MessageHelper.ReplyAsync(ctx, $"Please mention a user. Usage: `{Config.Prefix}marriage force divorce @user`"); + return; + } + + var member = await ctx.Planet.FetchMemberAsync(mentions[0].TargetId); + + if (member is null) + { + await MessageHelper.ReplyAsync(ctx, "Could not find that member."); + return; + } + + var (result, partnerId) = await MarriageService.ForceDivorceAsync(member.UserId); + + switch (result) + { + case MarriageService.DivorceResult.NotMarried: + await MessageHelper.ReplyAsync(ctx, $"«@u-{member.UserId}» is not married."); + break; + case MarriageService.DivorceResult.Ok: + await MessageHelper.ReplyAsync(ctx, $"💔 «@u-{member.UserId}» and «@u-{partnerId}» have been forcefully divorced."); + break; + } + break; + } + default: + await MessageHelper.ReplyAsync(ctx, $"Usage: `{Config.Prefix}marriage force marry @user1 @user2` or `{Config.Prefix}marriage force divorce @user`"); + break; + } + } + + } +} \ No newline at end of file diff --git a/SkyBot/Commands/Utils/Accept.cs b/SkyBot/Commands/Utils/Accept.cs new file mode 100644 index 0000000..1a9ef33 --- /dev/null +++ b/SkyBot/Commands/Utils/Accept.cs @@ -0,0 +1,23 @@ +using SkyBot.Helpers; +using SkyBot.Models; + +namespace SkyBot.Commands +{ + public class Accept : ICommand + { + public string Name => "accept"; + public string[] Aliases => []; + public string Description => "Accepts a pending action."; + public string Category => "Utils"; + public string Usage => "accept"; + public string[] SubCommands => []; + + public async Task Execute(CommandContext ctx) + { + foreach (var handler in PendingActionRegistry.AcceptHandlers) + if (await handler(ctx)) return; + + await MessageHelper.ReplyAsync(ctx, "You have nothing pending to accept."); + } + } +} diff --git a/SkyBot/Commands/Utils/Cancel.cs b/SkyBot/Commands/Utils/Cancel.cs new file mode 100644 index 0000000..c0ce4fa --- /dev/null +++ b/SkyBot/Commands/Utils/Cancel.cs @@ -0,0 +1,23 @@ +using SkyBot.Helpers; +using SkyBot.Models; + +namespace SkyBot.Commands +{ + public class Cancel : ICommand + { + public string Name => "cancel"; + public string[] Aliases => []; + public string Description => "Cancels a pending action."; + public string Category => "Utils"; + public string Usage => "cancel"; + public string[] SubCommands => []; + + public async Task Execute(CommandContext ctx) + { + foreach (var handler in PendingActionRegistry.CancelHandlers) + if (await handler(ctx)) return; + + await MessageHelper.ReplyAsync(ctx, "You have nothing pending to cancel."); + } + } +} diff --git a/SkyBot/Commands/Utils/Confirm.cs b/SkyBot/Commands/Utils/Confirm.cs new file mode 100644 index 0000000..2eaee3b --- /dev/null +++ b/SkyBot/Commands/Utils/Confirm.cs @@ -0,0 +1,23 @@ +using SkyBot.Helpers; +using SkyBot.Models; + +namespace SkyBot.Commands +{ + public class Confirm : ICommand + { + public string Name => "confirm"; + public string[] Aliases => []; + public string Description => "Confirms a pending action."; + public string Category => "Utils"; + public string Usage => "confirm"; + public string[] SubCommands => []; + + public async Task Execute(CommandContext ctx) + { + foreach (var handler in PendingActionRegistry.ConfirmHandlers) + if (await handler(ctx)) return; + + await MessageHelper.ReplyAsync(ctx, "You have nothing pending to confirm."); + } + } +} diff --git a/SkyBot/Commands/Utils/Decline.cs b/SkyBot/Commands/Utils/Decline.cs new file mode 100644 index 0000000..3aa48e9 --- /dev/null +++ b/SkyBot/Commands/Utils/Decline.cs @@ -0,0 +1,23 @@ +using SkyBot.Helpers; +using SkyBot.Models; + +namespace SkyBot.Commands +{ + public class Decline : ICommand + { + public string Name => "decline"; + public string[] Aliases => []; + public string Description => "Declines a pending action."; + public string Category => "Utils"; + public string Usage => "decline"; + public string[] SubCommands => []; + + public async Task Execute(CommandContext ctx) + { + foreach (var handler in PendingActionRegistry.DeclineHandlers) + if (await handler(ctx)) return; + + await MessageHelper.ReplyAsync(ctx, "You have nothing pending to decline."); + } + } +} diff --git a/SkyBot/Helpers/MessageHelper.cs b/SkyBot/Helpers/MessageHelper.cs index 1f0fa03..0319630 100644 --- a/SkyBot/Helpers/MessageHelper.cs +++ b/SkyBot/Helpers/MessageHelper.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Text.Json; using SkyBot.Models; using Valour.Sdk.Models; using Valour.Sdk.Models.Messages.Embeds; @@ -13,35 +12,29 @@ namespace SkyBot.Helpers public static string Mention(this User user) => $"«@u-{user.Id}»"; public static string ToTitleCase(this string str) => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str); + public static string OrdinalDay(this DateTimeOffset dt) + { + string suffix = (dt.Day % 100) switch + { + 11 or 12 or 13 => "th", + _ => (dt.Day % 10) switch + { + 1 => "st", + 2 => "nd", + 3 => "rd", + _ => "th" + } + }; + return $"{dt.Day}{suffix}"; + } + public static async Task> ReplyAsync(CommandContext ctx, string? content, Embed? embed = null, bool reply = false) { - long? replyToId; + long? replyToId = reply ? ctx.Message.ReplyToId : ctx.Message.Id; - if (reply) - { - replyToId = ctx.Message.ReplyToId; - } - else - { - replyToId = ctx.Message.Id; - } - - string? embedData; - if (embed is not null) - { - embedData = JsonSerializer.Serialize(embed); - } - else - { - embedData = null; - } - - - - Message msg = new Message(ctx.Client) + Message msg = new(ctx.Client) { Content = content, - EmbedData = embedData, ChannelId = ctx.Channel.Id, PlanetId = ctx.Planet.Id, AuthorUserId = ctx.Client.Me.Id, @@ -49,6 +42,10 @@ namespace SkyBot.Helpers ReplyToId = replyToId, Fingerprint = Guid.NewGuid().ToString() }; + + if (embed is not null) + msg.SetEmbed(embed); + return await ctx.Client.MessageService.SendMessage(msg); } diff --git a/SkyBot/Helpers/PendingActionRegistry.cs b/SkyBot/Helpers/PendingActionRegistry.cs new file mode 100644 index 0000000..abc263b --- /dev/null +++ b/SkyBot/Helpers/PendingActionRegistry.cs @@ -0,0 +1,97 @@ +using SkyBot.Models; +using SkyBot.Services; + +namespace SkyBot.Helpers +{ + public static class PendingActionRegistry + { + public static readonly List>> ConfirmHandlers = + [ + TryConfirmDivorce, + // Register new confirm handlers here + ]; + + public static readonly List>> CancelHandlers = + [ + TryCancelDivorce, + // Register new cancel handlers here + ]; + + public static readonly List>> AcceptHandlers = + [ + TryAcceptProposal, + // Register new accept handlers here + ]; + + public static readonly List>> DeclineHandlers = + [ + TryDeclineProposal, + // Register new decline handlers here + ]; + + private static async Task TryConfirmDivorce(CommandContext ctx) + { + string name = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown"; + var (result, partnerId) = await MarriageService.DivorceAsync(ctx.Member.UserId, confirmed: true); + + switch (result) + { + case MarriageService.DivorceResult.Ok: + await MessageHelper.ReplyAsync(ctx, $"💔 {name} and «@u-{partnerId}» have divorced."); + return true; + case MarriageService.DivorceResult.ConfirmationExpired: + await MessageHelper.ReplyAsync(ctx, $"Your divorce confirmation expired. Run `{Config.Prefix}marriage divorce` again to start over."); + return true; + default: + return false; + } + } + + private static async Task TryCancelDivorce(CommandContext ctx) + { + string name = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown"; + + if (!MarriageService.CancelDivorce(ctx.Member.UserId)) + return false; + + await MessageHelper.ReplyAsync(ctx, $"Divorce cancelled, {name}."); + return true; + } + + private static async Task TryAcceptProposal(CommandContext ctx) + { + string name = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown"; + var (result, proposerId) = await MarriageService.AcceptAsync(ctx.Member.UserId); + + switch (result) + { + case MarriageService.AcceptResult.Ok: + await MessageHelper.ReplyAsync(ctx, $"💒 {name} has accepted «@u-{proposerId}»'s proposal! They are now married! 🎉"); + return true; + case MarriageService.AcceptResult.Expired: + await MessageHelper.ReplyAsync(ctx, $"The proposal from «@u-{proposerId}» has expired."); + return true; + case MarriageService.AcceptResult.ProposerAlreadyMarried: + await MessageHelper.ReplyAsync(ctx, $"«@u-{proposerId}» is already married!"); + return true; + case MarriageService.AcceptResult.AcceptorAlreadyMarried: + await MessageHelper.ReplyAsync(ctx, "You are already married!"); + return true; + default: + return false; + } + } + + private static async Task TryDeclineProposal(CommandContext ctx) + { + string name = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown"; + var (result, proposerId) = MarriageService.Decline(ctx.Member.UserId); + + if (result == MarriageService.DeclineResult.NoPendingProposal) + return false; + + await MessageHelper.ReplyAsync(ctx, $"💔 {name} has declined «@u-{proposerId}»'s proposal."); + return true; + } + } +} diff --git a/SkyBot/Models/MarriageModel.cs b/SkyBot/Models/MarriageModel.cs new file mode 100644 index 0000000..6207f16 --- /dev/null +++ b/SkyBot/Models/MarriageModel.cs @@ -0,0 +1,5 @@ +public class MarriageModel +{ + public long SpouseId { get; set; } + public long MarriedAt {get; set; } +} \ No newline at end of file diff --git a/SkyBot/Services/DatabaseService.cs b/SkyBot/Services/DatabaseService.cs new file mode 100644 index 0000000..a1af32e --- /dev/null +++ b/SkyBot/Services/DatabaseService.cs @@ -0,0 +1,36 @@ +using Microsoft.Data.Sqlite; + +namespace SkyBot.Services +{ + public static class DatabaseService + { + 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 InitialiseAsync() + { + using SqliteConnection connection = GetConnection(); + + using (SqliteCommand cmd = connection.CreateCommand()) + { + cmd.CommandText = @" + CREATE TABLE IF NOT EXISTS Marriages ( + UserId1 INTEGER NOT NULL, + UserId2 INTEGER NOT NULL, + MarriedAt INTEGER NOT NULL, + PRIMARY KEY (UserID1, UserId2) + ); + "; + await cmd.ExecuteNonQueryAsync(); + } + + Console.WriteLine("Database initialised."); + } + } +} \ No newline at end of file diff --git a/SkyBot/Services/MarriageService.cs b/SkyBot/Services/MarriageService.cs new file mode 100644 index 0000000..e266879 --- /dev/null +++ b/SkyBot/Services/MarriageService.cs @@ -0,0 +1,219 @@ +using System.Collections.Concurrent; +using Microsoft.Data.Sqlite; + +namespace SkyBot.Services +{ + public static class MarriageService + { + private static readonly ConcurrentDictionary marriages = new(); + private static readonly ConcurrentDictionary pendingProposals = new(); + private static readonly TimeSpan ProposalTimeout = TimeSpan.FromMinutes(5); + + public static async Task InitialiseAsync() + { + using SqliteConnection connection = DatabaseService.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT UserId1, UserId2, MarriedAt FROM Marriages"; + using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + long u1 = (long)reader["UserId1"]; + long u2 = (long)reader["UserId2"]; + long marriedAt = (long)reader["MarriedAt"]; + marriages[u1] = new MarriageModel { SpouseId = u2, MarriedAt = marriedAt }; + marriages[u2] = new MarriageModel { SpouseId = u1, MarriedAt = marriedAt }; + } + Console.WriteLine($"MarriageService initialised. Loaded {marriages.Count / 2} marriages"); + } + + public static MarriageModel? GetMarriage(long userId) + { + return marriages.TryGetValue(userId, out var marriage) ? marriage : null; + } + + public enum ProposeResult + { + Ok, + SelfPropose, + AlreadyMarried, + TargetAlreadyMarried, + AlreadyProposed + } + + public static ProposeResult Propose(long proposerId, long targetId) + { + if (proposerId == targetId) return ProposeResult.SelfPropose; + if (marriages.ContainsKey(proposerId)) return ProposeResult.AlreadyMarried; + if (marriages.ContainsKey(targetId)) return ProposeResult.TargetAlreadyMarried; + + if (pendingProposals.TryGetValue(targetId, out var existing) && existing.ProposerId == proposerId && existing.ExpiresAt > DateTime.UtcNow) return ProposeResult.AlreadyProposed; + + pendingProposals[targetId] = (proposerId, DateTime.UtcNow.Add(ProposalTimeout)); + return ProposeResult.Ok; + } + + public enum AcceptResult + { + Ok, + NoPendingProposal, + Expired, + ProposerAlreadyMarried, + AcceptorAlreadyMarried + } + + public static async Task<(AcceptResult Result, long ProposerId)> AcceptAsync(long acceptorId) + { + if (!pendingProposals.TryRemove(acceptorId, out var proposal)) + { + return (AcceptResult.NoPendingProposal, 0); + } + + if (proposal.ExpiresAt <= DateTime.UtcNow) + { + return (AcceptResult.Expired, proposal.ProposerId); + } + + if (marriages.ContainsKey(proposal.ProposerId)) + { + return (AcceptResult.ProposerAlreadyMarried, proposal.ProposerId); + } + + if (marriages.ContainsKey(acceptorId)) + { + return (AcceptResult.AcceptorAlreadyMarried, proposal.ProposerId); + } + + long u1 = Math.Min(acceptorId, proposal.ProposerId); + long u2 = Math.Max(acceptorId, proposal.ProposerId); + long marriedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + using SqliteConnection connection = DatabaseService.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = "INSERT OR IGNORE INTO Marriages (UserId1, UserId2, MarriedAt) VALUES ($u1, $u2, $at)"; + cmd.Parameters.AddWithValue("$u1", u1); + cmd.Parameters.AddWithValue("$u2", u2); + cmd.Parameters.AddWithValue("$at", marriedAt); + await cmd.ExecuteNonQueryAsync(); + + marriages[acceptorId] = new MarriageModel { SpouseId = proposal.ProposerId, MarriedAt = marriedAt}; + marriages[proposal.ProposerId] = new MarriageModel { SpouseId = acceptorId, MarriedAt = marriedAt}; + + return (AcceptResult.Ok, proposal.ProposerId); + } + + public enum DeclineResult + { + Ok, + NoPendingProposal + } + + public static (DeclineResult Result, long ProposerId) Decline(long acceptorId) + { + if (!pendingProposals.TryRemove(acceptorId, out var proposal)) + { + return (DeclineResult.NoPendingProposal, 0); + } + + return (DeclineResult.Ok, proposal.ProposerId); + } + + public enum ForceMarryResult + { + Ok, + SameUser, + User1AlreadyMarried, + User2AlreadyMarried + } + + public static async Task ForceMarryAsync(long userId1, long userId2) + { + if (userId1 == userId2) return ForceMarryResult.SameUser; + if (marriages.ContainsKey(userId1)) return ForceMarryResult.User1AlreadyMarried; + if (marriages.ContainsKey(userId2)) return ForceMarryResult.User2AlreadyMarried; + + long u1 = Math.Min(userId1, userId2); + long u2 = Math.Max(userId1, userId2); + long marriedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + using SqliteConnection connection = DatabaseService.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = "INSERT OR IGNORE INTO Marriages (UserId1, UserId2, MarriedAt) VALUES ($u1, $u2, $at)"; + cmd.Parameters.AddWithValue("$u1", u1); + cmd.Parameters.AddWithValue("$u2", u2); + cmd.Parameters.AddWithValue("$at", marriedAt); + await cmd.ExecuteNonQueryAsync(); + + marriages[userId1] = new MarriageModel { SpouseId = userId2, MarriedAt = marriedAt}; + marriages[userId2] = new MarriageModel { SpouseId = userId1, MarriedAt = marriedAt}; + + return ForceMarryResult.Ok; + } + + private static readonly ConcurrentDictionary pendingDivorces = new(); + private static readonly TimeSpan DivorceConfirmTimeout = TimeSpan.FromMinutes(1); + + public static bool RequestDivorceConfirmation(long userId) + { + if (!marriages.ContainsKey(userId)) return false; + pendingDivorces[userId] = DateTime.UtcNow.Add(DivorceConfirmTimeout); + return true; + } + + public static bool CancelDivorce(long userId) + { + return pendingDivorces.TryRemove(userId, out _); + } + + public enum DivorceResult + { + Ok, + NotMarried, + NoConfirmation, + ConfirmationExpired + } + + public static async Task<(DivorceResult Result, long PartnerId)> DivorceAsync(long userId, bool confirmed = false) + { + if (!marriages.ContainsKey(userId)) return (DivorceResult.NotMarried, 0); + if (!confirmed) return (DivorceResult.NoConfirmation, 0); + if (!pendingDivorces.TryRemove(userId, out var expiresAt)) return (DivorceResult.NoConfirmation, 0); + if (expiresAt <= DateTime.UtcNow) return (DivorceResult.ConfirmationExpired, 0); + if (!marriages.TryRemove(userId, out MarriageModel? marriage)) return (DivorceResult.NotMarried, 0); + + marriages.TryRemove(marriage.SpouseId, out _); + + long u1 = Math.Min(userId, marriage.SpouseId); + long u2 = Math.Max(userId, marriage.SpouseId); + + using SqliteConnection connection = DatabaseService.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = "DELETE FROM Marriages WHERE UserId1 = $u1 AND UserId2 = $u2"; + cmd.Parameters.AddWithValue("$u1", u1); + cmd.Parameters.AddWithValue("$u2", u2); + await cmd.ExecuteNonQueryAsync(); + + return (DivorceResult.Ok, marriage.SpouseId); + } + + public static async Task<(DivorceResult Result, long PartnerId)> ForceDivorceAsync(long userId) + { + if (!marriages.TryRemove(userId, out MarriageModel? marriage)) return (DivorceResult.NotMarried, 0); + + marriages.TryRemove(marriage.SpouseId, out _); + pendingDivorces.TryRemove(userId, out _); + + long u1 = Math.Min(userId, marriage.SpouseId); + long u2 = Math.Max(userId, marriage.SpouseId); + + using SqliteConnection connection = DatabaseService.GetConnection(); + using SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = "DELETE FROM Marriages WHERE UserId1 = $u1 AND UserId2 = $u2"; + cmd.Parameters.AddWithValue("$u1", u1); + cmd.Parameters.AddWithValue("$u2", u2); + await cmd.ExecuteNonQueryAsync(); + + return (DivorceResult.Ok, marriage.SpouseId); + } + + } +} \ No newline at end of file diff --git a/SkyBot/SkyBot.cs b/SkyBot/SkyBot.cs index 490bdd0..593e329 100644 --- a/SkyBot/SkyBot.cs +++ b/SkyBot/SkyBot.cs @@ -16,7 +16,10 @@ namespace SkyBot client.SetupHttpClient(); try { + await DatabaseService.InitialiseAsync(); + await MarriageService.InitialiseAsync(); await BotService.InitialiseBotAsync(client, channelCache, initialisedPlanets); + Console.WriteLine("Ready and Listening..."); await Task.Delay(Timeout.Infinite); diff --git a/SkyBot/SkyBot.csproj b/SkyBot/SkyBot.csproj index 206799e..89faa4c 100644 --- a/SkyBot/SkyBot.csproj +++ b/SkyBot/SkyBot.csproj @@ -5,12 +5,13 @@ net10.0 enable enable - 0.3.1.0 + 0.3.2.0 - - + + +