mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: close the last route gaps — 100% client route+verb coverage
Every HTTP request the 2023-03-21 client can issue now reaches a handler under
the verb it actually uses: 522/522, with an empty KnownGaps allowlist. The
coverage test asserts it, so a regression fails the build rather than becoming
a 404 in the wild.
The six routes left after the previous pass all needed storage that did not
exist, so they are done here as one schema change rather than stubbed:
- ClubRolePermissionEntity (ClubId, MembershipType, six bools) backs
PUT club/{id}/permissions/{role}. A club with no stored row falls back to the
derived default policy, so every club that predates the table behaves exactly
as before. Partial writes keep the values they currently resolve to, so a
client that omits a field cannot silently revoke a permission.
- ClubAdditionalImageEntity (ClubId, Slot, ImageName) backs the gallery. The
details envelope now emits real {ImageName, Slot} rows instead of an empty
array, and DELETE clears a slot.
- ClubEntity.MinLevel and ClubEntity.ClubChatEnabled back their setters and are
read back by hasDisabledClubChat and the Club wire shape, which previously
reported hardcoded defaults.
- ChatMessageEntity.ModerationState backs PUT thread/message/{id}/moderate,
gated so an author can retract their own message but only club moderators and
admins can action anyone else's.
- TestCaseEntity.CommentsJson backs the QA comment endpoint, appending rather
than overwriting so a second tester's note cannot erase the first.
Also corrects the Joinability comment on ClubEntity, which documented
1=RequestToJoin / 2=InviteOnly. The real enum is Open=0, InviteOnly=1,
AskToJoin=2 — the inverted reading the requesttojoin handler was already fixed
away from, left behind where it could mislead the next change.
This commit is contained in:
@@ -475,6 +475,60 @@ public class ChatController(
|
||||
/// that client a 405 (and a 415 even if it had used PUT). The
|
||||
/// response is consumed by an <c>Action<MGGNHLHPHOF></c>
|
||||
/// (instr 162) — a BARE integer body, not a wrapper object.</summary>
|
||||
/// <summary>PUT <c>thread/message/{messageId}/moderate</c> — a moderator
|
||||
/// actions one message.
|
||||
///
|
||||
/// Binary evidence: <c>DLDKCILCKNA.txt</c> method
|
||||
/// <c>FGLDKEJLAKB<MGGNHLHPHOF> PLBCHNGAEML(RecNet.ChatMessage, MOFGOGFEHNN)</c>
|
||||
/// (:8365); route literal at :8600 with verb constant
|
||||
/// <c>Move rdx, 3</c> (= PUT) at :8610 and chat host <c>r8 = 8</c>; the
|
||||
/// single form field is <c>moderationState</c> (:8615). The response is the
|
||||
/// same BARE <c>MGGNHLHPHOF</c> integer every other chat mutation returns.
|
||||
///
|
||||
/// Gated to club/room moderators and admins — a plain thread member must
|
||||
/// not be able to rewrite other people's messages.</summary>
|
||||
[HttpPut("/thread/message/{messageId:long}/moderate")]
|
||||
[HttpPost("/thread/message/{messageId:long}/moderate")]
|
||||
public async Task<IActionResult> ModerateMessage(long messageId)
|
||||
{
|
||||
var message = await db.ChatMessages.FirstOrDefaultAsync(m => m.Id == messageId);
|
||||
if (message is null) return Ok(ChatThreadNotFound);
|
||||
|
||||
var raw = Request.HasFormContentType
|
||||
? (await Request.ReadFormAsync())["moderationState"].FirstOrDefault()
|
||||
: Request.Query["moderationState"].FirstOrDefault();
|
||||
if (!int.TryParse(raw, out var state)) return Ok(ChatInvalidArguments);
|
||||
|
||||
// The message author can retract their own message; anyone else needs
|
||||
// moderator standing on the club whose thread it belongs to.
|
||||
var isAuthor = message.SenderPlayerId == Me;
|
||||
if (!isAuthor)
|
||||
{
|
||||
var clubId = ClubIdFromKey(message.ThreadKey);
|
||||
var allowed = false;
|
||||
if (clubId != 0)
|
||||
{
|
||||
var membership = await db.ClubMemberships
|
||||
.FirstOrDefaultAsync(m => m.ClubId == clubId && m.PlayerId == Me);
|
||||
allowed = membership is not null &&
|
||||
ClubService.MembershipTypeFromPerms(membership.Permissions)
|
||||
>= ClubService.MembershipTypeModerator;
|
||||
}
|
||||
if (!allowed)
|
||||
{
|
||||
allowed = await db.Players
|
||||
.Where(p => p.Id == Me)
|
||||
.Select(p => p.IsAdmin || p.IsCommunityTeam)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
if (!allowed) return Ok(ChatMembershipNotFound);
|
||||
}
|
||||
|
||||
message.ModerationState = state;
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(ChatSuccess);
|
||||
}
|
||||
|
||||
[HttpPut("/thread/{chatThreadId}/rename")]
|
||||
[HttpPost("/thread/{chatThreadId}/rename")]
|
||||
public async Task<IActionResult> Rename(string chatThreadId,
|
||||
|
||||
Reference in New Issue
Block a user