feat(guild_chat): messages window + system events

- GuildChatService.EmitSystemEventAsync: replaces no-op stub with real
  chat-row insertion via _msgs.AppendAsync (per-guild monotonic id).
- GuildChatService.GetWindowAsync: real implementation — verifies caller
  membership, delegates to IGuildChatMessageRepository.GetWindowAsync
  (direction 1=OLD/2=NEW/3=BOTH, limit 50), returns adaptive wait interval
  (ChatPollActiveSeconds when messages returned, ChatPollIdleSeconds otherwise).
- Drop unused repo injections from GuildChatService (guilds, invites, joinRequests).
- GuildChatController.Messages: wires the full response — chat_message[],
  users[] (deduplicated author profiles with Emblem+Degree nav includes),
  maintenance_card_list (empty until maintenance service lands), wait_interval.
- GuildChatPollTests (7 integration tests): fresh-guild CreateGuild event,
  sequenced Create+Join ordering, direction=NEW filter, direction=BOTH window,
  users[] deduplication, empty result for viewer-not-in-guild, active vs idle
  interval comparison. All 7 pass; full suite 1515/1515.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-27 14:44:09 -04:00
parent 484b51086a
commit 358f43aa7a
3 changed files with 479 additions and 28 deletions

View File

@@ -1,3 +1,5 @@
using SVSim.Database.Entities.Guild;
using SVSim.Database.Models.Config;
using SVSim.Database.Repositories.Guild;
using SVSim.Database.Services;
@@ -5,31 +7,65 @@ namespace SVSim.Database.Services.Guild;
public sealed class GuildChatService : IGuildChatService
{
private readonly IGuildRepository _guilds;
private readonly IGuildMemberRepository _members;
private readonly IGuildInviteRepository _invites;
private readonly IGuildJoinRequestRepository _joinRequests;
private readonly IGuildChatMessageRepository _chatMessages;
private readonly IGameConfigService _config;
private readonly IGuildChatMessageRepository _msgs;
private readonly IGameConfigService _cfg;
public GuildChatService(
IGuildRepository guilds,
IGuildMemberRepository members,
IGuildInviteRepository invites,
IGuildJoinRequestRepository joinRequests,
IGuildChatMessageRepository chatMessages,
IGameConfigService config)
IGuildChatMessageRepository msgs,
IGameConfigService cfg)
{
_guilds = guilds;
_members = members;
_invites = invites;
_joinRequests = joinRequests;
_chatMessages = chatMessages;
_config = config;
_msgs = msgs;
_cfg = cfg;
}
public Task<ChatWindow> GetWindowAsync(long viewerId, int startMessageId, int direction, int waitIntervalHint, CancellationToken ct = default)
=> throw new NotImplementedException();
// ── System-event emission ────────────────────────────────────────────────
public async Task EmitSystemEventAsync(
int guildId,
long actorViewerId,
GuildChatMessageType type,
string? body = null,
CancellationToken ct = default)
{
var msg = new GuildChatMessage
{
GuildId = guildId,
AuthorViewerId = actorViewerId,
MessageType = type,
Body = body ?? "",
CreatedAt = DateTime.UtcNow,
};
await _msgs.AppendAsync(msg, ct);
}
// ── Window query ─────────────────────────────────────────────────────────
public async Task<ChatWindow> GetWindowAsync(
long viewerId,
int startMessageId,
int direction,
int waitIntervalHint,
CancellationToken ct = default)
{
var membership = await _members.GetMembershipAsync(viewerId, ct);
if (membership is null)
{
var idleInterval = _cfg.Get<GuildConfig>().ChatPollIdleSeconds;
return new(Array.Empty<GuildChatMessage>(), idleInterval);
}
const int limit = 50;
var msgs = await _msgs.GetWindowAsync(membership.GuildId, startMessageId, direction, limit, ct);
var cfg = _cfg.Get<GuildConfig>();
int wait = msgs.Count == 0 ? cfg.ChatPollIdleSeconds : cfg.ChatPollActiveSeconds;
return new(msgs, wait);
}
// ── Not-yet-implemented stubs ────────────────────────────────────────────
public Task<ChatPostResult> PostTextOrStampAsync(long viewerId, int type, string message, CancellationToken ct = default)
=> throw new NotImplementedException();
@@ -48,8 +84,4 @@ public sealed class GuildChatService : IGuildChatService
public Task<string?> GetDeckLogAsync(long viewerId, int messageId, CancellationToken ct = default)
=> throw new NotImplementedException();
// No-op skeleton — will be wired in Phase 5.
public Task EmitSystemEventAsync(int guildId, long actorViewerId, Entities.Guild.GuildChatMessageType type, string? body = null, CancellationToken ct = default)
=> Task.CompletedTask;
}