Minor Update
This commit is contained in:
@@ -12,7 +12,7 @@ namespace SkyBot.Commands
|
|||||||
public string[] Aliases => [];
|
public string[] Aliases => [];
|
||||||
public string Description => "Planet Commands";
|
public string Description => "Planet Commands";
|
||||||
public string Category => "Dev";
|
public string Category => "Dev";
|
||||||
public string Usage => "planet <join|leave|list>";
|
public string Usage => "planet <sub>";
|
||||||
public string[] SubCommands => ["join", "leave", "list"];
|
public string[] SubCommands => ["join", "leave", "list"];
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
public async Task Execute(CommandContext ctx)
|
||||||
|
|||||||
222
SkyBot/Commands/RP/Marriage.cs
Normal file
222
SkyBot/Commands/RP/Marriage.cs
Normal file
@@ -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 <sub>";
|
||||||
|
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 <propose|status|divorce>`");
|
||||||
|
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 <reply|@user>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SkyBot/Commands/Utils/Accept.cs
Normal file
23
SkyBot/Commands/Utils/Accept.cs
Normal file
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SkyBot/Commands/Utils/Cancel.cs
Normal file
23
SkyBot/Commands/Utils/Cancel.cs
Normal file
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SkyBot/Commands/Utils/Confirm.cs
Normal file
23
SkyBot/Commands/Utils/Confirm.cs
Normal file
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SkyBot/Commands/Utils/Decline.cs
Normal file
23
SkyBot/Commands/Utils/Decline.cs
Normal file
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
|
||||||
using SkyBot.Models;
|
using SkyBot.Models;
|
||||||
using Valour.Sdk.Models;
|
using Valour.Sdk.Models;
|
||||||
using Valour.Sdk.Models.Messages.Embeds;
|
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 Mention(this User user) => $"«@u-{user.Id}»";
|
||||||
public static string ToTitleCase(this string str) => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str);
|
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<TaskResult<Message>> ReplyAsync(CommandContext ctx, string? content, Embed? embed = null, bool reply = false)
|
public static async Task<TaskResult<Message>> ReplyAsync(CommandContext ctx, string? content, Embed? embed = null, bool reply = false)
|
||||||
{
|
{
|
||||||
long? replyToId;
|
long? replyToId = reply ? ctx.Message.ReplyToId : ctx.Message.Id;
|
||||||
|
|
||||||
if (reply)
|
Message msg = new(ctx.Client)
|
||||||
{
|
|
||||||
replyToId = ctx.Message.ReplyToId;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
replyToId = ctx.Message.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? embedData;
|
|
||||||
if (embed is not null)
|
|
||||||
{
|
|
||||||
embedData = JsonSerializer.Serialize(embed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
embedData = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Message msg = new Message(ctx.Client)
|
|
||||||
{
|
{
|
||||||
Content = content,
|
Content = content,
|
||||||
EmbedData = embedData,
|
|
||||||
ChannelId = ctx.Channel.Id,
|
ChannelId = ctx.Channel.Id,
|
||||||
PlanetId = ctx.Planet.Id,
|
PlanetId = ctx.Planet.Id,
|
||||||
AuthorUserId = ctx.Client.Me.Id,
|
AuthorUserId = ctx.Client.Me.Id,
|
||||||
@@ -49,6 +42,10 @@ namespace SkyBot.Helpers
|
|||||||
ReplyToId = replyToId,
|
ReplyToId = replyToId,
|
||||||
Fingerprint = Guid.NewGuid().ToString()
|
Fingerprint = Guid.NewGuid().ToString()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (embed is not null)
|
||||||
|
msg.SetEmbed(embed);
|
||||||
|
|
||||||
return await ctx.Client.MessageService.SendMessage(msg);
|
return await ctx.Client.MessageService.SendMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
97
SkyBot/Helpers/PendingActionRegistry.cs
Normal file
97
SkyBot/Helpers/PendingActionRegistry.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using SkyBot.Models;
|
||||||
|
using SkyBot.Services;
|
||||||
|
|
||||||
|
namespace SkyBot.Helpers
|
||||||
|
{
|
||||||
|
public static class PendingActionRegistry
|
||||||
|
{
|
||||||
|
public static readonly List<Func<CommandContext, Task<bool>>> ConfirmHandlers =
|
||||||
|
[
|
||||||
|
TryConfirmDivorce,
|
||||||
|
// Register new confirm handlers here
|
||||||
|
];
|
||||||
|
|
||||||
|
public static readonly List<Func<CommandContext, Task<bool>>> CancelHandlers =
|
||||||
|
[
|
||||||
|
TryCancelDivorce,
|
||||||
|
// Register new cancel handlers here
|
||||||
|
];
|
||||||
|
|
||||||
|
public static readonly List<Func<CommandContext, Task<bool>>> AcceptHandlers =
|
||||||
|
[
|
||||||
|
TryAcceptProposal,
|
||||||
|
// Register new accept handlers here
|
||||||
|
];
|
||||||
|
|
||||||
|
public static readonly List<Func<CommandContext, Task<bool>>> DeclineHandlers =
|
||||||
|
[
|
||||||
|
TryDeclineProposal,
|
||||||
|
// Register new decline handlers here
|
||||||
|
];
|
||||||
|
|
||||||
|
private static async Task<bool> 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<bool> 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<bool> 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<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
SkyBot/Models/MarriageModel.cs
Normal file
5
SkyBot/Models/MarriageModel.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
public class MarriageModel
|
||||||
|
{
|
||||||
|
public long SpouseId { get; set; }
|
||||||
|
public long MarriedAt {get; set; }
|
||||||
|
}
|
||||||
36
SkyBot/Services/DatabaseService.cs
Normal file
36
SkyBot/Services/DatabaseService.cs
Normal file
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
219
SkyBot/Services/MarriageService.cs
Normal file
219
SkyBot/Services/MarriageService.cs
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace SkyBot.Services
|
||||||
|
{
|
||||||
|
public static class MarriageService
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<long, MarriageModel> marriages = new();
|
||||||
|
private static readonly ConcurrentDictionary<long, (long ProposerId, DateTime ExpiresAt)> 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<ForceMarryResult> 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<long, DateTime> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,11 @@ namespace SkyBot
|
|||||||
client.SetupHttpClient();
|
client.SetupHttpClient();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await DatabaseService.InitialiseAsync();
|
||||||
|
await MarriageService.InitialiseAsync();
|
||||||
await BotService.InitialiseBotAsync(client, channelCache, initialisedPlanets);
|
await BotService.InitialiseBotAsync(client, channelCache, initialisedPlanets);
|
||||||
|
|
||||||
|
|
||||||
Console.WriteLine("Ready and Listening...");
|
Console.WriteLine("Ready and Listening...");
|
||||||
await Task.Delay(Timeout.Infinite);
|
await Task.Delay(Timeout.Infinite);
|
||||||
} catch (Exception ex)
|
} catch (Exception ex)
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.3.1.0</Version>
|
<Version>0.3.2.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
<PackageReference Include="DotNetEnv" Version="3.2.0" />
|
||||||
<PackageReference Include="Valour.Sdk" Version="0.5.21" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||||
|
<PackageReference Include="Valour.Sdk" Version="0.5.31" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user