Implements GuildService.BreakupAsync (soft-delete guild, hard-delete dependents, clear viewer GuildId pointers). Wires /guild/breakup, /guild/emblem_list, /guild/others_info in GuildController. Fixes ToDetailDto to accept and populate leader_name from the member list. Adds GuildServiceBreakupTests (3 tests: cascade delete, permission guard, leader_name HTTP regression). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
190 lines
8.3 KiB
C#
190 lines
8.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using SVSim.Database.Entities.Guild;
|
|
using SVSim.Database.Repositories.Guild;
|
|
using SVSim.Database.Repositories.Viewer;
|
|
using SVSim.Database.Services;
|
|
|
|
namespace SVSim.Database.Services.Guild;
|
|
|
|
public sealed class GuildService : IGuildService
|
|
{
|
|
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 IGuildIdGenerator _idGen;
|
|
private readonly IGuildChatService _chat;
|
|
private readonly IViewerRepository _viewers;
|
|
private readonly SVSimDbContext _db;
|
|
|
|
public GuildService(
|
|
IGuildRepository guilds,
|
|
IGuildMemberRepository members,
|
|
IGuildInviteRepository invites,
|
|
IGuildJoinRequestRepository joinRequests,
|
|
IGuildChatMessageRepository chatMessages,
|
|
IGameConfigService config,
|
|
IGuildIdGenerator idGen,
|
|
IGuildChatService chat,
|
|
IViewerRepository viewers,
|
|
SVSimDbContext db)
|
|
{
|
|
_guilds = guilds;
|
|
_members = members;
|
|
_invites = invites;
|
|
_joinRequests = joinRequests;
|
|
_chatMessages = chatMessages;
|
|
_config = config;
|
|
_idGen = idGen;
|
|
_chat = chat;
|
|
_viewers = viewers;
|
|
_db = db;
|
|
}
|
|
|
|
public async Task<GuildFullView?> GetMyGuildAsync(long viewerId, CancellationToken ct = default)
|
|
{
|
|
var membership = await _members.GetMembershipAsync(viewerId, ct);
|
|
if (membership is null) return null;
|
|
|
|
var guild = await _guilds.GetWithMembersAsync(membership.GuildId, ct);
|
|
if (guild is null || guild.BreakupAt is not null) return null;
|
|
|
|
int joinReqCount = await _joinRequests.CountPendingForGuildAsync(guild.GuildId, ct);
|
|
int inviteCount = await _invites.CountPendingForInviteeAsync(viewerId, ct);
|
|
|
|
return new GuildFullView(guild, guild.Members, joinReqCount, inviteCount);
|
|
}
|
|
|
|
public Task<Entities.Guild.Guild?> GetActiveAsync(int guildId, CancellationToken ct = default)
|
|
=> _guilds.GetActiveByIdAsync(guildId, ct);
|
|
|
|
public Task<IReadOnlyList<GuildSearchEntry>> SearchAsync(string name, int activity, int joinCondition, int memberBucket, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public async Task<GuildOpResult> CreateAsync(long viewerId, CreateGuildRequest req, CancellationToken ct = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(req.Name) || req.Name.Length > 64)
|
|
return new(GuildOpResultCode.NameInvalid);
|
|
if (req.Activity is < 1 or > 16)
|
|
return new(GuildOpResultCode.NameInvalid);
|
|
if (req.JoinCondition is < 1 or > 3)
|
|
return new(GuildOpResultCode.NameInvalid);
|
|
|
|
if (await _members.GetMembershipAsync(viewerId, ct) is not null)
|
|
return new(GuildOpResultCode.AlreadyInGuild);
|
|
if (await _guilds.NameExistsAsync(req.Name, ct))
|
|
return new(GuildOpResultCode.NameTaken);
|
|
|
|
var viewer = await _db.Viewers
|
|
.Include(v => v.Info.SelectedEmblem)
|
|
.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
|
|
if (viewer is null) return new(GuildOpResultCode.PermissionDenied);
|
|
|
|
var id = await _idGen.NextAsync(
|
|
(cand, c) => _guilds.GetByIdAsync(cand, c).ContinueWith(t => t.Result is not null, c),
|
|
ct);
|
|
var now = DateTime.UtcNow;
|
|
var g = new Entities.Guild.Guild
|
|
{
|
|
GuildId = id,
|
|
Name = req.Name,
|
|
Description = "",
|
|
LeaderViewerId = viewerId,
|
|
EmblemId = DefaultEmblemId(viewer),
|
|
Activity = (GuildActivity)req.Activity,
|
|
JoinCondition = (GuildJoinCondition)req.JoinCondition,
|
|
CreatedAt = now,
|
|
};
|
|
await _guilds.AddAsync(g, ct);
|
|
await _members.AddAsync(
|
|
new GuildMember { GuildId = id, ViewerId = viewerId, Role = GuildRole.Leader, JoinedAt = now },
|
|
ct);
|
|
|
|
// Side-effects: clear any pending invites + cancel pending join requests for this viewer.
|
|
await _invites.ConsumePendingForViewerAsync(viewerId, now, ct);
|
|
await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct);
|
|
|
|
// Update Viewer.GuildId pointer for quick lookups.
|
|
viewer.GuildId = id;
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
await _chat.EmitSystemEventAsync(id, viewerId, GuildChatMessageType.CreateGuild, body: null, ct);
|
|
return new(GuildOpResultCode.Ok, GuildId: id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the viewer's currently-equipped emblem id, or 100000000 (default) if none.
|
|
/// </summary>
|
|
private static long DefaultEmblemId(Models.Viewer viewer)
|
|
{
|
|
var emblemId = viewer.Info?.SelectedEmblem?.Id;
|
|
return emblemId is > 0 ? emblemId.Value : 100_000_000L;
|
|
}
|
|
|
|
public Task<GuildOpResult> UpdateAsync(long viewerId, UpdateGuildRequest req, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> UpdateDescriptionAsync(long viewerId, string description, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> UpdateEmblemAsync(long viewerId, long emblemId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public async Task<GuildOpResult> BreakupAsync(long viewerId, CancellationToken ct = default)
|
|
{
|
|
var membership = await _members.GetMembershipAsync(viewerId, ct);
|
|
if (membership is null) return new(GuildOpResultCode.NotInGuild);
|
|
if (membership.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied);
|
|
|
|
var now = DateTime.UtcNow;
|
|
var members = await _members.ListByGuildAsync(membership.GuildId, ct);
|
|
foreach (var m in members) await _viewers.ClearGuildIdAsync(m.ViewerId, ct);
|
|
|
|
await _chatMessages.DeleteAllForGuildAsync(membership.GuildId, ct);
|
|
await _invites.DeleteAllForGuildAsync(membership.GuildId, ct);
|
|
await _joinRequests.DeleteAllForGuildAsync(membership.GuildId, ct);
|
|
await _members.DeleteAllForGuildAsync(membership.GuildId, ct);
|
|
await _guilds.MarkBrokenUpAsync(membership.GuildId, now, ct);
|
|
|
|
return GuildOpResult.Ok;
|
|
}
|
|
|
|
public Task<GuildOpResult> InviteAsync(long callerViewerId, long targetViewerId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> CancelInviteAsync(long callerViewerId, long targetViewerId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> RejectInviteAsync(long callerViewerId, int guildId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> JoinAsync(long viewerId, int guildId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> CancelJoinRequestAsync(long viewerId, int guildId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> AcceptJoinRequestAsync(long callerViewerId, long applicantViewerId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> RejectJoinRequestAsync(long callerViewerId, long applicantViewerId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> LeaveAsync(long viewerId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> RemoveAsync(long callerViewerId, long targetViewerId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<GuildOpResult> ChangeRoleAsync(long callerViewerId, long targetViewerId, int newRoleId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<IReadOnlyList<Entities.Guild.GuildInvite>> ListPendingInvitesForMeAsync(long viewerId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<IReadOnlyList<Entities.Guild.GuildJoinRequest>> ListPendingJoinRequestsForMyGuildAsync(long viewerId, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
}
|