marriage.
This commit is contained in:
14
COMMANDS.md
14
COMMANDS.md
@@ -61,6 +61,20 @@ All commands are prefixed with your configured prefix (e.g. `s/`). Arguments in
|
|||||||
|
|
||||||
All RP commands accept an optional `[@user]` argument or a message reply to target someone.
|
All RP commands accept an optional `[@user]` argument or a message reply to target someone.
|
||||||
|
|
||||||
|
### Marriage
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `marriage propose @user` | Propose to someone (reply to their message or mention them). Proposal expires after 5 minutes. |
|
||||||
|
| `marriage accept` | Accept a pending proposal |
|
||||||
|
| `marriage decline` | Decline a pending proposal |
|
||||||
|
| `marriage status` | Show your current marriage status |
|
||||||
|
| `marriage divorce` | End your current marriage |
|
||||||
|
|
||||||
|
Marriages persist across bot restarts.
|
||||||
|
|
||||||
|
### GIF Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `angry` | Express anger |
|
| `angry` | Express anger |
|
||||||
|
|||||||
346
SkyBot/Commands/RP/Marriage.cs
Normal file
346
SkyBot/Commands/RP/Marriage.cs
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using SkyBot.Helpers;
|
||||||
|
using SkyBot.Models;
|
||||||
|
using SkyBot.Services;
|
||||||
|
using Valour.Sdk.Models;
|
||||||
|
|
||||||
|
namespace SkyBot.Commands
|
||||||
|
{
|
||||||
|
public class Marriage : ICommand
|
||||||
|
{
|
||||||
|
public string Name => "marriage";
|
||||||
|
public string[] Aliases => ["marry"];
|
||||||
|
public string Description => "Marriage system — propose, accept, decline, check status, or divorce.";
|
||||||
|
public string Section => "RP";
|
||||||
|
public string Usage => "marriage <propose|accept|decline|status|divorce>";
|
||||||
|
|
||||||
|
public async Task Execute(CommandContext ctx)
|
||||||
|
{
|
||||||
|
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
||||||
|
long channelId = ctx.ChannelId;
|
||||||
|
|
||||||
|
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
||||||
|
|
||||||
|
string sub = ctx.Args.Length > 0 ? ctx.Args[0].ToLower() : "status";
|
||||||
|
|
||||||
|
switch (sub)
|
||||||
|
{
|
||||||
|
case "propose":
|
||||||
|
await HandlePropose(ctx, channel);
|
||||||
|
break;
|
||||||
|
case "accept":
|
||||||
|
await HandleAccept(ctx, channel);
|
||||||
|
break;
|
||||||
|
case "decline":
|
||||||
|
await HandleDecline(ctx, channel);
|
||||||
|
break;
|
||||||
|
case "divorce":
|
||||||
|
await HandleDivorce(ctx, channel);
|
||||||
|
break;
|
||||||
|
case "force":
|
||||||
|
await HandleForce(ctx, channel);
|
||||||
|
break;
|
||||||
|
case "status":
|
||||||
|
default:
|
||||||
|
await HandleStatus(ctx, channel);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandlePropose(CommandContext ctx, Channel channel)
|
||||||
|
{
|
||||||
|
long proposerId = ctx.Member.UserId;
|
||||||
|
string sender = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown";
|
||||||
|
|
||||||
|
long? targetUserId = null;
|
||||||
|
string? targetMention = null;
|
||||||
|
|
||||||
|
// Prefer reply-to for target resolution
|
||||||
|
if (ctx.Message.ReplyToId is not null)
|
||||||
|
{
|
||||||
|
var replied = await ctx.Message.FetchReplyMessageAsync();
|
||||||
|
if (replied is not null)
|
||||||
|
{
|
||||||
|
var author = await replied.FetchAuthorAsync();
|
||||||
|
if (author is not null)
|
||||||
|
{
|
||||||
|
targetUserId = author.Id;
|
||||||
|
targetMention = $"«@u-{author.Id}»";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to message mention
|
||||||
|
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 not null)
|
||||||
|
{
|
||||||
|
targetUserId = mentioned.UserId;
|
||||||
|
targetMention = $"«@u-{mentioned.UserId}»";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUserId is null)
|
||||||
|
{
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"Please reply to someone's message or mention them. Usage: `{Config.Prefix}marriage propose @user`");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = MarriageService.Propose(proposerId, targetUserId.Value);
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case MarriageService.ProposeResult.SelfPropose:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel, "You can't propose to yourself!");
|
||||||
|
break;
|
||||||
|
case MarriageService.ProposeResult.AlreadyMarried:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"You're already married, {sender}! You'd need to `{Config.Prefix}marriage divorce` first.");
|
||||||
|
break;
|
||||||
|
case MarriageService.ProposeResult.TargetAlreadyMarried:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"{targetMention} is already married!");
|
||||||
|
break;
|
||||||
|
case MarriageService.ProposeResult.AlreadyProposed:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"You've already proposed to {targetMention}! Waiting for their response...");
|
||||||
|
break;
|
||||||
|
case MarriageService.ProposeResult.Ok:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"💍 {sender} gets down on one knee and proposes to {targetMention}!\n" +
|
||||||
|
$"{targetMention}, type `{Config.Prefix}marriage accept` to accept or `{Config.Prefix}marriage decline` to decline.\n" +
|
||||||
|
$"This proposal expires in 5 minutes.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleAccept(CommandContext ctx, Channel channel)
|
||||||
|
{
|
||||||
|
long acceptorId = ctx.Member.UserId;
|
||||||
|
string acceptorName = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown";
|
||||||
|
|
||||||
|
var (result, proposerId) = await MarriageService.AcceptAsync(acceptorId);
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case MarriageService.AcceptResult.NoPendingProposal:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
"You don't have any pending proposals to accept.");
|
||||||
|
break;
|
||||||
|
case MarriageService.AcceptResult.Expired:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"That proposal from «@u-{proposerId}» has expired.");
|
||||||
|
break;
|
||||||
|
case MarriageService.AcceptResult.AlreadyMarried:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
"One of you is already married! The proposal has been cancelled.");
|
||||||
|
break;
|
||||||
|
case MarriageService.AcceptResult.Ok:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"💒 {acceptorName} has accepted «@u-{proposerId}»'s proposal! They are now married! 🎉");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleDecline(CommandContext ctx, Channel channel)
|
||||||
|
{
|
||||||
|
long acceptorId = ctx.Member.UserId;
|
||||||
|
string acceptorName = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown";
|
||||||
|
|
||||||
|
var (result, proposerId) = MarriageService.Decline(acceptorId);
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case MarriageService.DeclineResult.NoPendingProposal:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
"You don't have any pending proposals to decline.");
|
||||||
|
break;
|
||||||
|
case MarriageService.DeclineResult.Ok:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"💔 {acceptorName} has declined «@u-{proposerId}»'s proposal.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleStatus(CommandContext ctx, Channel channel)
|
||||||
|
{
|
||||||
|
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 not null)
|
||||||
|
{
|
||||||
|
userId = mentioned.UserId;
|
||||||
|
name = mentioned.Nickname ?? mentioned.User?.Name ?? "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long? partnerId = MarriageService.GetPartner(userId);
|
||||||
|
|
||||||
|
if (partnerId is null)
|
||||||
|
{
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"💔 {name} is not currently married.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"💑 {name} is married to «@u-{partnerId}»!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleForce(CommandContext ctx, Channel channel)
|
||||||
|
{
|
||||||
|
if (ctx.Member.UserId != Config.OwnerId)
|
||||||
|
{
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel, "You don't have permission to use this command.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string action = ctx.Args.Length > 1 ? ctx.Args[1].ToLower() : "";
|
||||||
|
|
||||||
|
if (action is not ("marry" or "divorce"))
|
||||||
|
{
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"Usage: `{Config.Prefix}marriage force marry @user1 @user2` or `{Config.Prefix}marriage force divorce @user1`");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mentions = ctx.Message.Mentions ?? [];
|
||||||
|
|
||||||
|
if (action == "marry")
|
||||||
|
{
|
||||||
|
if (mentions.Count < 2)
|
||||||
|
{
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"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, channel, "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, channel, "You can't marry someone to themselves!");
|
||||||
|
break;
|
||||||
|
case MarriageService.ForceMarryResult.User1AlreadyMarried:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"«@u-{member1.UserId}» is already married.");
|
||||||
|
break;
|
||||||
|
case MarriageService.ForceMarryResult.User2AlreadyMarried:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"«@u-{member2.UserId}» is already married.");
|
||||||
|
break;
|
||||||
|
case MarriageService.ForceMarryResult.Ok:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"💒 «@u-{member1.UserId}» and «@u-{member2.UserId}» are now married! 🎉");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // divorce
|
||||||
|
{
|
||||||
|
if (mentions.Count < 1)
|
||||||
|
{
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"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, channel, "Could not find that member.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (result, partnerId) = await MarriageService.DivorceAsync(member.UserId);
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case MarriageService.DivorceResult.NotMarried:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"«@u-{member.UserId}» is not married.");
|
||||||
|
break;
|
||||||
|
case MarriageService.DivorceResult.Ok:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"💔 «@u-{member.UserId}» and «@u-{partnerId}» have been forcefully divorced.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleDivorce(CommandContext ctx, Channel channel)
|
||||||
|
{
|
||||||
|
long userId = ctx.Member.UserId;
|
||||||
|
string name = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown";
|
||||||
|
string action = ctx.Args.Length > 1 ? ctx.Args[1].ToLower() : "";
|
||||||
|
|
||||||
|
if (action == "cancel")
|
||||||
|
{
|
||||||
|
if (MarriageService.CancelDivorce(userId))
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel, $"Divorce cancelled, {name}.");
|
||||||
|
else
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel, "You don't have a pending divorce to cancel.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == "confirm")
|
||||||
|
{
|
||||||
|
var (result, partnerId) = await MarriageService.DivorceAsync(userId, confirmed: true);
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case MarriageService.DivorceResult.NotMarried:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel, $"You're not married, {name}.");
|
||||||
|
break;
|
||||||
|
case MarriageService.DivorceResult.NoConfirmation:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"No pending divorce found. Run `{Config.Prefix}marriage divorce` first.");
|
||||||
|
break;
|
||||||
|
case MarriageService.DivorceResult.ConfirmationExpired:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"Your divorce confirmation expired. Run `{Config.Prefix}marriage divorce` again to start over.");
|
||||||
|
break;
|
||||||
|
case MarriageService.DivorceResult.Ok:
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"💔 {name} and «@u-{partnerId}» have divorced.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial divorce request — ask for confirmation
|
||||||
|
long? partnerId2 = MarriageService.GetPartner(userId);
|
||||||
|
if (partnerId2 is null)
|
||||||
|
{
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel, $"You're not married, {name}.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool requested = MarriageService.RequestDivorceConfirmation(userId);
|
||||||
|
if (requested)
|
||||||
|
{
|
||||||
|
await MessageHelper.ReplyAsync(ctx, channel,
|
||||||
|
$"Are you sure you want to divorce «@u-{partnerId2}»?\n" +
|
||||||
|
$"Type `{Config.Prefix}marriage divorce confirm` within 60 seconds to confirm, or `{Config.Prefix}marriage divorce cancel` to cancel.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
SkyBot/Helpers/DatabaseHelper.cs
Normal file
49
SkyBot/Helpers/DatabaseHelper.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (SqliteCommand cmd = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
cmd.CommandText = @"
|
||||||
|
CREATE TABLE IF NOT EXISTS Marriages (
|
||||||
|
UserId1 INTEGER NOT NULL,
|
||||||
|
UserId2 INTEGER NOT NULL,
|
||||||
|
MarriedAt TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (UserId1, UserId2)
|
||||||
|
);
|
||||||
|
";
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Database initialized.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
203
SkyBot/Services/MarriageService.cs
Normal file
203
SkyBot/Services/MarriageService.cs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using SkyBot.Helpers;
|
||||||
|
|
||||||
|
namespace SkyBot.Services
|
||||||
|
{
|
||||||
|
public static class MarriageService
|
||||||
|
{
|
||||||
|
// userId -> partnerId (stored in both directions)
|
||||||
|
private static readonly ConcurrentDictionary<long, long> _marriages = new();
|
||||||
|
|
||||||
|
// targetId -> (proposerId, expiresAt)
|
||||||
|
private static readonly ConcurrentDictionary<long, (long ProposerId, DateTime ExpiresAt)> _pendingProposals = new();
|
||||||
|
|
||||||
|
private static readonly TimeSpan ProposalTimeout = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
public static async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
using SqliteConnection connection = DatabaseHelper.GetConnection();
|
||||||
|
using SqliteCommand cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT UserId1, UserId2 FROM Marriages";
|
||||||
|
using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
long u1 = (long)reader["UserId1"];
|
||||||
|
long u2 = (long)reader["UserId2"];
|
||||||
|
_marriages[u1] = u2;
|
||||||
|
_marriages[u2] = u1;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"MarriageService initialized. Loaded {_marriages.Count / 2} marriages from database.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long? GetPartner(long userId)
|
||||||
|
{
|
||||||
|
return _marriages.TryGetValue(userId, out long partnerId) ? partnerId : 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;
|
||||||
|
|
||||||
|
// Check if there's already a non-expired proposal from this proposer to this target
|
||||||
|
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,
|
||||||
|
AlreadyMarried
|
||||||
|
}
|
||||||
|
|
||||||
|
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(acceptorId) || _marriages.ContainsKey(proposal.ProposerId))
|
||||||
|
return (AcceptResult.AlreadyMarried, proposal.ProposerId);
|
||||||
|
|
||||||
|
long u1 = Math.Min(acceptorId, proposal.ProposerId);
|
||||||
|
long u2 = Math.Max(acceptorId, proposal.ProposerId);
|
||||||
|
string marriedAt = DateTime.UtcNow.ToString("o");
|
||||||
|
|
||||||
|
using SqliteConnection connection = DatabaseHelper.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] = proposal.ProposerId;
|
||||||
|
_marriages[proposal.ProposerId] = acceptorId;
|
||||||
|
|
||||||
|
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);
|
||||||
|
string marriedAt = DateTime.UtcNow.ToString("o");
|
||||||
|
|
||||||
|
using SqliteConnection connection = DatabaseHelper.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] = userId2;
|
||||||
|
_marriages[userId2] = userId1;
|
||||||
|
|
||||||
|
return ForceMarryResult.Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// userId -> confirmationExpiresAt
|
||||||
|
private static readonly ConcurrentDictionary<long, DateTime> _pendingDivorces = new();
|
||||||
|
|
||||||
|
private static readonly TimeSpan DivorceConfirmTimeout = TimeSpan.FromSeconds(60);
|
||||||
|
|
||||||
|
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 long partnerId))
|
||||||
|
return (DivorceResult.NotMarried, 0);
|
||||||
|
|
||||||
|
_marriages.TryRemove(partnerId, out _);
|
||||||
|
|
||||||
|
long u1 = Math.Min(userId, partnerId);
|
||||||
|
long u2 = Math.Max(userId, partnerId);
|
||||||
|
|
||||||
|
using SqliteConnection connection = DatabaseHelper.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, partnerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ namespace SkyBot
|
|||||||
StartTime = DateTime.UtcNow;
|
StartTime = DateTime.UtcNow;
|
||||||
await DatabaseHelper.InitializeAsync();
|
await DatabaseHelper.InitializeAsync();
|
||||||
await WelcomeService.InitializeAsync();
|
await WelcomeService.InitializeAsync();
|
||||||
|
await MarriageService.InitializeAsync();
|
||||||
await BotService.InitializeBotAsync(_client, _channelCache, _initializedPlanets);
|
await BotService.InitializeBotAsync(_client, _channelCache, _initializedPlanets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user