diplomail (Stage B): admin/owner sends + lifecycle hooks
Item 7 of the spec wants game-state and membership-state changes to land as durable inbox entries the affected players can re-read after the fact — push alone times out of the 5-minute ring buffer. Stage B adds the admin-kind send matrix (owner-driven via /user, site-admin driven via /admin) plus the lobby lifecycle hooks: paused / cancelled emit a broadcast system mail to active members, kick / ban emit a single-recipient system mail to the affected user (which they keep read access to even after the membership row is revoked, per item 8). Migration relaxes diplomail_messages_kind_sender_chk so an owner sending kind=admin keeps sender_kind=player; the new LifecyclePublisher dep on lobby.Service is wired through a thin adapter in cmd/backend/main, mirroring how lobby's notification publisher is plumbed today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,7 @@ func (s *Service) AdminBanMember(ctx context.Context, gameID, userID uuid.UUID,
|
||||
zap.String("membership_id", updated.MembershipID.String()),
|
||||
zap.Error(pubErr))
|
||||
}
|
||||
s.emitMembershipLifecycleMail(ctx, updated, MembershipStatusBlocked, true, reason)
|
||||
_ = game
|
||||
return updated, nil
|
||||
}
|
||||
@@ -142,9 +143,44 @@ func (s *Service) changeMembershipStatus(
|
||||
zap.String("kind", notificationKind),
|
||||
zap.Error(pubErr))
|
||||
}
|
||||
s.emitMembershipLifecycleMail(ctx, updated, newStatus, callerIsAdmin, "")
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// emitMembershipLifecycleMail asks the diplomail publisher to drop a
|
||||
// durable explanation into the kicked player's inbox. The mail
|
||||
// survives the membership row going to `removed` / `blocked` so the
|
||||
// player keeps read access to it (soft-access rule, item 8).
|
||||
func (s *Service) emitMembershipLifecycleMail(ctx context.Context, membership Membership, newStatus string, callerIsAdmin bool, reason string) {
|
||||
var kind string
|
||||
switch newStatus {
|
||||
case MembershipStatusRemoved:
|
||||
kind = LifecycleKindMembershipRemoved
|
||||
case MembershipStatusBlocked:
|
||||
kind = LifecycleKindMembershipBlocked
|
||||
default:
|
||||
return
|
||||
}
|
||||
actor := "the game owner"
|
||||
if callerIsAdmin {
|
||||
actor = "an administrator"
|
||||
}
|
||||
target := membership.UserID
|
||||
ev := LifecycleEvent{
|
||||
GameID: membership.GameID,
|
||||
Kind: kind,
|
||||
Actor: actor,
|
||||
Reason: reason,
|
||||
TargetUser: &target,
|
||||
}
|
||||
if err := s.deps.Diplomail.PublishLifecycle(ctx, ev); err != nil {
|
||||
s.deps.Logger.Warn("publish membership lifecycle mail failed",
|
||||
zap.String("membership_id", membership.MembershipID.String()),
|
||||
zap.String("kind", kind),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) canManageMembership(game GameRecord, membership Membership, callerUserID *uuid.UUID, allowSelf bool) bool {
|
||||
if game.Visibility == VisibilityPublic {
|
||||
// Public-game membership management is admin-only.
|
||||
|
||||
Reference in New Issue
Block a user