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,289 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { pool } = require('./db'); // Shared database pool
|
|
3
|
+
|
|
4
|
+
class ExchangeRateService {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.apiKey = process.env.EXCHANGE_API_KEY || '757419e9be20039acaf308a9';
|
|
7
|
+
this.apiBaseUrl = process.env.EXCHANGE_API_BASE_URL || 'https://v6.exchangerate-api.com/v6';
|
|
8
|
+
this.cacheTTL = parseInt(process.env.EXCHANGE_CACHE_TTL) || 300; // 5 minutes in seconds
|
|
9
|
+
this.baseCurrency = process.env.BASE_CURRENCY || 'USD';
|
|
10
|
+
this.supportedCurrencies = (process.env.SUPPORTED_CURRENCIES || 'USD,EUR,CAD,GBP,JPY,AUD,CHF,CNY,SEK,NZD').split(',');
|
|
11
|
+
|
|
12
|
+
// Initialize cache table on startup
|
|
13
|
+
this.initializeCacheTable();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize the exchange_rates_cache table if it doesn't exist
|
|
18
|
+
*/
|
|
19
|
+
async initializeCacheTable() {
|
|
20
|
+
try {
|
|
21
|
+
await pool.query(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS exchange_rates_cache (
|
|
23
|
+
id SERIAL PRIMARY KEY,
|
|
24
|
+
base_currency VARCHAR(3) NOT NULL UNIQUE,
|
|
25
|
+
rates JSONB NOT NULL,
|
|
26
|
+
last_updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
27
|
+
expires_at TIMESTAMP NOT NULL,
|
|
28
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_exchange_rates_base ON exchange_rates_cache(base_currency);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_exchange_rates_expires ON exchange_rates_cache(expires_at);
|
|
33
|
+
`);
|
|
34
|
+
console.log('[Exchange Rates] Cache table initialized');
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[Exchange Rates] Error initializing cache table:', error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get exchange rates for all supported currencies
|
|
42
|
+
* @param {string} base - Base currency (default: USD)
|
|
43
|
+
* @returns {Object} Exchange rates object
|
|
44
|
+
*/
|
|
45
|
+
async getExchangeRates(base = this.baseCurrency) {
|
|
46
|
+
try {
|
|
47
|
+
// Try to get from cache first
|
|
48
|
+
const cachedRates = await this.getCachedRates(base);
|
|
49
|
+
if (cachedRates) {
|
|
50
|
+
console.log(`[Exchange Rates] Retrieved ${base} from cache`);
|
|
51
|
+
return {
|
|
52
|
+
...cachedRates,
|
|
53
|
+
source: 'cache'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If not in cache, fetch from API
|
|
58
|
+
console.log(`[Exchange Rates] Fetching fresh rates for ${base} from API`);
|
|
59
|
+
const freshRates = await this.fetchFromAPI(base);
|
|
60
|
+
|
|
61
|
+
// Cache the fresh rates
|
|
62
|
+
await this.cacheRates(base, freshRates);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...freshRates,
|
|
66
|
+
source: 'api'
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('[Exchange Rates] Error getting exchange rates:', error);
|
|
70
|
+
throw new Error('Failed to retrieve exchange rates');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get specific currency pair rate
|
|
76
|
+
* @param {string} from - From currency
|
|
77
|
+
* @param {string} to - To currency
|
|
78
|
+
* @returns {Object} Exchange rate data
|
|
79
|
+
*/
|
|
80
|
+
async getCurrencyPair(from, to) {
|
|
81
|
+
if (!this.supportedCurrencies.includes(from) || !this.supportedCurrencies.includes(to)) {
|
|
82
|
+
throw new Error('Unsupported currency pair');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rates = await this.getExchangeRates(from);
|
|
86
|
+
|
|
87
|
+
if (!rates.rates[to]) {
|
|
88
|
+
throw new Error(`Exchange rate for ${from} to ${to} not available`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
from,
|
|
93
|
+
to,
|
|
94
|
+
rate: rates.rates[to],
|
|
95
|
+
timestamp: rates.timestamp,
|
|
96
|
+
date: rates.date,
|
|
97
|
+
source: rates.source
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Convert amount from one currency to another
|
|
103
|
+
* @param {number} amount - Amount to convert
|
|
104
|
+
* @param {string} from - From currency
|
|
105
|
+
* @param {string} to - To currency
|
|
106
|
+
* @returns {Object} Conversion result
|
|
107
|
+
*/
|
|
108
|
+
async convertCurrency(amount, from, to) {
|
|
109
|
+
const pairData = await this.getCurrencyPair(from, to);
|
|
110
|
+
const convertedAmount = amount * pairData.rate;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
originalAmount: amount,
|
|
114
|
+
convertedAmount: parseFloat(convertedAmount.toFixed(4)),
|
|
115
|
+
from,
|
|
116
|
+
to,
|
|
117
|
+
rate: pairData.rate,
|
|
118
|
+
timestamp: pairData.timestamp,
|
|
119
|
+
source: pairData.source
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get cached exchange rates from PostgreSQL
|
|
125
|
+
* @param {string} base - Base currency
|
|
126
|
+
* @returns {Object|null} Cached rates or null
|
|
127
|
+
*/
|
|
128
|
+
async getCachedRates(base) {
|
|
129
|
+
try {
|
|
130
|
+
const result = await pool.query(
|
|
131
|
+
'SELECT rates, last_updated FROM exchange_rates_cache WHERE base_currency = $1 AND expires_at > NOW()',
|
|
132
|
+
[base]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (result.rows.length > 0) {
|
|
136
|
+
const row = result.rows[0];
|
|
137
|
+
return {
|
|
138
|
+
...row.rates,
|
|
139
|
+
lastUpdated: row.last_updated
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return null;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('[Exchange Rates] Error getting cached rates:', error);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Cache exchange rates in PostgreSQL
|
|
152
|
+
* @param {string} base - Base currency
|
|
153
|
+
* @param {Object} rates - Exchange rates data
|
|
154
|
+
*/
|
|
155
|
+
async cacheRates(base, rates) {
|
|
156
|
+
try {
|
|
157
|
+
const expiresAt = new Date(Date.now() + this.cacheTTL * 1000);
|
|
158
|
+
|
|
159
|
+
await pool.query(`
|
|
160
|
+
INSERT INTO exchange_rates_cache (base_currency, rates, last_updated, expires_at)
|
|
161
|
+
VALUES ($1, $2, NOW(), $3)
|
|
162
|
+
ON CONFLICT (base_currency)
|
|
163
|
+
DO UPDATE SET
|
|
164
|
+
rates = $2,
|
|
165
|
+
last_updated = NOW(),
|
|
166
|
+
expires_at = $3
|
|
167
|
+
`, [base, JSON.stringify(rates), expiresAt]);
|
|
168
|
+
|
|
169
|
+
console.log(`[Exchange Rates] Cached ${base} rates with TTL: ${this.cacheTTL}s`);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('[Exchange Rates] Error caching rates:', error);
|
|
172
|
+
// Don't throw error, caching failure shouldn't break the service
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fetch exchange rates from external API
|
|
178
|
+
* @param {string} base - Base currency
|
|
179
|
+
* @returns {Object} Fresh exchange rates
|
|
180
|
+
*/
|
|
181
|
+
async fetchFromAPI(base) {
|
|
182
|
+
try {
|
|
183
|
+
const url = `${this.apiBaseUrl}/${this.apiKey}/latest/${base}`;
|
|
184
|
+
const response = await axios.get(url, {
|
|
185
|
+
timeout: 10000, // 10 second timeout
|
|
186
|
+
headers: {
|
|
187
|
+
'User-Agent': 'Dubs-Exchange-API/1.0'
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!response.data || !response.data.conversion_rates) {
|
|
192
|
+
throw new Error('Invalid response from exchange rate API');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check if the request was successful
|
|
196
|
+
if (response.data.result !== 'success') {
|
|
197
|
+
throw new Error(`API Error: ${response.data['error-type'] || 'Unknown error'}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Filter rates to only include supported currencies
|
|
201
|
+
const filteredRates = {};
|
|
202
|
+
for (const currency of this.supportedCurrencies) {
|
|
203
|
+
if (response.data.conversion_rates[currency] !== undefined) {
|
|
204
|
+
filteredRates[currency] = response.data.conversion_rates[currency];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
base: response.data.base_code,
|
|
210
|
+
date: new Date(response.data.time_last_update_unix * 1000).toISOString().split('T')[0],
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
rates: filteredRates,
|
|
213
|
+
lastUpdated: response.data.time_last_update_utc,
|
|
214
|
+
nextUpdate: response.data.time_next_update_utc
|
|
215
|
+
};
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error.response) {
|
|
218
|
+
console.error('[Exchange Rates] API Error:', error.response.status, error.response.data);
|
|
219
|
+
|
|
220
|
+
// Handle specific API errors
|
|
221
|
+
if (error.response.status === 403) {
|
|
222
|
+
throw new Error('Exchange rate API: Invalid or expired API key');
|
|
223
|
+
} else if (error.response.status === 404) {
|
|
224
|
+
throw new Error('Exchange rate API: Unsupported currency code');
|
|
225
|
+
} else if (error.response.status === 429) {
|
|
226
|
+
throw new Error('Exchange rate API: Rate limit exceeded');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw new Error(`Exchange rate API error: ${error.response.status}`);
|
|
230
|
+
} else if (error.request) {
|
|
231
|
+
console.error('[Exchange Rates] Network Error:', error.message);
|
|
232
|
+
throw new Error('Network error connecting to exchange rate API');
|
|
233
|
+
} else {
|
|
234
|
+
console.error('[Exchange Rates] Error:', error.message);
|
|
235
|
+
throw new Error('Failed to fetch exchange rates');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get list of supported currencies
|
|
242
|
+
* @returns {Array} List of supported currency codes
|
|
243
|
+
*/
|
|
244
|
+
getSupportedCurrencies() {
|
|
245
|
+
return this.supportedCurrencies;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Clear cache for a specific base currency
|
|
250
|
+
* @param {string} base - Base currency
|
|
251
|
+
*/
|
|
252
|
+
async clearCache(base = this.baseCurrency) {
|
|
253
|
+
try {
|
|
254
|
+
await pool.query(
|
|
255
|
+
'DELETE FROM exchange_rates_cache WHERE base_currency = $1',
|
|
256
|
+
[base]
|
|
257
|
+
);
|
|
258
|
+
console.log(`[Exchange Rates] Cache cleared for ${base}`);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('[Exchange Rates] Error clearing cache:', error);
|
|
261
|
+
throw new Error('Failed to clear cache');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Clean up expired cache entries
|
|
267
|
+
*/
|
|
268
|
+
async cleanupExpiredCache() {
|
|
269
|
+
try {
|
|
270
|
+
const result = await pool.query(
|
|
271
|
+
'DELETE FROM exchange_rates_cache WHERE expires_at < NOW()'
|
|
272
|
+
);
|
|
273
|
+
if (result.rowCount > 0) {
|
|
274
|
+
console.log(`[Exchange Rates] Cleaned up ${result.rowCount} expired cache entries`);
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error('[Exchange Rates] Error cleaning up cache:', error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = new ExchangeRateService();
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo Push Notification Service
|
|
3
|
+
* Handles token registration, push sending, and pick'em-specific notifications.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Expo } = require('expo-server-sdk');
|
|
7
|
+
const { pool } = require('./db');
|
|
8
|
+
|
|
9
|
+
const expo = new Expo();
|
|
10
|
+
|
|
11
|
+
// ── Token Management ──
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register or reactivate an Expo push token for a user.
|
|
15
|
+
*/
|
|
16
|
+
async function registerToken(userId, token, platform, deviceName, developerAppId) {
|
|
17
|
+
if (!Expo.isExpoPushToken(token)) {
|
|
18
|
+
throw new Error(`Invalid Expo push token: ${token}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = await pool.query(
|
|
22
|
+
`INSERT INTO expo_push_tokens (user_id, token, platform, device_name, developer_app_id, active, updated_at)
|
|
23
|
+
VALUES ($1, $2, $3, $4, $5, true, NOW())
|
|
24
|
+
ON CONFLICT (user_id, token)
|
|
25
|
+
DO UPDATE SET active = true, platform = $3, device_name = $4, developer_app_id = $5, updated_at = NOW()
|
|
26
|
+
RETURNING *`,
|
|
27
|
+
[userId, token, platform, deviceName || null, developerAppId || null]
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
console.log(`[ExpoPush] Token registered: ${token.slice(0, 30)}... for user ${userId}`);
|
|
31
|
+
return result.rows[0];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Deactivate a push token for a user.
|
|
36
|
+
*/
|
|
37
|
+
async function unregisterToken(userId, token) {
|
|
38
|
+
await pool.query(
|
|
39
|
+
`UPDATE expo_push_tokens SET active = false, updated_at = NOW()
|
|
40
|
+
WHERE user_id = $1 AND token = $2`,
|
|
41
|
+
[userId, token]
|
|
42
|
+
);
|
|
43
|
+
console.log(`[ExpoPush] Token unregistered for user ${userId}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get all active tokens for a user, optionally filtered by app.
|
|
48
|
+
*/
|
|
49
|
+
async function getTokensForUser(userId, developerAppId) {
|
|
50
|
+
const params = [userId];
|
|
51
|
+
let where = 'WHERE user_id = $1 AND active = true';
|
|
52
|
+
if (developerAppId) {
|
|
53
|
+
where += ' AND developer_app_id = $2';
|
|
54
|
+
params.push(developerAppId);
|
|
55
|
+
}
|
|
56
|
+
const result = await pool.query(
|
|
57
|
+
`SELECT token, platform, device_name, created_at, updated_at FROM expo_push_tokens ${where} ORDER BY updated_at DESC`,
|
|
58
|
+
params
|
|
59
|
+
);
|
|
60
|
+
return result.rows;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get all active tokens for an app with user info (for dashboard).
|
|
65
|
+
*/
|
|
66
|
+
async function getTokensForApp(developerAppId) {
|
|
67
|
+
const result = await pool.query(
|
|
68
|
+
`SELECT ept.token, ept.platform, ept.device_name, ept.active, ept.created_at,
|
|
69
|
+
u.wallet_address, u.username, u.avatar
|
|
70
|
+
FROM expo_push_tokens ept
|
|
71
|
+
JOIN users u ON ept.user_id = u.id
|
|
72
|
+
WHERE ept.developer_app_id = $1 AND ept.active = true
|
|
73
|
+
ORDER BY ept.updated_at DESC`,
|
|
74
|
+
[developerAppId]
|
|
75
|
+
);
|
|
76
|
+
return result.rows;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Push Sending ──
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Send a push notification to all active tokens for given user IDs.
|
|
83
|
+
* Auto-deactivates tokens that return DeviceNotRegistered.
|
|
84
|
+
*/
|
|
85
|
+
async function sendToUsers(userIds, notification) {
|
|
86
|
+
if (!userIds || userIds.length === 0) return { sent: 0, failed: 0 };
|
|
87
|
+
|
|
88
|
+
const result = await pool.query(
|
|
89
|
+
`SELECT id, user_id, token FROM expo_push_tokens
|
|
90
|
+
WHERE user_id = ANY($1) AND active = true`,
|
|
91
|
+
[userIds]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (result.rows.length === 0) return { sent: 0, failed: 0 };
|
|
95
|
+
|
|
96
|
+
const messages = result.rows
|
|
97
|
+
.filter(row => Expo.isExpoPushToken(row.token))
|
|
98
|
+
.map(row => ({
|
|
99
|
+
to: row.token,
|
|
100
|
+
sound: 'default',
|
|
101
|
+
title: notification.title,
|
|
102
|
+
body: notification.body,
|
|
103
|
+
data: notification.data || {},
|
|
104
|
+
channelId: notification.channelId || 'default',
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
if (messages.length === 0) return { sent: 0, failed: 0 };
|
|
108
|
+
|
|
109
|
+
const chunks = expo.chunkPushNotifications(messages);
|
|
110
|
+
let sent = 0;
|
|
111
|
+
let failed = 0;
|
|
112
|
+
const tokensToDeactivate = [];
|
|
113
|
+
|
|
114
|
+
for (const chunk of chunks) {
|
|
115
|
+
try {
|
|
116
|
+
const receipts = await expo.sendPushNotificationsAsync(chunk);
|
|
117
|
+
for (let i = 0; i < receipts.length; i++) {
|
|
118
|
+
const receipt = receipts[i];
|
|
119
|
+
if (receipt.status === 'ok') {
|
|
120
|
+
sent++;
|
|
121
|
+
} else {
|
|
122
|
+
failed++;
|
|
123
|
+
if (receipt.details?.error === 'DeviceNotRegistered') {
|
|
124
|
+
tokensToDeactivate.push(chunk[i].to);
|
|
125
|
+
}
|
|
126
|
+
console.error(`[ExpoPush] Error sending to ${chunk[i].to}: ${receipt.message || receipt.details?.error}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('[ExpoPush] Chunk send error:', err.message);
|
|
131
|
+
failed += chunk.length;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Deactivate stale tokens
|
|
136
|
+
if (tokensToDeactivate.length > 0) {
|
|
137
|
+
await pool.query(
|
|
138
|
+
`UPDATE expo_push_tokens SET active = false, updated_at = NOW()
|
|
139
|
+
WHERE token = ANY($1)`,
|
|
140
|
+
[tokensToDeactivate]
|
|
141
|
+
).catch(err => console.error('[ExpoPush] Failed to deactivate stale tokens:', err.message));
|
|
142
|
+
console.log(`[ExpoPush] Deactivated ${tokensToDeactivate.length} stale token(s)`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`[ExpoPush] Sent: ${sent}, Failed: ${failed}`);
|
|
146
|
+
return { sent, failed };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Send a push notification to a single user.
|
|
151
|
+
*/
|
|
152
|
+
async function sendToUser(userId, notification) {
|
|
153
|
+
return sendToUsers([userId], notification);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Pick'em Notification Helpers ──
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Notify users who picked on a fight that just went live.
|
|
160
|
+
*/
|
|
161
|
+
async function sendFightLiveNotifications(fightId, fight) {
|
|
162
|
+
// Get all users who have picks on this fight
|
|
163
|
+
const pickResult = await pool.query(
|
|
164
|
+
`SELECT DISTINCT pe.user_id
|
|
165
|
+
FROM pickem_picks pp
|
|
166
|
+
JOIN pickem_entries pe ON pp.entry_id = pe.id
|
|
167
|
+
WHERE pp.fight_id = $1`,
|
|
168
|
+
[fightId]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (pickResult.rows.length === 0) return;
|
|
172
|
+
|
|
173
|
+
const userIds = pickResult.rows.map(r => r.user_id);
|
|
174
|
+
const fighterA = fight.fighterAName || fight.fighter_a_name || 'Fighter A';
|
|
175
|
+
const fighterB = fight.fighterBName || fight.fighter_b_name || 'Fighter B';
|
|
176
|
+
|
|
177
|
+
await sendToUsers(userIds, {
|
|
178
|
+
title: 'Fight Starting NOW!',
|
|
179
|
+
body: `${fighterA} vs ${fighterB} is LIVE!`,
|
|
180
|
+
data: {
|
|
181
|
+
type: 'fight_live',
|
|
182
|
+
fightId: String(fightId),
|
|
183
|
+
poolId: String(fight.poolId || fight.pool_id),
|
|
184
|
+
},
|
|
185
|
+
channelId: 'pickem',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
console.log(`[ExpoPush] Fight live notifications sent for fight ${fightId} to ${userIds.length} user(s)`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Notify users who picked on a fight that just resolved.
|
|
193
|
+
* Sends different messages for correct vs incorrect picks.
|
|
194
|
+
*/
|
|
195
|
+
async function sendFightResolvedNotifications(fightId, fight) {
|
|
196
|
+
const winner = fight.winner;
|
|
197
|
+
if (!winner) return;
|
|
198
|
+
|
|
199
|
+
// Get all picks for this fight with user IDs
|
|
200
|
+
const pickResult = await pool.query(
|
|
201
|
+
`SELECT pp.pick, pe.user_id
|
|
202
|
+
FROM pickem_picks pp
|
|
203
|
+
JOIN pickem_entries pe ON pp.entry_id = pe.id
|
|
204
|
+
WHERE pp.fight_id = $1`,
|
|
205
|
+
[fightId]
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (pickResult.rows.length === 0) return;
|
|
209
|
+
|
|
210
|
+
const fighterA = fight.fighterAName || fight.fighter_a_name || 'Fighter A';
|
|
211
|
+
const fighterB = fight.fighterBName || fight.fighter_b_name || 'Fighter B';
|
|
212
|
+
const winnerName = winner === 'a' ? fighterA : fighterB;
|
|
213
|
+
const method = fight.method || '';
|
|
214
|
+
const methodStr = method ? ` by ${method}` : '';
|
|
215
|
+
const poolId = String(fight.poolId || fight.pool_id);
|
|
216
|
+
|
|
217
|
+
const correctUserIds = [];
|
|
218
|
+
const incorrectUserIds = [];
|
|
219
|
+
|
|
220
|
+
for (const row of pickResult.rows) {
|
|
221
|
+
if (row.pick === winner) {
|
|
222
|
+
correctUserIds.push(row.user_id);
|
|
223
|
+
} else {
|
|
224
|
+
incorrectUserIds.push(row.user_id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const promises = [];
|
|
229
|
+
|
|
230
|
+
if (correctUserIds.length > 0) {
|
|
231
|
+
promises.push(
|
|
232
|
+
sendToUsers(correctUserIds, {
|
|
233
|
+
title: 'Correct Pick!',
|
|
234
|
+
body: `${winnerName} wins${methodStr}! Your pick was correct!`,
|
|
235
|
+
data: { type: 'fight_resolved', fightId: String(fightId), poolId, result: 'correct' },
|
|
236
|
+
channelId: 'pickem',
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (incorrectUserIds.length > 0) {
|
|
242
|
+
promises.push(
|
|
243
|
+
sendToUsers(incorrectUserIds, {
|
|
244
|
+
title: 'Fight Result',
|
|
245
|
+
body: `${winnerName} wins${methodStr}. Better luck on the next fight!`,
|
|
246
|
+
data: { type: 'fight_resolved', fightId: String(fightId), poolId, result: 'incorrect' },
|
|
247
|
+
channelId: 'pickem',
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await Promise.all(promises);
|
|
253
|
+
console.log(`[ExpoPush] Fight resolved notifications: ${correctUserIds.length} correct, ${incorrectUserIds.length} incorrect`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Notify all users in a pool when it resolves with final rankings.
|
|
258
|
+
*/
|
|
259
|
+
async function sendPoolResolvedNotifications(poolId, scores) {
|
|
260
|
+
if (!scores || !scores.entries || scores.entries.length === 0) return;
|
|
261
|
+
|
|
262
|
+
const totalFights = scores.maxScore != null ? undefined : undefined; // scores contain entries with score/rank
|
|
263
|
+
|
|
264
|
+
// Get the total number of resolved fights for context
|
|
265
|
+
const fightCountResult = await pool.query(
|
|
266
|
+
`SELECT COUNT(*) as total FROM pickem_fights WHERE pool_id = $1 AND status = 'final'`,
|
|
267
|
+
[poolId]
|
|
268
|
+
);
|
|
269
|
+
const totalResolved = parseInt(fightCountResult.rows[0]?.total || '0');
|
|
270
|
+
|
|
271
|
+
const promises = [];
|
|
272
|
+
|
|
273
|
+
for (const entry of scores.entries) {
|
|
274
|
+
const rank = entry.rank;
|
|
275
|
+
const score = entry.score || 0;
|
|
276
|
+
const userId = entry.user_id || entry.userId;
|
|
277
|
+
if (!userId) continue;
|
|
278
|
+
|
|
279
|
+
const isWinner = rank === 1;
|
|
280
|
+
const title = isWinner ? "You Won the Pool!" : 'Pool Results';
|
|
281
|
+
const body = isWinner
|
|
282
|
+
? `Congratulations! You finished #1 with ${score}/${totalResolved} correct picks!`
|
|
283
|
+
: `Final results: You finished #${rank} with ${score}/${totalResolved} correct picks.`;
|
|
284
|
+
|
|
285
|
+
promises.push(
|
|
286
|
+
sendToUser(userId, {
|
|
287
|
+
title,
|
|
288
|
+
body,
|
|
289
|
+
data: {
|
|
290
|
+
type: 'pool_resolved',
|
|
291
|
+
poolId: String(poolId),
|
|
292
|
+
rank: String(rank),
|
|
293
|
+
score: String(score),
|
|
294
|
+
},
|
|
295
|
+
channelId: 'pickem',
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await Promise.all(promises);
|
|
301
|
+
console.log(`[ExpoPush] Pool resolved notifications sent for pool ${poolId} to ${scores.entries.length} user(s)`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = {
|
|
305
|
+
registerToken,
|
|
306
|
+
unregisterToken,
|
|
307
|
+
getTokensForUser,
|
|
308
|
+
getTokensForApp,
|
|
309
|
+
sendToUsers,
|
|
310
|
+
sendToUser,
|
|
311
|
+
sendFightLiveNotifications,
|
|
312
|
+
sendFightResolvedNotifications,
|
|
313
|
+
sendPoolResolvedNotifications,
|
|
314
|
+
};
|