Files
GmRelayBot/src/GmRelay.Web/Services/ISessionStore.cs
T
Toutsu 6cb2fbe610
PR Checks / test-and-build (pull_request) Successful in 7m28s
feat(web): add private club showcases with membership flow (v3.7.0)
Implements Issue #110: game masters can now publish sessions
exclusively to a club's private showcase, gated behind a
member application and approval flow. Adds a 4-state
publication_mode (None/Catalog/ClubOnly/Both) replacing the
binary is_public, plus a club_memberships table with
Pending/Active/Rejected/Left lifecycle and partial unique
index ensuring a single Active row per (group, player).

Highlights
- V030 migration: club_memberships, publication_mode, drop
  is_public, recreate partial indexes, portfolio_games gains
  publication_mode.
- PublicationMode enum + extensions in GmRelay.Shared.
- ISessionStore gains 12 membership/showcase methods;
  AuthorizedMembershipService owns the membership flow with
  GM-only approve/reject authorization.
- PublicClub / PublicMasterProfile / PublicSession: member-
  aware queries (ClubOnly visible only to Active members).
- New pages: MyClubMemberships (/profile/memberships) and
  ClubApplications (/group/{id}/applications).
- GroupDetails and EditSession switch from a bool toggle to
  a 4-state publication_mode selector.
- NavMenu adds Moji kluby, PublicLayout adds Kluby.

Tests: 4 new test files (PublicationMode, ClubMemberships,
AuthorizedMembershipService, ClubShowcaseSource) + updates
to PublicClubPages, AuthorizedSessionService/Portfolio
service FakeSessionStore, CampaignTemplatesNavigation.
493 tests pass.

Bump version 3.6.0 -> 3.7.0 across Directory.Build.props,
compose.yaml, deploy.yml, NavMenu.razor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:09:22 +03:00

189 lines
8.1 KiB
C#

using GmRelay.Shared.Domain;
using GmRelay.Shared.Features.Showcase;
namespace GmRelay.Web.Services;
public sealed record PlayerAttendanceStats(
Guid PlayerId,
string DisplayName,
string? ExternalUsername,
long TotalSessions,
long ConfirmedCount,
long DeclinedCount,
long NoResponseCount,
long WaitlistedCount,
long CancellationAffectedCount,
decimal AttendanceRate);
public sealed record SessionAuditLogEntry(
Guid Id,
Guid SessionId,
string ActorExternalUserId,
string ActorName,
string ChangeType,
string? OldValue,
string? NewValue,
DateTime ChangedAt);
public sealed record WebPublicGroupSettings(
Guid GroupId,
string GroupName,
string? PublicSlug,
bool PublicScheduleEnabled,
int PublicSessionCount);
public sealed record WebPublicSession(
Guid Id,
Guid GroupId,
string GroupName,
string? GroupSlug,
string Title,
DateTime ScheduledAt,
string Status,
int? MaxPlayers,
int ActivePlayerCount,
int WaitlistedPlayerCount,
string PublicationMode = PublicationModeExtensions.NoneValue,
bool IsMembersOnly = false,
string? MasterProfileSlug = null,
string? MasterDisplayName = null);
public sealed record WebMembership(
Guid MembershipId,
Guid GroupId,
string GroupName,
string? GroupSlug,
string Status,
string Role,
string? Message,
DateTime AppliedAt,
DateTime? DecidedAt,
string? DecidedByDisplayName);
public sealed record WebPendingApplication(
Guid MembershipId,
Guid PlayerId,
string DisplayName,
string Platform,
string? ExternalUsername,
string? Message,
DateTime AppliedAt);
public sealed record WebClubShowcaseSession(
Guid Id,
string Title,
DateTime ScheduledAt,
string Status,
string? System,
bool IsOneShot,
string? Format,
int? DurationMinutes,
string? CoverImageUrl,
int? MaxPlayers,
int ActivePlayerCount,
int WaitlistedPlayerCount,
string PublicationMode,
bool IsMembersOnly,
string? Description,
bool AllowDirectRegistration);
public sealed record WebPublicClub(
Guid GroupId,
string Name,
string Slug,
IReadOnlyList<WebPublicSession> Sessions,
string? MasterProfileSlug = null,
string? MasterDisplayName = null);
public sealed record MasterProfileSettings(
Guid PlayerId,
string DisplayName,
string? PublicSlug,
bool IsPublic,
string? Bio);
public sealed record PublicMasterClub(
Guid GroupId,
string Name,
string Slug);
public sealed record PublicMasterProfile(
string Slug,
string DisplayName,
string? Bio,
IReadOnlyList<PublicMasterClub> Clubs,
IReadOnlyList<WebPublicSession> Sessions);
public interface ISessionStore
{
Task<List<WebGameGroup>> GetGroupsForUserAsync(string platform, string externalUserId);
Task<WebGameGroup?> GetGroupAsync(Guid groupId);
Task<WebPublicGroupSettings?> GetPublicGroupSettingsAsync(Guid groupId);
Task UpdatePublicGroupSettingsAsync(Guid groupId, string? publicSlug, bool publicScheduleEnabled);
Task SetSessionPublicationModeAsync(Guid sessionId, Guid groupId, PublicationMode mode);
Task SetBatchPublicationModeAsync(Guid batchId, Guid groupId, PublicationMode mode);
Task<WebPublicClub?> GetPublicClubBySlugAsync(string slug, Guid? viewerPlayerId);
Task<WebPublicSession?> GetPublicSessionAsync(Guid sessionId, Guid? viewerPlayerId);
Task<bool> IsGroupManagerAsync(Guid groupId, string platform, string externalUserId);
Task<bool> IsGroupOwnerAsync(Guid groupId, string platform, string externalUserId);
Task<bool> IsActiveClubMemberAsync(Guid groupId, Guid playerId);
Task<Guid?> GetPlayerIdByPlatformIdentityAsync(string platform, string externalUserId);
Task<List<WebGroupManager>> GetGroupManagersAsync(Guid groupId);
Task<List<WebSession>> GetUpcomingSessionsAsync(Guid groupId);
Task<WebSession?> GetSessionAsync(Guid sessionId);
Task<WebSessionBatch?> GetBatchAsync(Guid batchId);
Task UpdateSessionAsync(Guid sessionId, Guid groupId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers);
Task PromoteWaitlistedPlayerAsync(Guid sessionId, Guid groupId);
Task UpdateBatchDetailsAsync(Guid batchId, Guid groupId, string title, string joinLink);
Task UpdateBatchNotificationModeAsync(Guid batchId, Guid groupId, SessionNotificationMode notificationMode);
Task RescheduleBatchAsync(Guid batchId, Guid groupId, DateTime firstScheduledAt, int intervalDays);
Task<WebSessionBatch> CloneBatchAsync(Guid batchId, Guid groupId, BatchCloneInterval interval);
Task<List<WebCampaignTemplate>> GetCampaignTemplatesAsync(Guid groupId);
Task<WebCampaignTemplate?> GetCampaignTemplateAsync(Guid templateId);
Task<WebCampaignTemplate> CreateCampaignTemplateAsync(Guid groupId, CreateCampaignTemplateRequest request);
Task DeleteCampaignTemplateAsync(Guid templateId, Guid groupId);
Task<WebSessionBatch> CreateBatchFromTemplateAsync(Guid templateId, Guid groupId, DateTime firstScheduledAt);
Task AddGroupCoGmAsync(Guid groupId, string ownerPlatform, string ownerExternalUserId, string coGmPlatform, string coGmExternalUserId, string displayName, string? externalUsername);
Task RemoveGroupCoGmAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId);
Task<List<WebParticipant>> GetSessionParticipantsAsync(Guid sessionId);
Task RemovePlayerFromSessionAsync(Guid sessionId, Guid groupId, Guid participantId);
Task<List<PlayerAttendanceStats>> GetGroupAttendanceStatsAsync(Guid groupId);
Task LogSessionChangeAsync(Guid sessionId, string actorExternalUserId, string actorName, string changeType, string? oldValue, string? newValue);
Task<List<SessionAuditLogEntry>> GetSessionHistoryAsync(Guid sessionId);
Task UpsertDiscordUserAsync(string discordId, string displayName, string? avatarUrl);
Task<MasterProfileSettings?> GetMasterProfileSettingsAsync(string platform, string externalUserId);
Task UpdateMasterProfileSettingsAsync(string platform, string externalUserId, string? publicSlug, bool isPublic, string displayName, string? bio);
Task<PublicMasterProfile?> GetPublicMasterProfileBySlugAsync(string slug, Guid? viewerPlayerId);
// --- Identity linking (issue #35) ---
Task<Guid?> ResolveEffectivePlayerIdAsync(string platform, string externalUserId);
Task<List<LinkedIdentity>> GetLinkedIdentitiesAsync(string platform, string externalUserId);
Task LinkIdentityAsync(string currentPlatform, string currentExternalUserId, string targetPlatform, string targetExternalUserId, string? currentName);
Task UnlinkIdentityAsync(string currentPlatform, string currentExternalUserId, string targetPlatform, string targetExternalUserId);
Task UpsertPlayerAsync(string platform, string externalUserId, string displayName, string? avatarUrl);
// --- Showcase / game catalog (issue #39) ---
Task<IReadOnlyList<ShowcaseSessionDto>> GetShowcaseSessionsAsync(ShowcaseFilter filter, int page, int pageSize);
Task<ShowcaseSessionDto?> GetShowcaseSessionAsync(Guid sessionId);
Task<bool> RegisterFromShowcaseAsync(Guid sessionId, string platform, string externalUserId, string displayName);
// --- Private club showcases / memberships (issue #110) ---
Task<IReadOnlyList<WebClubShowcaseSession>> GetClubShowcaseSessionsAsync(Guid groupId, Guid? viewerPlayerId, int page, int pageSize);
Task<int> GetPendingApplicationsCountAsync(Guid groupId);
Task<List<WebPendingApplication>> GetPendingApplicationsAsync(Guid groupId);
Task<List<WebMembership>> GetMembershipsForPlayerAsync(Guid playerId);
Task<Guid> ApplyForMembershipAsync(Guid groupId, Guid playerId, string? message);
Task ApproveMembershipAsync(Guid membershipId, Guid approverPlayerId);
Task RejectMembershipAsync(Guid membershipId, Guid approverPlayerId);
Task LeaveClubMembershipAsync(Guid membershipId, Guid playerId);
Task<Guid?> GetGroupIdForMembershipAsync(Guid membershipId);
}
public sealed record LinkedIdentity(
string Platform,
string ExternalUserId,
string DisplayName,
string? ExternalUsername,
string? AvatarUrl,
DateTime LinkedAt);