feat(guild_chat): deck + replay attachments
Implement all five guild_chat attachment endpoints:
- add_deck: server looks up deck by (viewerId, format, deckNo) and snapshots
the current state into a DECK-type chat message DeckPayload column.
- delete_deck: author OR leader/sub-leader may clear; returns refreshed
deck_log. Extended IGuildChatMessageRepository.ClearDeckAsync with
leaderOverride flag; added GetDeckMessagesAsync + GetByMessageIdAsync.
- add_replay: stores {"battle_id": N} in ReplayPayload; full shape is
TODO(post-merge) pending replay-subsystem capture.
- replay_detail: returns the stored ReplayPayload as JsonElement passthrough.
- deck_log: returns all active DECK messages grouped by API-side Format string
key ("1"=Rotation, "2"=Unlimited, "3"=PreRotation always present;
"4"=Crossover, "5"=MyRotation only when non-empty).
Adds DeckLogDataDto (typed) for deck_log / delete_deck responses.
Replay response uses JsonElement passthrough (TODO(post-merge)).
8 new integration tests — all green. Full suite 1530/1530 pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,57 @@
|
||||
using SVSim.Database.Enums;
|
||||
|
||||
namespace SVSim.Database.Services.Guild;
|
||||
|
||||
public sealed record ChatWindow(IReadOnlyList<Entities.Guild.GuildChatMessage> Messages, int WaitIntervalSeconds);
|
||||
public sealed record ChatPostResult(bool Ok, int? AssignedMessageId);
|
||||
|
||||
/// <summary>
|
||||
/// A single entry in the deck log: the raw JSON stored in DeckPayload, plus the envelope fields
|
||||
/// (messageId, authorViewerId) needed to compute delete_permission_exists in the controller.
|
||||
/// </summary>
|
||||
public sealed record DeckLogEntry(int MessageId, long AuthorViewerId, string PayloadJson);
|
||||
|
||||
/// <summary>
|
||||
/// Result for /guild_chat/deck_log and /guild_chat/delete_deck.
|
||||
/// All active (non-deleted) DECK messages in the guild — caller is responsible for grouping by
|
||||
/// deck_format and computing delete_permission_exists per entry.
|
||||
/// </summary>
|
||||
public sealed record DeckLogResult(IReadOnlyList<DeckLogEntry> Entries);
|
||||
|
||||
public interface IGuildChatService
|
||||
{
|
||||
Task<ChatWindow> GetWindowAsync(long viewerId, int startMessageId, int direction, int waitIntervalHint, CancellationToken ct = default);
|
||||
Task<ChatPostResult> PostTextOrStampAsync(long viewerId, int type, string message, CancellationToken ct = default);
|
||||
Task<ChatPostResult> PostDeckAsync(long viewerId, string deckPayloadJson, CancellationToken ct = default);
|
||||
Task<bool> DeleteDeckAsync(long viewerId, int messageId, CancellationToken ct = default);
|
||||
Task<ChatPostResult> PostReplayAsync(long viewerId, string replayPayloadJson, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Share a deck snapshot to guild chat. The deck is looked up by (viewerId, format, deckNo)
|
||||
/// and its current state is stored as the DECK message's DeckPayload JSON.
|
||||
/// <paramref name="deckFormatApi"/> is the API-side wire integer (used verbatim in the stored payload).
|
||||
/// </summary>
|
||||
Task<ChatPostResult> PostDeckAsync(long viewerId, Format format, int deckFormatApi, int deckNo, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a shared deck. Caller must be the author OR a leader/sub-leader.
|
||||
/// Returns true on success, false if not found / no permission / already cleared.
|
||||
/// On success also returns the refreshed deck log.
|
||||
/// </summary>
|
||||
Task<(bool Ok, DeckLogResult? Log)> DeleteDeckAsync(long viewerId, int messageId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Share a replay to guild chat. Stores {"battle_id": N} as the REPLAY message's ReplayPayload.
|
||||
/// </summary>
|
||||
Task<ChatPostResult> PostReplayAsync(long viewerId, long battleId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Fetch the replay payload for a given REPLAY chat message. Returns null if not found / not a replay.
|
||||
/// </summary>
|
||||
Task<string?> GetReplayDetailAsync(long viewerId, int messageId, CancellationToken ct = default);
|
||||
Task<string?> GetDeckLogAsync(long viewerId, int messageId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Return the guild's full deck log — all active (non-deleted) DECK messages grouped by
|
||||
/// API-side Format string key. Returns null if caller is not in a guild.
|
||||
/// </summary>
|
||||
Task<DeckLogResult?> GetDeckLogAsync(long viewerId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Emit a system-event row. Called by IGuildService at create/join/leave/remove/role-change/description-change.</summary>
|
||||
Task EmitSystemEventAsync(int guildId, long actorViewerId, Entities.Guild.GuildChatMessageType type, string? body = null, CancellationToken ct = default);
|
||||
|
||||
Reference in New Issue
Block a user