86 Commits

Author SHA1 Message Date
Toutsu 2942da0c35 fix(discord): use GuildInteractionUser.Permissions instead of REST guild lookup
PR Checks / test-and-build (pull_request) Successful in 11m25s
Replace REST GetGuildAsync/GetGuildUserAsync calls with authoritative
member.Permissions from the slash-command interaction payload. Discord
already resolves channel/guild permissions in the interaction JSON, so
we no longer need to fetch the guild via REST (which returns 404 when
the bot is not a REST member of the guild, e.g. user-installed apps).

Keep a best-effort GetGuildAsync call only to obtain OwnerId for the
permission checker fallback, swallowing 404 silently.

Bump version → 3.0.7

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:44:59 +03:00
Toutsu dd9337dd20 fix(discord): cast COUNT to int for slash command list query
PR Checks / test-and-build (pull_request) Successful in 9m34s
PostgreSQL COUNT() returns bigint, but DiscordSessionListItemDto expects
int for PlayerCount and WaitlistCount. Dapper 2.1.72 in GmRelay.DiscordBot
(without Dapper.AOT) fails to materialize the record with bigint→int mismatch.
Added ::int casts to both COUNT expressions.

Bump version to 3.0.6.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:10:13 +03:00
Toutsu f6d5281af8 fix(discord): resolve slash commands from interaction payload instead of gateway cache
PR Checks / test-and-build (pull_request) Successful in 8m46s
Context.Guild in NetCord resolves the Guild object from the gateway client cache
(cache.Guilds.GetValueOrDefault(guildId)), not from the interaction JSON payload.
After a bot restart, the guild may not yet be cached when the first slash command
arrives, causing Context.Guild to be null even though the command is invoked
inside a guild channel. This produced "This command can only be used in a guild."

Changes:
- DiscordListSessionsCommand: use Context.Interaction.GuildId instead of Context.Guild.Id
- DiscordNewSessionCommand: use Context.Interaction.GuildId + REST GetGuildAsync/GetGuildUserAsync
- DiscordRescheduleCommand: same as above
- DiscordSessionInteractionModule: same fix for button interactions (CreateInput)
- Add null guard in GetResolvedPermissions for safety
- Bump version to 3.0.5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:01:53 +03:00
Toutsu 9bd5fe75c9 test: sync version assertions to 3.0.4
PR Checks / test-and-build (pull_request) Successful in 8m35s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:08:01 +03:00
Toutsu 0b45aee96d fix(discord): declare slash commands on module methods
PR Checks / test-and-build (pull_request) Successful in 8m26s
2026-05-25 16:27:29 +03:00
Toutsu eff0128d29 fix(discord): register slash command modules
PR Checks / test-and-build (pull_request) Successful in 8m27s
Register NetCord application command modules after the host is built so module-based commands are published alongside the minimal /ping command.

Update README Discord env guidance to avoid the unused DISCORD_BOT_CLIENT_ID variable.

Bump version to 3.0.2.
2026-05-25 15:49:36 +03:00
Toutsu 8214e052af bump: version 3.0.1
Deploy Telegram Bot / build-and-push (push) Successful in 4m55s
Deploy Telegram Bot / scan-images (push) Successful in 2m2s
Deploy Telegram Bot / deploy (push) Successful in 28s
Synchronize version across all files:
- Directory.Build.props → 3.0.1
- compose.yaml → gmrelay-bot/web/discord-bot:3.0.1
- deploy.yml → VERSION: 3.0.1
- NavMenu.razor → v3.0.1
- DiscordProjectStructureTests → 3.0.1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:34:25 +03:00
Toutsu 5e3028e470 fix: SameSite=Lax for auth cookie + bidirectional identity linking
Deploy Telegram Bot / build-and-push (push) Successful in 4m45s
Deploy Telegram Bot / scan-images (push) Successful in 2m7s
Deploy Telegram Bot / deploy (push) Successful in 28s
- Change cookie auth SameSite from Strict to Lax so Discord OAuth callback
can see existing Telegram auth session and perform linking instead of
creating a new standalone Discord session (root cause of broken linking).
- Add linking logic to /auth/telegram endpoint for Discord→Telegram linking.
- Add Telegram Login Widget in Profile.razor for Discord users.
- Add CookieAuthOptionsTests to verify Lax SameSite configuration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:58:25 +03:00
Toutsu baa25f2e1e feat: unify Telegram and Discord accounts via identity linking
PR Checks / test-and-build (pull_request) Successful in 7m6s
- Add V020 migration: player_links + identity_audit_log tables
- Add ISessionStore methods: ResolveEffectivePlayerId, LinkIdentity, UnlinkIdentity, GetLinkedIdentities
- Update SessionService to resolve effective player id for all permission checks
- Add /auth/discord/callback linking flow when already authenticated
- Add /api/me/identities GET/DELETE endpoints
- Add Profile.razor page for managing linked accounts
- Update NavMenu with profile link and v3.0.0 badge
- Bump version to 3.0.0 across all files

Bump version → 3.0.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:51:10 +03:00
Toutsu 72a392e652 fix: Discord OAuth CSRF cookie SameSite=None for cross-site callback
PR Checks / test-and-build (pull_request) Successful in 6m34s
- Changed __DiscordOAuthState cookie from SameSite=Strict to SameSite=None
  because Discord redirects from discord.com (cross-site) and Strict
  prevents the cookie from being sent on the callback request.
- Added logging for CSRF validation failure to aid future diagnostics.

Bump version → 2.8.1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:08:14 +03:00
Toutsu 7e02e86cd6 fix: add Discord OAuth token exchange logging for production diagnostics
PR Checks / test-and-build (pull_request) Failing after 6m20s
- Log status code and response body when Discord /oauth2/token fails
- Helps identify why ExchangeCodeAsync returns null in production

Bump version → 2.8.1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 12:46:56 +03:00
Toutsu 50f5307aac feat(web): finalize Discord OAuth and platform-agnostic auth
PR Checks / test-and-build (pull_request) Successful in 5m47s
- Bump version to 2.8.0 across all versioned files
- Fix AuthorizedSessionServiceTests for platform-agnostic identity
- Update Razor Pages to use *ForCurrentUserAsync APIs
- Add backward-compatible constructors to WebGameGroup/WebGroupManager
- Make DiscordOAuthOptions properties non-required for config binding

Bump version → 2.8.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 11:47:54 +03:00
Toutsu 5fa7e26f72 test(web): add Discord auth and platform identity tests
- DiscordAuthServiceTests: authorize URL, token exchange, profile fetch
- PlatformIdentityTests: Telegram fallback, Discord identity, avatar URL

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 11:13:08 +03:00
Toutsu 1853a7a9c7 chore(release): bump version to 2.7.2
PR Checks / test-and-build (pull_request) Successful in 8m3s
Issue: #33
2026-05-21 15:36:17 +03:00
Toutsu befb2da6a0 test: add DiscordLandingPromisesSmokeTests
Mirror TelegramLandingPromisesSmokeTests for Discord MVP:
- Join/leave/waitlist promotion via capacity rules
- Reschedule voting flow
- Direct message notifications on reschedule
- Dashboard batch update

Issue: #33
2026-05-21 15:34:03 +03:00
Toutsu d29c6c0725 test: extend DiscordSessionBatchRendererTests with regression cases
- Confirmed status blue color
- Empty player description
- Embed URL from join link
- Inline field values for capacity/waitlist/status

Issue: #33
2026-05-21 15:21:02 +03:00
Toutsu 47b22c7401 test: extend TelegramSessionBatchRendererTests with regression cases
- Empty sessions
- HTML encoding in title
- Confirmed status buttons
- No join link handling

Issue: #33
2026-05-21 15:19:08 +03:00
Toutsu b4a39c027f test: extend SessionBatchViewBuilderTests with edge cases
- Empty sessions
- Confirmed status
- Null MaxPlayers

Issue: #33
2026-05-21 15:11:01 +03:00
Toutsu fe8d5fe026 test(discord): update version assertions to 2.7.1
PR Checks / test-and-build (pull_request) Successful in 7m7s
Issue #32
2026-05-21 14:26:02 +03:00
Toutsu a2fa9aaa6c chore(release): bump version to 2.7.1
Issue #32
2026-05-21 14:24:17 +03:00
Toutsu 5b65ac4a2f chore(discord): add healthcheck to compose.yaml discord service
Issue #32
2026-05-21 14:21:35 +03:00
Toutsu feb3e08b63 feat(discord): register health check hosted service in Program.cs
Issue #32
2026-05-21 14:20:40 +03:00
Toutsu f1d8f56fec feat(discord): add health check hosted service
Issue #32
2026-05-21 14:19:50 +03:00
Toutsu 08ffc6694e chore(discord): add DISCORD_BOT_TOKEN to .env.example
Issue #32
2026-05-21 14:19:02 +03:00
Toutsu 2a707e4825 feat(platform): route scheduler notifications through platform messenger
PR Checks / test-and-build (pull_request) Successful in 7m9s
2026-05-21 12:30:35 +03:00
Toutsu 35548a03cb test(discord): update version assertions to 2.6.0
PR Checks / test-and-build (pull_request) Successful in 6m27s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:05 +03:00
Toutsu a13edf20af feat(shared): add RescheduleVotingFinalizer and ISystemClock 2026-05-20 11:54:53 +03:00
Toutsu fcd7de035f refactor(shared): extract reschedule voting types to Shared 2026-05-20 11:44:57 +03:00
Toutsu 3251846001 fix(shared): enable dapper aot for session handlers
PR Checks / test-and-build (pull_request) Successful in 6m30s
2026-05-20 09:01:34 +03:00
Toutsu 39132be4e8 feat(discord): enable session join leave buttons
PR Checks / test-and-build (pull_request) Successful in 6m6s
Move neutral join/leave handlers into GmRelay.Shared so Telegram and Discord share capacity, waitlist, duplicate-click, and schedule-update behavior.

Add Discord component routing for join_session and leave_session buttons with deferred ephemeral replies and serialized schedule message updates.

Bump version to 2.5.0 and update Discord docs.

Refs #29
2026-05-19 14:13:48 +03:00
Toutsu d55003a2a9 feat(discord): improve UX and add source-level tests for /newsession
PR Checks / test-and-build (pull_request) Successful in 5m59s
- DiscordNewSessionCommand: on success, renders session details via
  DiscordSessionBatchRenderer.Render() with embeds and action rows.
- DiscordNewSessionCommand: uses Discord emoji shortcodes for error
  and success messages (, , 💥).
- DiscordNewSessionHandlerTests: added 7 source-level structural tests
  verifying Dapper usage, NpgsqlDataSource, permission checks,
  platform neutrality, transaction safety, CancellationToken usage,
  and embed rendering in the command.

Refs issue #28

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 12:36:17 +03:00
Toutsu daa59335cc fix(discord): resolve permission checking for /newsession command
- DiscordPermissionChecker: removed dead-code userRoles overload;
  now only uses resolvedPermissions bitflag (Administrator = 0x8).
- DiscordNewSessionCommand: computes resolved permissions from guild
  user roles via Context.Guild.Users[Id].RoleIds + guild.Roles.
- DiscordNewSessionHandler: updated signature to accept ulong
  resolvedPermissions instead of unused userRoles.
- Added ILogger to command for diagnostics on unexpected errors.
- Added test: regular user with ManageServer (but not Admin) is rejected.

Refs issue #28

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 12:30:25 +03:00
Toutsu 474e7f62f7 chore: bump version to 2.4.0
Synchronized across Directory.Build.props, compose.yaml, deploy.yml,
NavMenu.razor, and project structure tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 11:36:28 +03:00
Toutsu 8666b8984e feat: register Discord session handlers and permission checker in DI
Task 5: DI wiring for DiscordNewSessionHandler, DiscordListSessionsHandler,
DiscordPermissionChecker, and DiscordPlatformMessenger.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 11:33:33 +03:00
Toutsu d373ff49ba feat(discord): add DiscordPlatformMessenger IPlatformMessenger implementation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 11:22:44 +03:00
Toutsu 95aad3a2f6 feat(discord): add /newsession slash command and handler
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 11:17:07 +03:00
Toutsu 76456cc28a feat(discord): add /listsessions slash command and handler
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 11:09:45 +03:00
Toutsu ac8f03ecc9 feat(discord): add DiscordPermissionChecker for session management rights
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 10:51:32 +03:00
Toutsu 5dddf99288 chore: bump version to 2.3.0
PR Checks / test-and-build (pull_request) Successful in 5m34s
Synchronized across Directory.Build.props, compose.yaml, deploy.yml, NavMenu.razor, and DiscordProjectStructureTests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 18:08:12 +03:00
Toutsu 1c75994722 feat: implement DiscordSessionBatchRenderer for Embed and Buttons
- Render SessionBatchViewModel into NetCord EmbedProperties + ActionRowProperties
- One embed per session with game title, Moscow date, players, capacity, waitlist, status
- Buttons map AvailableAction to ButtonProperties with platform-neutral custom IDs
- Cancelled sessions get embed but no action row
- Full sessions trigger waitlist button label
- 7 tests covering open/full/waitlist/cancelled/reschedule states

Closes #27

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 18:05:35 +03:00
Toutsu c0147fd310 test: add DiscordSessionBatchRenderer tests (RED)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:56:48 +03:00
Toutsu 05ca8061e9 feat: add Discord NetCord gateway worker
PR Checks / test-and-build (pull_request) Successful in 5m46s
Add a separate GmRelay.DiscordBot worker using NetCord Gateway with startup token validation, PostgreSQL datasource registration, slash-command setup, component interaction service registration, and lifecycle logging.

Wire the Discord service through Aspire AppHost, Docker Compose, PR checks, deploy image build/push/scan/pull steps, README docs, and synchronized version 2.2.0.

Add TDD coverage for project isolation, token validation, startup wiring, runtime wiring, and version synchronization.

Bump version -> 2.2.0
2026-05-18 16:04:31 +03:00
Toutsu e791fc2f4a refactor: make session join leave platform-neutral
PR Checks / test-and-build (pull_request) Successful in 5m3s
Convert join/leave interaction commands to PlatformUser, PlatformGroup, and PlatformMessageRef. Persist and look up participants by platform identity while keeping Telegram callbacks intact. Add V017 migration and TDD coverage. Bump version to 2.1.1.
2026-05-18 13:30:48 +03:00
Toutsu 18e702cd04 fix: validate platform schedule update target
PR Checks / test-and-build (pull_request) Successful in 13m7s
2026-05-15 18:31:17 +03:00
Toutsu 8bcd16fbc9 refactor: add platform messenger contracts
PR Checks / test-and-build (pull_request) Successful in 12m35s
Introduce platform-neutral PlatformKind, PlatformUser, PlatformGroup, and IPlatformMessenger contracts in GmRelay.Shared.

Route Telegram session schedule updates, direct notifications, interaction replies, and calendar export through TelegramPlatformMessenger while preserving existing Telegram behavior.

Bump version -> 2.0.1
2026-05-15 12:30:37 +03:00
Toutsu 11b145a967 chore: add platform identity and platform_messages for multi-platform support (#23)
PR Checks / test-and-build (pull_request) Successful in 9m36s
TDD cycle for issue #23:
- RED: 9 migration smoke tests (file presence + schema expectations)
- GREEN: V016 migration adding platform identity columns
- GREEN: CreateSessionHandler, JoinSessionHandler, Web SessionService updated
  with dual-write to legacy and new identity columns + COALESCE fallbacks
- GREEN: get_group_attendance_stats recreated for external_username
- Bump version to 2.0.0

Changes:
- V016__add_platform_identity.sql:
  - players: platform, external_user_id, external_username
  - game_groups: platform, external_group_id, external_channel_id
  - platform_messages table with cross-platform message tracking
  - Backfill all existing Telegram data into new columns
  - Recreate get_group_attendance_stats with COALESCE fallback
- V012__add_attendance_stats.sql: use COALESCE(external_username, telegram_username)
- CreateSessionHandler: dual-write + COALESCE fallbacks in SELECTs
- JoinSessionHandler: dual-write to new identity columns
- Web SessionService: dual-write to new identity columns
- PlatformIdentityMigrationTests (9 smoke tests covering all handlers)
- Version synced: Directory.Build.props, compose.yaml, deploy.yml, NavMenu.razor → 2.0.0

Legacy telegram_* columns preserved for backward compatibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 10:48:10 +03:00
Toutsu 105b3c59d7 fix: address review feedback for health check endpoints
PR Checks / test-and-build (pull_request) Successful in 8m34s
- Install wget in Web Dockerfile for compose healthcheck
- Ensure HttpListener response is always closed in BotHealthCheckHostedService
- Use ephemeral port in Bot health check test to avoid port conflicts
- Rename NpgsqlHealthCheck test to reflect actual behavior

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 11:16:58 +03:00
Toutsu 3bea327043 feat: add health check endpoints for Bot and Web
PR Checks / test-and-build (pull_request) Successful in 8m53s
- Web: add /health endpoint with PostgreSQL readiness check (returns 200+JSON or 503)
- Web: add /alive endpoint for liveness probe
- Bot: add BotHealthCheckHostedService serving /health on port 8081 via HttpListener
- Bot: expose port 8081 in Dockerfile and install wget for healthcheck
- compose.yaml: add healthcheck sections for bot and web services
- tests: add TDD tests for both health endpoints

Bump version -> 1.16.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:54:22 +03:00
Toutsu 9deccd3a9d docs: add MIT LICENSE file
PR Checks / test-and-build (pull_request) Successful in 7m7s
Add LICENSE file with MIT License text to repository root.
README.md already references it; the file was missing.

Includes TDD-verified tests ensuring LICENSE exists and contains
MIT License text, and README references it correctly.

Bump version → 1.15.1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 16:25:17 +03:00
Toutsu 121272fdfe infra: add PostgreSQL daily backup via pg_dump with rotation
PR Checks / test-and-build (pull_request) Successful in 6m24s
- Add db-backup service to compose.yaml (postgres:17-alpine + cron)
- Add pgbackups volume for backup storage
- Add scripts/restore.sh for manual restore from latest backup
- Update .env.example with BACKUP_RETENTION_DAYS and BACKUP_VOLUME_NAME
- Document backup/restore flow in README

Bump version -> 1.15.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 13:36:47 +03:00