dubs-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +280 -0
- package/CLAUDE.md +46 -0
- package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
- package/CURRENT_SESSION.md +171 -0
- package/CURRENT_SESSION_DRAW.md +516 -0
- package/MARCH_MADNESS_SURVIVOR.md +254 -0
- package/PANDA.md +166 -0
- package/Procfile +4 -0
- package/README.md +476 -0
- package/controllers/livescoresController.js +376 -0
- package/controllers/pickemController.js +554 -0
- package/controllers/survivorAdminController.js +887 -0
- package/controllers/survivorController.js +623 -0
- package/cron/oracleMonitor.js +77 -0
- package/cron/pickemOracleMonitor.js +73 -0
- package/data/jackpot-history.json +952 -0
- package/data/ncaaTeams.js +406 -0
- package/documentation/API_SECURITY_GUIDE.md +327 -0
- package/documentation/ARCADE_API.md +593 -0
- package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
- package/documentation/ARCADE_QUICKSTART.md +242 -0
- package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
- package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
- package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
- package/documentation/CLAIM_STATUS_FIX.md +67 -0
- package/documentation/CLI_TOOL_GUIDE.md +372 -0
- package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
- package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
- package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
- package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
- package/documentation/DATABASE_QUERIES.md +269 -0
- package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
- package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
- package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
- package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
- package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
- package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
- package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
- package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
- package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
- package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
- package/documentation/HEROKU_DEPLOYMENT.md +134 -0
- package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
- package/documentation/JACKPOT_API.md +521 -0
- package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
- package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
- package/documentation/JWT_QUICK_SETUP.md +268 -0
- package/documentation/JWT_TESTING_GUIDE.md +404 -0
- package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
- package/documentation/KEEPER_SETUP.md +206 -0
- package/documentation/KEEPER_STATE_MACHINE.md +423 -0
- package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
- package/documentation/LOCAL_VOTING_TEST.md +279 -0
- package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
- package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
- package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
- package/documentation/PNL_TRACKING_SETUP.md +189 -0
- package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
- package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
- package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
- package/documentation/QUICK_AUTH_SETUP.md +99 -0
- package/documentation/QUICK_DEPLOY.md +224 -0
- package/documentation/QUICK_FIX.md +114 -0
- package/documentation/QUICK_START.md +152 -0
- package/documentation/REFEREE_MODE_GUIDE.md +392 -0
- package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
- package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
- package/documentation/RUN_MIGRATION_NOW.md +39 -0
- package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
- package/documentation/SETUP_GUIDE.md +184 -0
- package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
- package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
- package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
- package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
- package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
- package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
- package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
- package/ecosystem.config.js +65 -0
- package/env.template +125 -0
- package/middleware/apiKeyAuth.js +136 -0
- package/middleware/authenticate.js +214 -0
- package/middleware/developerUserAuth.js +76 -0
- package/middleware/socketAuth.js +69 -0
- package/package.json +49 -0
- package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
- package/postman/Dubs-API-v1.postman_collection.json +205 -0
- package/postman/Dubs_Developer_API.postman_collection.json +662 -0
- package/postman/QUICKSTART.md +118 -0
- package/postman/QUICK_REFERENCE.md +246 -0
- package/postman/README.md +71 -0
- package/postman/VOTING_API_GUIDE.md +426 -0
- package/refactor/Animations.md +148 -0
- package/refactor/Chat.md +252 -0
- package/routes/actionsRoutes.js +699 -0
- package/routes/adminRoutes.js +370 -0
- package/routes/analyticsRoutes.js +1262 -0
- package/routes/arcadeRoutes.js +557 -0
- package/routes/authRoutes.js +2310 -0
- package/routes/avatarRoutes.js +85 -0
- package/routes/botRoutes.js +211 -0
- package/routes/chatRoutes.js +377 -0
- package/routes/cryptoPriceRoutes.js +105 -0
- package/routes/developerRoutes.js +4201 -0
- package/routes/deviceRoutes.js +214 -0
- package/routes/dmRoutes.js +167 -0
- package/routes/esportsRoutes.js +806 -0
- package/routes/exchangeRateRoutes.js +233 -0
- package/routes/gamesRoutes.js +3028 -0
- package/routes/jackpotRoutes.js +754 -0
- package/routes/keeperMonitoringRoutes.js +156 -0
- package/routes/keeperWebhookRoutes.js +466 -0
- package/routes/livescoresRoutes.js +31 -0
- package/routes/pickemAdminRoutes.js +199 -0
- package/routes/pickemRoutes.js +231 -0
- package/routes/playerStatsRoutes.js +147 -0
- package/routes/portfolioRoutes.js +217 -0
- package/routes/promoRoutes.js +418 -0
- package/routes/referralEarningsRoutes.js +392 -0
- package/routes/socialRoutes.js +459 -0
- package/routes/sportsRoutes.js +1271 -0
- package/routes/survivorAdminRoutes.js +345 -0
- package/routes/survivorRoutes.js +756 -0
- package/routes/uploadRoutes.js +256 -0
- package/routes/userProfileRoutes.js +244 -0
- package/routes/whatsNewRoutes.js +331 -0
- package/scripts/.claude/settings.local.json +15 -0
- package/scripts/README.md +170 -0
- package/scripts/RESTART_EVERYTHING.sh +104 -0
- package/scripts/add-claim-columns.sql +48 -0
- package/scripts/add-crypto-prices-cache.sql +27 -0
- package/scripts/add-exchange-rates-cache.sql +40 -0
- package/scripts/add-game-invite-column.sql +23 -0
- package/scripts/add-game-invite-notification.sql +33 -0
- package/scripts/add-game-invite-telegram-pref.sql +16 -0
- package/scripts/add-game-joined-notification.sql +16 -0
- package/scripts/add-game-joined-pref.js +40 -0
- package/scripts/add-game-joined-preference.sql +6 -0
- package/scripts/add-game-start-notifications.sql +41 -0
- package/scripts/add-notification-flags-to-games.sql +55 -0
- package/scripts/add-pending-game-dismissals.sql +19 -0
- package/scripts/add-preferred-currency.sql +34 -0
- package/scripts/add-winner-columns.js +61 -0
- package/scripts/add_mention_system.sql +53 -0
- package/scripts/add_payment_system.sql +96 -0
- package/scripts/add_sports_event_id_column.sql +22 -0
- package/scripts/analyze-cohort-data-heroku.js +276 -0
- package/scripts/analyze-cohort-data.js +295 -0
- package/scripts/analyze-prod-cohorts.sh +10 -0
- package/scripts/backfill-matchup-images.js +245 -0
- package/scripts/backfill-missing-signatures.js +175 -0
- package/scripts/backfill-referral-earnings.js +202 -0
- package/scripts/check-chat-schema.js +130 -0
- package/scripts/check-db.sh +14 -0
- package/scripts/check_oracle_in_game.js +54 -0
- package/scripts/cleanup-database.js +193 -0
- package/scripts/clear-notification-cache.js +85 -0
- package/scripts/convert-mnemonic.js +50 -0
- package/scripts/create-users-table.sql +44 -0
- package/scripts/debug-cohort-counts.js +248 -0
- package/scripts/debug-winner-calc.js +84 -0
- package/scripts/deploy-payment-system.sh +118 -0
- package/scripts/deploy-to-heroku.sh +63 -0
- package/scripts/diagnose-locked-round.js +143 -0
- package/scripts/dubs-cli.js +720 -0
- package/scripts/dump-account.js +65 -0
- package/scripts/find-vrf-offset.js +48 -0
- package/scripts/fix-chat-notifications-constraint.sql +122 -0
- package/scripts/fix-claim-columns.js +124 -0
- package/scripts/fix-constraint-now.js +44 -0
- package/scripts/fix-lock-timestamps.js +96 -0
- package/scripts/fix-locked-round.sh +126 -0
- package/scripts/fix-missing-badges.sql +91 -0
- package/scripts/fix-payment-notifications.sql +41 -0
- package/scripts/force-new-round.js +55 -0
- package/scripts/force-resolve-and-claim.js +278 -0
- package/scripts/important/README.md +115 -0
- package/scripts/important/authority-force-lock.js +197 -0
- package/scripts/important/authority-resolve-game.js +267 -0
- package/scripts/important/check-game-status.js +373 -0
- package/scripts/important/list-pending-games-by-version.js +270 -0
- package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
- package/scripts/initialize-jackpot.js +111 -0
- package/scripts/jackpot/.claude/settings.local.json +10 -0
- package/scripts/jackpot/force-reset.js +84 -0
- package/scripts/jackpot/initialize-mainnet.js +100 -0
- package/scripts/jackpot/keeper.js +742 -0
- package/scripts/jackpot/status.js +107 -0
- package/scripts/jackpot/update-round-duration.js +143 -0
- package/scripts/keeper-bot.js +112 -0
- package/scripts/list-pending-games.js +131 -0
- package/scripts/migrate-chat-v2.js +127 -0
- package/scripts/migrate-chat-winners.js +84 -0
- package/scripts/migrate-chat.sh +17 -0
- package/scripts/migrate-game-invite.js +83 -0
- package/scripts/migrate-heroku-game-notifications.sh +159 -0
- package/scripts/migrations/001_analytics_tables.sql +422 -0
- package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
- package/scripts/migrations/003_referral_earnings.sql +208 -0
- package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
- package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
- package/scripts/migrations/005_push_notifications.sql +55 -0
- package/scripts/migrations/006_add_draw_team_players.sql +28 -0
- package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
- package/scripts/migrations/007_add_gif_url.sql +8 -0
- package/scripts/migrations/008_add_connect4_columns.sql +139 -0
- package/scripts/migrations/008_add_pool_tracking.sql +22 -0
- package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
- package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
- package/scripts/migrations/011_create_developer_tables.sql +67 -0
- package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
- package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
- package/scripts/migrations/013_add_network_mode.sql +18 -0
- package/scripts/migrations/014_create_developer_app_users.sql +19 -0
- package/scripts/migrations/015_add_ui_config.sql +4 -0
- package/scripts/migrations/016_add_resolution_secret.sql +4 -0
- package/scripts/migrations/017_add_external_game_id.sql +3 -0
- package/scripts/migrations/018_create_pickem_tables.sql +115 -0
- package/scripts/migrations/019_expo_push_tokens.sql +19 -0
- package/scripts/migrations/create_whats_new_tables.sql +88 -0
- package/scripts/migrations/drop_live_games_tables.sql +34 -0
- package/scripts/open-jackpot-round.js +85 -0
- package/scripts/purge-all-data.sh +329 -0
- package/scripts/purge-all-data.sql +142 -0
- package/scripts/purge-heroku-data.sh +149 -0
- package/scripts/purge-heroku-data.sql +62 -0
- package/scripts/rebuild-heroku-database.sh +113 -0
- package/scripts/recover-funds.js +357 -0
- package/scripts/regenerate-epl-images.js +278 -0
- package/scripts/resize-s3-matchup-images.js +374 -0
- package/scripts/resolve-direct.js +88 -0
- package/scripts/resolve-mock-game.js +124 -0
- package/scripts/resolve-pickem-game.js +55 -0
- package/scripts/resolve-round-manual.js +83 -0
- package/scripts/resolve-stuck-game.js +382 -0
- package/scripts/resolve-stuck-round.js +42 -0
- package/scripts/run-connect4-migration.sh +16 -0
- package/scripts/run-mention-migration.sh +32 -0
- package/scripts/run-payment-migration.sh +51 -0
- package/scripts/run-preferred-currency-migration.sh +31 -0
- package/scripts/run-referral-earnings-migration.sh +32 -0
- package/scripts/run-survivor-outcome-migration.sh +16 -0
- package/scripts/seed-test-users.js +346 -0
- package/scripts/setup-auth-tables.js +78 -0
- package/scripts/setup-complete-database.sql +992 -0
- package/scripts/setup-database-fresh.sh +359 -0
- package/scripts/setup-heroku-keeper.sh +48 -0
- package/scripts/setup-keeper-database.js +83 -0
- package/scripts/setup-keeper-state-db.sql +110 -0
- package/scripts/setup-oracle.sh +39 -0
- package/scripts/setup-pnl-tracking.js +111 -0
- package/scripts/start-devnet.sh +14 -0
- package/scripts/test-arcade-devnet.sh +160 -0
- package/scripts/test-arcade-match.sh +109 -0
- package/scripts/test-automatic-mode.sh +239 -0
- package/scripts/test-connect4-cancel-claim.js +370 -0
- package/scripts/test-connect4-e2e.js +369 -0
- package/scripts/test-connect4-resolve.js +369 -0
- package/scripts/test-game-state-endpoint.js +136 -0
- package/scripts/test-invite-notification.js +86 -0
- package/scripts/test-jackpot-api.sh +71 -0
- package/scripts/test-poll-confirmation.js +267 -0
- package/scripts/test-resolve-game.js +271 -0
- package/scripts/test-resolve-signature.js +223 -0
- package/scripts/test-signature-preservation.js +124 -0
- package/scripts/test-state-machine.js +291 -0
- package/scripts/test-webhook-receiver.js +60 -0
- package/scripts/update-notification-constraint.js +52 -0
- package/scripts/verify-account-layout.js +145 -0
- package/scripts/verify-winner-algorithm.js +278 -0
- package/server.js +5259 -0
- package/services/arcadeMatchService.js +763 -0
- package/services/automaticGameOracle.js +1596 -0
- package/services/chatService.js +1612 -0
- package/services/connect4GameService.js +1049 -0
- package/services/connect4NotificationService.js +374 -0
- package/services/cryptoPriceService.js +223 -0
- package/services/customGameResolver.js +260 -0
- package/services/db.js +79 -0
- package/services/directMessageService.js +389 -0
- package/services/discordNotifications.js +160 -0
- package/services/exchangeRateService.js +289 -0
- package/services/expoPushService.js +314 -0
- package/services/gamesCacheService.js +539 -0
- package/services/jackpotHistory.js +331 -0
- package/services/jackpotService.js +856 -0
- package/services/keeperStateService.js +355 -0
- package/services/matchupImageService.js +591 -0
- package/services/notificationCacheService.js +407 -0
- package/services/pickemOracle.js +440 -0
- package/services/playerStatsService.js +389 -0
- package/services/portfolioService.js +555 -0
- package/services/promoService.js +757 -0
- package/services/promoTreasuryService.js +239 -0
- package/services/pushNotifications.js +353 -0
- package/services/redisService.js +422 -0
- package/services/referralEarningsService.js +728 -0
- package/services/s3Service.js +396 -0
- package/services/socialService.js +1202 -0
- package/services/survivorOracle.js +469 -0
- package/services/survivorSimulator.js +475 -0
- package/services/telegramNotifications.js +461 -0
- package/services/userProfileStatsService.js +1185 -0
- package/services/whatsNewService.js +388 -0
- package/utils/urlHelper.js +95 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
const { pool } = require('../services/db'); // Shared database pool
|
|
5
|
+
|
|
6
|
+
const PANDASCORE_BASE_URL = 'https://api.pandascore.co';
|
|
7
|
+
const PANDASCORE_API_KEY = process.env.PANDASCORE_API_KEY;
|
|
8
|
+
|
|
9
|
+
// Socket.IO will be injected by server.js
|
|
10
|
+
let chatNamespace = null;
|
|
11
|
+
|
|
12
|
+
// Inject Socket.IO instance
|
|
13
|
+
router.setSocketIO = (ioInstance, chatNS) => {
|
|
14
|
+
chatNamespace = chatNS;
|
|
15
|
+
console.log('🔌 Socket.IO injected into esports routes');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Helper: build PandaScore request headers
|
|
20
|
+
*/
|
|
21
|
+
function pandaHeaders() {
|
|
22
|
+
return {
|
|
23
|
+
accept: 'application/json',
|
|
24
|
+
authorization: `Bearer ${PANDASCORE_API_KEY}`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================
|
|
29
|
+
// LEAGUES
|
|
30
|
+
// ============================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @route GET /api/esports/leagues
|
|
34
|
+
* @desc List all esports leagues (paginated)
|
|
35
|
+
* @access Public
|
|
36
|
+
* @query page, per_page, sort, search[name], filter[videogame_id]
|
|
37
|
+
*/
|
|
38
|
+
router.get('/leagues', async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const params = buildParams(req.query);
|
|
41
|
+
|
|
42
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues`, {
|
|
43
|
+
headers: pandaHeaders(),
|
|
44
|
+
params,
|
|
45
|
+
timeout: 10000
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
res.json({ success: true, data: response.data });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Error fetching esports leagues:', error.message);
|
|
51
|
+
const status = error.response?.status || 500;
|
|
52
|
+
res.status(status).json({ success: false, error: 'Failed to fetch esports leagues' });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @route GET /api/esports/videogames/:videogameId/leagues
|
|
58
|
+
* @desc List leagues for a specific videogame
|
|
59
|
+
* @access Public
|
|
60
|
+
* @query page, per_page, sort, search[name]
|
|
61
|
+
*/
|
|
62
|
+
router.get('/videogames/:videogameId/leagues', async (req, res) => {
|
|
63
|
+
try {
|
|
64
|
+
const { videogameId } = req.params;
|
|
65
|
+
const params = buildParams(req.query);
|
|
66
|
+
|
|
67
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/videogames/${videogameId}/leagues`, {
|
|
68
|
+
headers: pandaHeaders(),
|
|
69
|
+
params,
|
|
70
|
+
timeout: 10000
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
res.json({ success: true, data: response.data });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`Error fetching leagues for videogame ${req.params.videogameId}:`, error.message);
|
|
76
|
+
const status = error.response?.status || 500;
|
|
77
|
+
res.status(status).json({ success: false, error: 'Failed to fetch videogame leagues' });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @route GET /api/esports/leagues/:leagueId
|
|
83
|
+
* @desc Get a single esports league by ID
|
|
84
|
+
* @access Public
|
|
85
|
+
*/
|
|
86
|
+
router.get('/leagues/:leagueId', async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const { leagueId } = req.params;
|
|
89
|
+
|
|
90
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueId}`, {
|
|
91
|
+
headers: pandaHeaders(),
|
|
92
|
+
timeout: 10000
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
res.json({ success: true, data: response.data });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`Error fetching esports league ${req.params.leagueId}:`, error.message);
|
|
98
|
+
const status = error.response?.status || 500;
|
|
99
|
+
res.status(status).json({ success: false, error: 'Failed to fetch esports league' });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ============================================
|
|
104
|
+
// LEAGUE SUB-RESOURCES (Series, Tournaments, Matches)
|
|
105
|
+
// ============================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Helper: build common query params from request.
|
|
109
|
+
* Express parses bracket notation (e.g. filter[status]=running) into nested
|
|
110
|
+
* objects ({ filter: { status: 'running' } }). We need to reconstruct the
|
|
111
|
+
* bracket keys for PandaScore's API.
|
|
112
|
+
*/
|
|
113
|
+
function buildParams(query) {
|
|
114
|
+
const params = {};
|
|
115
|
+
const { page, per_page, sort } = query;
|
|
116
|
+
if (page) params.page = page;
|
|
117
|
+
if (per_page) params.per_page = per_page;
|
|
118
|
+
if (sort) params.sort = sort;
|
|
119
|
+
|
|
120
|
+
// Handle Express-parsed nested objects (filter[x] → { filter: { x: val } })
|
|
121
|
+
for (const prefix of ['filter', 'search', 'range']) {
|
|
122
|
+
if (query[prefix] && typeof query[prefix] === 'object') {
|
|
123
|
+
for (const [key, value] of Object.entries(query[prefix])) {
|
|
124
|
+
params[`${prefix}[${key}]`] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Also handle literal bracket keys (e.g. from curl or non-Express clients)
|
|
130
|
+
for (const key of Object.keys(query)) {
|
|
131
|
+
if (key.startsWith('filter[') || key.startsWith('search[') || key.startsWith('range[')) {
|
|
132
|
+
params[key] = query[key];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return params;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @route GET /api/esports/leagues/:leagueIdOrSlug/series
|
|
141
|
+
* @desc List series for a league
|
|
142
|
+
* @access Public
|
|
143
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
144
|
+
*/
|
|
145
|
+
router.get('/leagues/:leagueIdOrSlug/series', async (req, res) => {
|
|
146
|
+
try {
|
|
147
|
+
const { leagueIdOrSlug } = req.params;
|
|
148
|
+
const params = buildParams(req.query);
|
|
149
|
+
|
|
150
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/series`, {
|
|
151
|
+
headers: pandaHeaders(),
|
|
152
|
+
params,
|
|
153
|
+
timeout: 10000
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
res.json({ success: true, data: response.data });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(`Error fetching series for league ${req.params.leagueIdOrSlug}:`, error.message);
|
|
159
|
+
const status = error.response?.status || 500;
|
|
160
|
+
res.status(status).json({ success: false, error: 'Failed to fetch league series' });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @route GET /api/esports/leagues/:leagueIdOrSlug/tournaments
|
|
166
|
+
* @desc List tournaments for a league
|
|
167
|
+
* @access Public
|
|
168
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
169
|
+
*/
|
|
170
|
+
router.get('/leagues/:leagueIdOrSlug/tournaments', async (req, res) => {
|
|
171
|
+
try {
|
|
172
|
+
const { leagueIdOrSlug } = req.params;
|
|
173
|
+
const params = buildParams(req.query);
|
|
174
|
+
|
|
175
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/tournaments`, {
|
|
176
|
+
headers: pandaHeaders(),
|
|
177
|
+
params,
|
|
178
|
+
timeout: 10000
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
res.json({ success: true, data: response.data });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error(`Error fetching tournaments for league ${req.params.leagueIdOrSlug}:`, error.message);
|
|
184
|
+
const status = error.response?.status || 500;
|
|
185
|
+
res.status(status).json({ success: false, error: 'Failed to fetch league tournaments' });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @route GET /api/esports/leagues/:leagueIdOrSlug/matches/upcoming
|
|
191
|
+
* @desc List upcoming matches for a league
|
|
192
|
+
* @access Public
|
|
193
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
194
|
+
*/
|
|
195
|
+
router.get('/leagues/:leagueIdOrSlug/matches/upcoming', async (req, res) => {
|
|
196
|
+
try {
|
|
197
|
+
const { leagueIdOrSlug } = req.params;
|
|
198
|
+
const params = buildParams(req.query);
|
|
199
|
+
|
|
200
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/matches/upcoming`, {
|
|
201
|
+
headers: pandaHeaders(),
|
|
202
|
+
params,
|
|
203
|
+
timeout: 10000
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
res.json({ success: true, data: response.data });
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error(`Error fetching upcoming matches for league ${req.params.leagueIdOrSlug}:`, error.message);
|
|
209
|
+
const status = error.response?.status || 500;
|
|
210
|
+
res.status(status).json({ success: false, error: 'Failed to fetch upcoming matches' });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @route GET /api/esports/leagues/:leagueIdOrSlug/matches/running
|
|
216
|
+
* @desc List currently running (live) matches for a league
|
|
217
|
+
* @access Public
|
|
218
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
219
|
+
*/
|
|
220
|
+
router.get('/leagues/:leagueIdOrSlug/matches/running', async (req, res) => {
|
|
221
|
+
try {
|
|
222
|
+
const { leagueIdOrSlug } = req.params;
|
|
223
|
+
const params = buildParams(req.query);
|
|
224
|
+
|
|
225
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/matches/running`, {
|
|
226
|
+
headers: pandaHeaders(),
|
|
227
|
+
params,
|
|
228
|
+
timeout: 10000
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
res.json({ success: true, data: response.data });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(`Error fetching running matches for league ${req.params.leagueIdOrSlug}:`, error.message);
|
|
234
|
+
const status = error.response?.status || 500;
|
|
235
|
+
res.status(status).json({ success: false, error: 'Failed to fetch running matches' });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @route GET /api/esports/leagues/:leagueIdOrSlug/matches
|
|
241
|
+
* @desc List ALL matches for a league (with rich filtering)
|
|
242
|
+
* @access Public
|
|
243
|
+
* @query page, per_page, sort, filter[status], filter[opponent_id], filter[future],
|
|
244
|
+
* filter[finished], filter[running], filter[not_started], filter[past],
|
|
245
|
+
* filter[tournament_id], filter[serie_id], filter[match_type],
|
|
246
|
+
* filter[opponents_filled], range[begin_at], search[name]
|
|
247
|
+
*/
|
|
248
|
+
router.get('/leagues/:leagueIdOrSlug/matches', async (req, res) => {
|
|
249
|
+
try {
|
|
250
|
+
const { leagueIdOrSlug } = req.params;
|
|
251
|
+
const params = buildParams(req.query);
|
|
252
|
+
|
|
253
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/matches`, {
|
|
254
|
+
headers: pandaHeaders(),
|
|
255
|
+
params,
|
|
256
|
+
timeout: 10000
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
res.json({ success: true, data: response.data });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error(`Error fetching matches for league ${req.params.leagueIdOrSlug}:`, error.message);
|
|
262
|
+
const status = error.response?.status || 500;
|
|
263
|
+
res.status(status).json({ success: false, error: 'Failed to fetch league matches' });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ============================================
|
|
268
|
+
// LIVES (real-time match data with WebSocket URLs)
|
|
269
|
+
// ============================================
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @route GET /api/esports/lives
|
|
273
|
+
* @desc Get all currently live matches with WebSocket endpoint URLs
|
|
274
|
+
* @access Public
|
|
275
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
276
|
+
*/
|
|
277
|
+
router.get('/lives', async (req, res) => {
|
|
278
|
+
try {
|
|
279
|
+
const params = buildParams(req.query);
|
|
280
|
+
|
|
281
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/lives`, {
|
|
282
|
+
headers: pandaHeaders(),
|
|
283
|
+
params,
|
|
284
|
+
timeout: 10000
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
res.json({ success: true, data: response.data });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error('Error fetching live matches:', error.message);
|
|
290
|
+
const status = error.response?.status || 500;
|
|
291
|
+
res.status(status).json({ success: false, error: 'Failed to fetch live matches' });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ============================================
|
|
296
|
+
// VIDEOGAME-SPECIFIC ENDPOINTS
|
|
297
|
+
// ============================================
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Map PandaScore videogame slugs to their API path segments.
|
|
301
|
+
* The slug in data responses (e.g. "cs-go") often differs from
|
|
302
|
+
* the API URL path (e.g. "csgo").
|
|
303
|
+
*/
|
|
304
|
+
function apiSlug(videogameSlug) {
|
|
305
|
+
const map = {
|
|
306
|
+
'cs-go': 'csgo',
|
|
307
|
+
'dota-2': 'dota2',
|
|
308
|
+
'cod-mw': 'codmw',
|
|
309
|
+
'league-of-legends': 'lol',
|
|
310
|
+
'r6-siege': 'r6siege',
|
|
311
|
+
};
|
|
312
|
+
return map[videogameSlug] || videogameSlug;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @route GET /api/esports/:videogameSlug/tournaments
|
|
317
|
+
* @desc List tournaments for a specific videogame (e.g. cod-mw, valorant, cs-go)
|
|
318
|
+
* @access Public
|
|
319
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
320
|
+
*/
|
|
321
|
+
router.get('/:videogameSlug/tournaments', async (req, res) => {
|
|
322
|
+
try {
|
|
323
|
+
const { videogameSlug } = req.params;
|
|
324
|
+
const params = buildParams(req.query);
|
|
325
|
+
|
|
326
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/tournaments`, {
|
|
327
|
+
headers: pandaHeaders(),
|
|
328
|
+
params,
|
|
329
|
+
timeout: 10000
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
res.json({ success: true, data: response.data });
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error(`Error fetching tournaments for ${req.params.videogameSlug}:`, error.message);
|
|
335
|
+
const status = error.response?.status || 500;
|
|
336
|
+
res.status(status).json({ success: false, error: `Failed to fetch ${req.params.videogameSlug} tournaments` });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @route GET /api/esports/:videogameSlug/tournaments/running
|
|
342
|
+
* @desc List currently running tournaments for a videogame
|
|
343
|
+
* @access Public
|
|
344
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
345
|
+
*/
|
|
346
|
+
router.get('/:videogameSlug/tournaments/running', async (req, res) => {
|
|
347
|
+
try {
|
|
348
|
+
const { videogameSlug } = req.params;
|
|
349
|
+
const params = buildParams(req.query);
|
|
350
|
+
|
|
351
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/tournaments/running`, {
|
|
352
|
+
headers: pandaHeaders(),
|
|
353
|
+
params,
|
|
354
|
+
timeout: 10000
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
res.json({ success: true, data: response.data });
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error(`Error fetching running tournaments for ${req.params.videogameSlug}:`, error.message);
|
|
360
|
+
const status = error.response?.status || 500;
|
|
361
|
+
res.status(status).json({ success: false, error: `Failed to fetch running ${req.params.videogameSlug} tournaments` });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @route GET /api/esports/:videogameSlug/tournaments/upcoming
|
|
367
|
+
* @desc List upcoming tournaments for a videogame
|
|
368
|
+
* @access Public
|
|
369
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
370
|
+
*/
|
|
371
|
+
router.get('/:videogameSlug/tournaments/upcoming', async (req, res) => {
|
|
372
|
+
try {
|
|
373
|
+
const { videogameSlug } = req.params;
|
|
374
|
+
const params = buildParams(req.query);
|
|
375
|
+
|
|
376
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/tournaments/upcoming`, {
|
|
377
|
+
headers: pandaHeaders(),
|
|
378
|
+
params,
|
|
379
|
+
timeout: 10000
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
res.json({ success: true, data: response.data });
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error(`Error fetching upcoming tournaments for ${req.params.videogameSlug}:`, error.message);
|
|
385
|
+
const status = error.response?.status || 500;
|
|
386
|
+
res.status(status).json({ success: false, error: `Failed to fetch upcoming ${req.params.videogameSlug} tournaments` });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @route GET /api/esports/tournaments/:tournamentId/matches
|
|
392
|
+
* @desc Get full matches (with opponents, results, games) for a tournament
|
|
393
|
+
* @access Public
|
|
394
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
395
|
+
*/
|
|
396
|
+
router.get('/tournaments/:tournamentId/matches', async (req, res) => {
|
|
397
|
+
try {
|
|
398
|
+
const { tournamentId } = req.params;
|
|
399
|
+
const params = buildParams(req.query);
|
|
400
|
+
|
|
401
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/tournaments/${tournamentId}/matches`, {
|
|
402
|
+
headers: pandaHeaders(),
|
|
403
|
+
params,
|
|
404
|
+
timeout: 10000
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
res.json({ success: true, data: response.data });
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error(`Error fetching matches for tournament ${req.params.tournamentId}:`, error.message);
|
|
410
|
+
const status = error.response?.status || 500;
|
|
411
|
+
res.status(status).json({ success: false, error: `Failed to fetch tournament matches` });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @route GET /api/esports/:videogameSlug/matches
|
|
417
|
+
* @desc List matches for a specific videogame
|
|
418
|
+
* @access Public
|
|
419
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
420
|
+
*/
|
|
421
|
+
router.get('/:videogameSlug/matches', async (req, res) => {
|
|
422
|
+
try {
|
|
423
|
+
const { videogameSlug } = req.params;
|
|
424
|
+
const params = buildParams(req.query);
|
|
425
|
+
|
|
426
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/matches`, {
|
|
427
|
+
headers: pandaHeaders(),
|
|
428
|
+
params,
|
|
429
|
+
timeout: 10000
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
res.json({ success: true, data: response.data });
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error(`Error fetching matches for ${req.params.videogameSlug}:`, error.message);
|
|
435
|
+
const status = error.response?.status || 500;
|
|
436
|
+
res.status(status).json({ success: false, error: `Failed to fetch ${req.params.videogameSlug} matches` });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @route GET /api/esports/:videogameSlug/matches/running
|
|
442
|
+
* @desc List currently running matches for a videogame
|
|
443
|
+
* @access Public
|
|
444
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
445
|
+
*/
|
|
446
|
+
router.get('/:videogameSlug/matches/running', async (req, res) => {
|
|
447
|
+
try {
|
|
448
|
+
const { videogameSlug } = req.params;
|
|
449
|
+
const params = buildParams(req.query);
|
|
450
|
+
|
|
451
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/matches/running`, {
|
|
452
|
+
headers: pandaHeaders(),
|
|
453
|
+
params,
|
|
454
|
+
timeout: 10000
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
res.json({ success: true, data: response.data });
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error(`Error fetching running matches for ${req.params.videogameSlug}:`, error.message);
|
|
460
|
+
const status = error.response?.status || 500;
|
|
461
|
+
res.status(status).json({ success: false, error: `Failed to fetch running ${req.params.videogameSlug} matches` });
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @route GET /api/esports/:videogameSlug/matches/upcoming
|
|
467
|
+
* @desc List upcoming matches for a videogame
|
|
468
|
+
* @access Public
|
|
469
|
+
* @query page, per_page, sort, filter[], search[], range[]
|
|
470
|
+
*/
|
|
471
|
+
router.get('/:videogameSlug/matches/upcoming', async (req, res) => {
|
|
472
|
+
try {
|
|
473
|
+
const { videogameSlug } = req.params;
|
|
474
|
+
const params = buildParams(req.query);
|
|
475
|
+
|
|
476
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/matches/upcoming`, {
|
|
477
|
+
headers: pandaHeaders(),
|
|
478
|
+
params,
|
|
479
|
+
timeout: 10000
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
res.json({ success: true, data: response.data });
|
|
483
|
+
} catch (error) {
|
|
484
|
+
console.error(`Error fetching upcoming matches for ${req.params.videogameSlug}:`, error.message);
|
|
485
|
+
const status = error.response?.status || 500;
|
|
486
|
+
res.status(status).json({ success: false, error: `Failed to fetch upcoming ${req.params.videogameSlug} matches` });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// ============================================
|
|
491
|
+
// SINGLE MATCH BY ID
|
|
492
|
+
// ============================================
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* @route GET /api/esports/matches/:matchId
|
|
496
|
+
* @desc Get a single match by PandaScore match ID (game-agnostic)
|
|
497
|
+
* @access Public
|
|
498
|
+
*/
|
|
499
|
+
router.get('/matches/:matchId', async (req, res) => {
|
|
500
|
+
try {
|
|
501
|
+
const { matchId } = req.params;
|
|
502
|
+
|
|
503
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/matches/${matchId}`, {
|
|
504
|
+
headers: pandaHeaders(),
|
|
505
|
+
timeout: 10000
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
res.json({ success: true, data: response.data });
|
|
509
|
+
} catch (error) {
|
|
510
|
+
console.error(`Error fetching match ${req.params.matchId}:`, error.message);
|
|
511
|
+
const status = error.response?.status || 500;
|
|
512
|
+
res.status(status).json({ success: false, error: 'Failed to fetch match' });
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* @route GET /api/esports/:videogameSlug/matches/:matchId
|
|
518
|
+
* @desc Get a single match by PandaScore match ID for a specific videogame
|
|
519
|
+
* @access Public
|
|
520
|
+
*/
|
|
521
|
+
router.get('/:videogameSlug/matches/:matchId', async (req, res) => {
|
|
522
|
+
try {
|
|
523
|
+
const { videogameSlug, matchId } = req.params;
|
|
524
|
+
|
|
525
|
+
const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/matches/${matchId}`, {
|
|
526
|
+
headers: pandaHeaders(),
|
|
527
|
+
timeout: 10000
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
res.json({ success: true, data: response.data });
|
|
531
|
+
} catch (error) {
|
|
532
|
+
console.error(`Error fetching match ${req.params.matchId} for ${req.params.videogameSlug}:`, error.message);
|
|
533
|
+
const status = error.response?.status || 500;
|
|
534
|
+
res.status(status).json({ success: false, error: `Failed to fetch ${req.params.videogameSlug} match` });
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// ============================================
|
|
539
|
+
// ESPORTS GAME CREATION
|
|
540
|
+
// ============================================
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* @route POST /api/esports/games/create
|
|
544
|
+
* @desc Validate a PandaScore match and return enriched data for game creation.
|
|
545
|
+
* The frontend uses this data to build the on-chain transaction, then
|
|
546
|
+
* calls the existing /api/auth/games/save endpoint to persist it.
|
|
547
|
+
* @access Public
|
|
548
|
+
* @body { pandascoreMatchId: number, videogameSlug: string }
|
|
549
|
+
*/
|
|
550
|
+
router.post('/games/validate', async (req, res) => {
|
|
551
|
+
try {
|
|
552
|
+
const { pandascoreMatchId, videogameSlug } = req.body;
|
|
553
|
+
|
|
554
|
+
if (!pandascoreMatchId) {
|
|
555
|
+
return res.status(400).json({
|
|
556
|
+
success: false,
|
|
557
|
+
error: 'pandascoreMatchId is required'
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Fetch full match data from PandaScore
|
|
562
|
+
const matchResponse = await axios.get(`${PANDASCORE_BASE_URL}/matches/${pandascoreMatchId}`, {
|
|
563
|
+
headers: pandaHeaders(),
|
|
564
|
+
timeout: 10000
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const match = matchResponse.data;
|
|
568
|
+
if (!match) {
|
|
569
|
+
return res.status(404).json({ success: false, error: 'Match not found on PandaScore' });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Validate: must be not_started or running (live betting allowed)
|
|
573
|
+
const isLive = match.status === 'running';
|
|
574
|
+
if (match.status !== 'not_started' && !isLive) {
|
|
575
|
+
return res.status(400).json({
|
|
576
|
+
success: false,
|
|
577
|
+
error: `Match status is "${match.status}" — can only bet on matches that haven't started or are currently live`,
|
|
578
|
+
matchStatus: match.status
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Validate: must have 2 opponents
|
|
583
|
+
if (!match.opponents || match.opponents.length < 2) {
|
|
584
|
+
return res.status(400).json({
|
|
585
|
+
success: false,
|
|
586
|
+
error: 'Match does not have 2 confirmed opponents yet'
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// For upcoming matches: scheduled_at must be > now + 2 minutes (Solana constraint)
|
|
591
|
+
// For live matches: skip this check — lock is set to now + 5 minutes instead
|
|
592
|
+
const scheduledAt = new Date(match.scheduled_at || match.begin_at);
|
|
593
|
+
if (!isLive) {
|
|
594
|
+
const minTime = new Date(Date.now() + 2 * 60 * 1000);
|
|
595
|
+
if (scheduledAt <= minTime) {
|
|
596
|
+
return res.status(400).json({
|
|
597
|
+
success: false,
|
|
598
|
+
error: 'Match starts too soon — must be at least 2 minutes from now'
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Build the sportsEvent JSONB that will be stored alongside the game
|
|
604
|
+
const opp0 = match.opponents[0];
|
|
605
|
+
const opp1 = match.opponents[1];
|
|
606
|
+
const sportsEvent = {
|
|
607
|
+
pandascoreMatchId: match.id,
|
|
608
|
+
videogameSlug: match.videogame?.slug || videogameSlug || 'unknown',
|
|
609
|
+
videogame: match.videogame?.name || videogameSlug || 'Unknown',
|
|
610
|
+
matchName: match.name || `${opp0.opponent?.name} vs ${opp1.opponent?.name}`,
|
|
611
|
+
tournament: match.tournament?.name || '',
|
|
612
|
+
tournamentId: match.tournament?.id || null,
|
|
613
|
+
serie: match.serie?.full_name || match.serie?.name || '',
|
|
614
|
+
tier: match.tournament?.tier || match.serie?.tier || null,
|
|
615
|
+
matchType: match.match_type || 'best_of',
|
|
616
|
+
numberOfGames: match.number_of_games || 1,
|
|
617
|
+
scheduledAt: match.scheduled_at || match.begin_at,
|
|
618
|
+
// strTimestamp is used by the oracle for lock timing — reuse same convention
|
|
619
|
+
// For live matches, use the lock time (now + 5 min) so countdown shows correctly
|
|
620
|
+
strTimestamp: isLive
|
|
621
|
+
? new Date(Date.now() + 5 * 60 * 1000).toISOString().replace('Z', '').split('.')[0]
|
|
622
|
+
: (match.scheduled_at || match.begin_at || '').replace('Z', '').split('.')[0],
|
|
623
|
+
opponents: match.opponents,
|
|
624
|
+
streamsList: match.streams_list || [],
|
|
625
|
+
leagueName: match.league?.name || '',
|
|
626
|
+
leagueId: match.league?.id || null,
|
|
627
|
+
// TheSportsDB-compatible fields so existing notification code works
|
|
628
|
+
strEvent: match.name || `${opp0.opponent?.name} vs ${opp1.opponent?.name}`,
|
|
629
|
+
strHomeTeam: opp0.opponent?.name,
|
|
630
|
+
strAwayTeam: opp1.opponent?.name,
|
|
631
|
+
strHomeTeamBadge: opp0.opponent?.image_url,
|
|
632
|
+
strAwayTeamBadge: opp1.opponent?.image_url,
|
|
633
|
+
strLeague: match.videogame?.name || videogameSlug,
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Calculate lock_timestamp (Unix seconds)
|
|
637
|
+
// For live matches: lock 5 minutes from now (gives time for opponent to join)
|
|
638
|
+
// For upcoming matches: lock at scheduled start time
|
|
639
|
+
const lockTimestamp = isLive
|
|
640
|
+
? Math.floor((Date.now() + 5 * 60 * 1000) / 1000)
|
|
641
|
+
: Math.floor(scheduledAt.getTime() / 1000);
|
|
642
|
+
|
|
643
|
+
res.json({
|
|
644
|
+
success: true,
|
|
645
|
+
match: {
|
|
646
|
+
id: match.id,
|
|
647
|
+
name: sportsEvent.matchName,
|
|
648
|
+
status: match.status,
|
|
649
|
+
scheduledAt: sportsEvent.scheduledAt,
|
|
650
|
+
lockTimestamp,
|
|
651
|
+
tournament: sportsEvent.tournament,
|
|
652
|
+
tier: sportsEvent.tier,
|
|
653
|
+
matchType: sportsEvent.matchType,
|
|
654
|
+
numberOfGames: sportsEvent.numberOfGames,
|
|
655
|
+
opponents: match.opponents.map(o => ({
|
|
656
|
+
id: o.opponent?.id,
|
|
657
|
+
name: o.opponent?.name,
|
|
658
|
+
acronym: o.opponent?.acronym,
|
|
659
|
+
imageUrl: o.opponent?.image_url,
|
|
660
|
+
})),
|
|
661
|
+
streams: sportsEvent.streamsList,
|
|
662
|
+
},
|
|
663
|
+
// Pre-built sportsEvent for the frontend to pass to /api/auth/games/save
|
|
664
|
+
sportsEvent,
|
|
665
|
+
// gameMode 5 = esports
|
|
666
|
+
gameMode: 5,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
} catch (error) {
|
|
670
|
+
console.error('Error validating esports match:', error.message);
|
|
671
|
+
const status = error.response?.status || 500;
|
|
672
|
+
res.status(status).json({ success: false, error: 'Failed to validate esports match' });
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* @route GET /api/esports/games/upcoming
|
|
678
|
+
* @desc Get bettable upcoming esports matches (CS + Valorant, S/A tier, not_started)
|
|
679
|
+
* @access Public
|
|
680
|
+
* @query videogame (cs-go|valorant), page, per_page
|
|
681
|
+
*/
|
|
682
|
+
router.get('/games/upcoming', async (req, res) => {
|
|
683
|
+
try {
|
|
684
|
+
const videogames = req.query.videogame
|
|
685
|
+
? [req.query.videogame]
|
|
686
|
+
: ['cs-go', 'valorant'];
|
|
687
|
+
|
|
688
|
+
const page = req.query.page || 1;
|
|
689
|
+
const perPage = req.query.per_page || 20;
|
|
690
|
+
|
|
691
|
+
const allMatches = [];
|
|
692
|
+
|
|
693
|
+
for (const vg of videogames) {
|
|
694
|
+
try {
|
|
695
|
+
const response = await axios.get(
|
|
696
|
+
`${PANDASCORE_BASE_URL}/${apiSlug(vg)}/matches/upcoming`,
|
|
697
|
+
{
|
|
698
|
+
headers: pandaHeaders(),
|
|
699
|
+
params: {
|
|
700
|
+
page,
|
|
701
|
+
per_page: perPage,
|
|
702
|
+
sort: 'scheduled_at',
|
|
703
|
+
'filter[opponents_filled]': true,
|
|
704
|
+
},
|
|
705
|
+
timeout: 10000
|
|
706
|
+
}
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
const matches = (response.data || [])
|
|
710
|
+
.filter(m => {
|
|
711
|
+
// Allow S, A, B, C, D tier tournaments
|
|
712
|
+
const tier = m.tournament?.tier || m.serie?.tier;
|
|
713
|
+
return ['s', 'a', 'b', 'c', 'd'].includes(tier);
|
|
714
|
+
})
|
|
715
|
+
.filter(m => {
|
|
716
|
+
// Must start > 2 min from now
|
|
717
|
+
const scheduled = new Date(m.scheduled_at || m.begin_at);
|
|
718
|
+
return scheduled.getTime() > Date.now() + 2 * 60 * 1000;
|
|
719
|
+
})
|
|
720
|
+
.map(m => ({
|
|
721
|
+
id: m.id,
|
|
722
|
+
name: m.name,
|
|
723
|
+
videogame: vg,
|
|
724
|
+
videogameName: m.videogame?.name || vg,
|
|
725
|
+
scheduledAt: m.scheduled_at || m.begin_at,
|
|
726
|
+
matchType: m.match_type,
|
|
727
|
+
numberOfGames: m.number_of_games,
|
|
728
|
+
tournament: m.tournament?.name,
|
|
729
|
+
tier: m.tournament?.tier || m.serie?.tier,
|
|
730
|
+
league: m.league?.name,
|
|
731
|
+
opponents: (m.opponents || []).map(o => ({
|
|
732
|
+
id: o.opponent?.id,
|
|
733
|
+
name: o.opponent?.name,
|
|
734
|
+
acronym: o.opponent?.acronym,
|
|
735
|
+
imageUrl: o.opponent?.image_url,
|
|
736
|
+
})),
|
|
737
|
+
streams: m.streams_list || [],
|
|
738
|
+
}));
|
|
739
|
+
|
|
740
|
+
allMatches.push(...matches);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
console.error(`Error fetching upcoming ${vg} matches:`, err.message);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Sort by scheduled time
|
|
747
|
+
allMatches.sort((a, b) => new Date(a.scheduledAt) - new Date(b.scheduledAt));
|
|
748
|
+
|
|
749
|
+
res.json({ success: true, data: allMatches });
|
|
750
|
+
} catch (error) {
|
|
751
|
+
console.error('Error fetching upcoming esports games:', error.message);
|
|
752
|
+
res.status(500).json({ success: false, error: 'Failed to fetch upcoming esports games' });
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* @route GET /api/esports/games/pending
|
|
758
|
+
* @desc Get esports games that are in our DB (game_mode=5, not resolved)
|
|
759
|
+
* @access Public
|
|
760
|
+
*/
|
|
761
|
+
router.get('/games/pending', async (req, res) => {
|
|
762
|
+
try {
|
|
763
|
+
const result = await pool.query(`
|
|
764
|
+
SELECT * FROM games
|
|
765
|
+
WHERE game_mode = 5
|
|
766
|
+
AND is_resolved = false
|
|
767
|
+
ORDER BY created_at DESC
|
|
768
|
+
`);
|
|
769
|
+
|
|
770
|
+
const games = result.rows.map(row => {
|
|
771
|
+
const se = row.sports_event || {};
|
|
772
|
+
const opp0 = se.opponents?.[0]?.opponent || {};
|
|
773
|
+
const opp1 = se.opponents?.[1]?.opponent || {};
|
|
774
|
+
return {
|
|
775
|
+
gameId: row.game_id,
|
|
776
|
+
gameAddress: row.game_address,
|
|
777
|
+
title: row.title || se.matchName || `${opp0.name || '?'} vs ${opp1.name || '?'}`,
|
|
778
|
+
gameMode: row.game_mode,
|
|
779
|
+
buyIn: parseFloat(row.buy_in),
|
|
780
|
+
createdBy: row.created_by,
|
|
781
|
+
sportsEvent: se,
|
|
782
|
+
homeTeam: opp0.name,
|
|
783
|
+
awayTeam: opp1.name,
|
|
784
|
+
homeTeamBadge: opp0.image_url,
|
|
785
|
+
awayTeamBadge: opp1.image_url,
|
|
786
|
+
videogame: se.videogame || se.videogameSlug,
|
|
787
|
+
tournament: se.tournament,
|
|
788
|
+
tier: se.tier,
|
|
789
|
+
matchType: se.matchType,
|
|
790
|
+
scheduledAt: se.scheduledAt,
|
|
791
|
+
homeTeamPlayers: row.home_team_players || [],
|
|
792
|
+
awayTeamPlayers: row.away_team_players || [],
|
|
793
|
+
isLocked: row.is_locked,
|
|
794
|
+
isResolved: row.is_resolved,
|
|
795
|
+
createdAt: row.created_at,
|
|
796
|
+
};
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
res.json({ success: true, games });
|
|
800
|
+
} catch (error) {
|
|
801
|
+
console.error('Error fetching pending esports games:', error.message);
|
|
802
|
+
res.status(500).json({ success: false, error: error.message });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
module.exports = router;
|