Files
SVSimServer/SVSim.Database/Services/Guild/GuildChatService.cs
gamer147 ab1e23b7cb feat(guild_chat): post text + stamp
Implements PostTextOrStampAsync: validates membership (NotInGuild),
validates stamp id against UsableStampList (PermissionDenied), rejects
type other than 0 or 1, allocates next per-guild message_id, appends row.
Wires /guild_chat/post controller action mapping failures to result_code=2.
Response is GuildChatPostResponse (empty body per spec). 7 integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:58:18 -04:00

116 lines
4.4 KiB
C#

using SVSim.Database.Entities.Guild;
using SVSim.Database.Models.Config;
using SVSim.Database.Repositories.Guild;
using SVSim.Database.Services;
namespace SVSim.Database.Services.Guild;
public sealed class GuildChatService : IGuildChatService
{
private readonly IGuildMemberRepository _members;
private readonly IGuildChatMessageRepository _msgs;
private readonly IGameConfigService _cfg;
public GuildChatService(
IGuildMemberRepository members,
IGuildChatMessageRepository msgs,
IGameConfigService cfg)
{
_members = members;
_msgs = msgs;
_cfg = cfg;
}
// ── 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);
}
// ── Post text / stamp ────────────────────────────────────────────────────
public async Task<ChatPostResult> PostTextOrStampAsync(long viewerId, int type, string message, CancellationToken ct = default)
{
var m = await _members.GetMembershipAsync(viewerId, ct);
if (m is null) return new(false, null);
var cfg = _cfg.Get<GuildConfig>();
if (type == 1)
{
if (!int.TryParse(message, out var stampId) || !cfg.UsableStampList.Contains(stampId))
return new(false, null);
}
else if (type != 0)
{
// Only NORMAL (0) and STAMP (1) are valid via this endpoint.
return new(false, null);
}
var now = DateTime.UtcNow;
var nextId = await _msgs.GetMaxMessageIdAsync(m.GuildId, ct) + 1;
var stored = await _msgs.AppendAsync(new GuildChatMessage
{
GuildId = m.GuildId,
MessageId = nextId,
AuthorViewerId = viewerId,
MessageType = type == 1 ? GuildChatMessageType.Stamp : GuildChatMessageType.Normal,
Body = message,
CreatedAt = now,
}, ct);
return new(true, stored.MessageId);
}
public Task<ChatPostResult> PostDeckAsync(long viewerId, string deckPayloadJson, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<bool> DeleteDeckAsync(long viewerId, int messageId, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<ChatPostResult> PostReplayAsync(long viewerId, string replayPayloadJson, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<string?> GetReplayDetailAsync(long viewerId, int messageId, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<string?> GetDeckLogAsync(long viewerId, int messageId, CancellationToken ct = default)
=> throw new NotImplementedException();
}