- 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>
112 lines
4.5 KiB
C#
112 lines
4.5 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SVSim.Database;
|
|
using SVSim.Database.Services.Guild;
|
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Common;
|
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild;
|
|
using SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat;
|
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
|
|
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
|
|
|
/// <summary>/guild_chat/* — 7 endpoints. See docs/api-spec/endpoints/post-login/guild_chat-*.md.</summary>
|
|
[Route("guild_chat")]
|
|
public sealed class GuildChatController : SVSimController
|
|
{
|
|
private readonly IGuildChatService _chat;
|
|
private readonly SVSimDbContext _db;
|
|
|
|
public GuildChatController(IGuildChatService chat, SVSimDbContext db)
|
|
{
|
|
_chat = chat;
|
|
_db = db;
|
|
}
|
|
|
|
[HttpPost("messages")]
|
|
public async Task<ActionResult<GuildChatMessagesResponse>> Messages(
|
|
[FromBody] GuildChatMessagesRequest req,
|
|
CancellationToken ct)
|
|
{
|
|
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
|
|
|
|
var window = await _chat.GetWindowAsync(
|
|
viewerId,
|
|
(int)req.StartMessageId,
|
|
req.Direction,
|
|
req.WaitInterval,
|
|
ct);
|
|
|
|
// Build chat_message list
|
|
var chatMessages = window.Messages.Select(m => new ChatMessageDto
|
|
{
|
|
ViewerId = m.AuthorViewerId,
|
|
MessageId = m.MessageId,
|
|
MessageType = (int)m.MessageType,
|
|
CreateTime = new DateTimeOffset(m.CreatedAt, TimeSpan.Zero).ToUnixTimeSeconds(),
|
|
Message = m.Body,
|
|
// deck / replay / room remain null until Task 17
|
|
}).ToList();
|
|
|
|
// Build deduplicated users[] catalog from message authors
|
|
var authorIds = window.Messages
|
|
.Select(m => m.AuthorViewerId)
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
var viewers = authorIds.Count == 0
|
|
? new Dictionary<long, SVSim.Database.Models.Viewer>()
|
|
: await _db.Viewers
|
|
.AsNoTracking()
|
|
.Include(v => v.Info.SelectedEmblem)
|
|
.Include(v => v.Info.SelectedDegree)
|
|
.Where(v => authorIds.Contains(v.Id))
|
|
.ToDictionaryAsync(v => v.Id, ct);
|
|
|
|
var users = authorIds.Select(vid =>
|
|
{
|
|
viewers.TryGetValue(vid, out var v);
|
|
return new ChatUserDto
|
|
{
|
|
ViewerId = vid,
|
|
Name = v?.DisplayName ?? "",
|
|
EmblemId = v?.Info?.SelectedEmblem?.Id is > 0 ? v.Info.SelectedEmblem.Id : 100_000_000L,
|
|
CountryCode = v?.Info?.CountryCode ?? "",
|
|
Rank = 1, // TODO: real rank when rank tracking lands
|
|
DegreeId = v?.Info?.SelectedDegree?.Id ?? 0,
|
|
};
|
|
}).ToList();
|
|
|
|
return new GuildChatMessagesResponse
|
|
{
|
|
MaintenanceCardList = new(),
|
|
Users = users,
|
|
ChatMessage = chatMessages,
|
|
WaitInterval = window.WaitIntervalSeconds,
|
|
};
|
|
}
|
|
|
|
[HttpPost("post")]
|
|
public Task<ActionResult<GuildChatPostResponse>> Post([FromBody] GuildChatPostRequest req, CancellationToken ct)
|
|
=> Task.FromResult<ActionResult<GuildChatPostResponse>>(new GuildChatPostResponse());
|
|
|
|
[HttpPost("add_deck")]
|
|
public Task<ActionResult<EmptyResponse>> AddDeck([FromBody] GuildChatAddDeckRequest req, CancellationToken ct)
|
|
=> Task.FromResult<ActionResult<EmptyResponse>>(new EmptyResponse());
|
|
|
|
[HttpPost("delete_deck")]
|
|
public Task<ActionResult<EmptyResponse>> DeleteDeck([FromBody] GuildChatDeleteDeckRequest req, CancellationToken ct)
|
|
=> Task.FromResult<ActionResult<EmptyResponse>>(new EmptyResponse());
|
|
|
|
[HttpPost("add_replay")]
|
|
public Task<ActionResult<EmptyResponse>> AddReplay([FromBody] GuildChatAddReplayRequest req, CancellationToken ct)
|
|
=> Task.FromResult<ActionResult<EmptyResponse>>(new EmptyResponse());
|
|
|
|
[HttpPost("replay_detail")]
|
|
public Task<ActionResult<GuildChatReplayDetailResponse>> ReplayDetail([FromBody] GuildChatReplayDetailRequest req, CancellationToken ct)
|
|
=> Task.FromResult<ActionResult<GuildChatReplayDetailResponse>>(new GuildChatReplayDetailResponse());
|
|
|
|
[HttpPost("deck_log")]
|
|
public Task<ActionResult<GuildChatDeckLogResponse>> DeckLog([FromBody] GuildChatDeckLogRequest req, CancellationToken ct)
|
|
=> Task.FromResult<ActionResult<GuildChatDeckLogResponse>>(new GuildChatDeckLogResponse());
|
|
}
|