feat(guild): leave + remove with leader-leaves rule
- LeaveAsync: regular/subleader removes self; leader blocked if members remain; sole-member leader auto-routes to BreakupAsync. - RemoveAsync: leader-only kick (SubLeader has no remove authority per GuildManager.cs decompile); self-remove and cross-guild target rejected. - GuildController: wired /guild/leave (BaseRequest) and /guild/remove (GuildRemoveRequest.remove_viewer_id field). - SpyGuildChatService: extended Emissions with EmissionsWithBody tuple to allow body-content assertions in leave/remove tests. - 8 new integration tests; all 1493 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -362,11 +362,46 @@ public sealed class GuildService : IGuildService
|
||||
return GuildOpResult.Ok;
|
||||
}
|
||||
|
||||
public Task<GuildOpResult> LeaveAsync(long viewerId, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
public async Task<GuildOpResult> LeaveAsync(long viewerId, CancellationToken ct = default)
|
||||
{
|
||||
var m = await _members.GetMembershipAsync(viewerId, ct);
|
||||
if (m is null) return new(GuildOpResultCode.NotInGuild);
|
||||
|
||||
public Task<GuildOpResult> RemoveAsync(long callerViewerId, long targetViewerId, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
var memberCount = await _members.CountByGuildAsync(m.GuildId, ct);
|
||||
|
||||
if (m.Role == GuildRole.Leader)
|
||||
{
|
||||
// Sole-member leader: auto-breakup.
|
||||
if (memberCount == 1) return await BreakupAsync(viewerId, ct);
|
||||
// Leader with remaining members: blocked.
|
||||
return new(GuildOpResultCode.LeaderLeaveBlocked);
|
||||
}
|
||||
|
||||
// Regular or SubLeader: remove self.
|
||||
await _members.RemoveAsync(m.GuildId, viewerId, ct);
|
||||
await _viewers.ClearGuildIdAsync(viewerId, ct);
|
||||
await _chat.EmitSystemEventAsync(m.GuildId, viewerId, GuildChatMessageType.Leave, body: null, ct);
|
||||
return GuildOpResult.Ok;
|
||||
}
|
||||
|
||||
public async Task<GuildOpResult> RemoveAsync(long callerViewerId, long targetViewerId, CancellationToken ct = default)
|
||||
{
|
||||
var caller = await _members.GetMembershipAsync(callerViewerId, ct);
|
||||
if (caller is null) return new(GuildOpResultCode.NotInGuild);
|
||||
// Only Leader may kick (GuildManager.cs has no HasAuthorityRemove — client has no SubLeader kick UI).
|
||||
if (caller.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied);
|
||||
// Cannot kick yourself.
|
||||
if (callerViewerId == targetViewerId) return new(GuildOpResultCode.PermissionDenied);
|
||||
|
||||
var target = await _members.GetMembershipAsync(targetViewerId, ct);
|
||||
if (target is null || target.GuildId != caller.GuildId) return new(GuildOpResultCode.TargetNotInGuild);
|
||||
|
||||
await _members.RemoveAsync(caller.GuildId, targetViewerId, ct);
|
||||
await _viewers.ClearGuildIdAsync(targetViewerId, ct);
|
||||
await _chat.EmitSystemEventAsync(caller.GuildId, callerViewerId, GuildChatMessageType.Remove,
|
||||
body: targetViewerId.ToString(), ct);
|
||||
return GuildOpResult.Ok;
|
||||
}
|
||||
|
||||
public Task<GuildOpResult> ChangeRoleAsync(long callerViewerId, long targetViewerId, int newRoleId, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
Reference in New Issue
Block a user