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,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ๐ฐ Promo Treasury Service
|
|
3
|
+
*
|
|
4
|
+
* Manages the treasury wallet for sponsored game joins.
|
|
5
|
+
* This wallet pays buy-ins on behalf of users who have promo codes.
|
|
6
|
+
*
|
|
7
|
+
* Security considerations:
|
|
8
|
+
* - Treasury keypair should be loaded from secure environment variable in production
|
|
9
|
+
* - File-based loading only for local development/testing
|
|
10
|
+
* - Treasury balance should be monitored and alerts set for low balance
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { Connection, Keypair, PublicKey, LAMPORTS_PER_SOL } = require('@solana/web3.js');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// Configuration
|
|
18
|
+
const DEFAULT_KEYPAIR_PATH = path.join(__dirname, '../../dubs/secure_backups/promo_treasury_devnet.json');
|
|
19
|
+
const MIN_BALANCE_WARNING_SOL = 1.0; // Warn when below this balance
|
|
20
|
+
|
|
21
|
+
let treasuryKeypair = null;
|
|
22
|
+
let connection = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize the treasury service
|
|
26
|
+
* @param {Connection} conn - Solana connection
|
|
27
|
+
* @param {Object} options - Initialization options
|
|
28
|
+
*/
|
|
29
|
+
function initialize(conn, options = {}) {
|
|
30
|
+
connection = conn;
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
keypairPath = process.env.PROMO_TREASURY_KEYPAIR_PATH || DEFAULT_KEYPAIR_PATH,
|
|
34
|
+
keypairJson = process.env.PROMO_TREASURY_KEYPAIR // JSON string of secret key array
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
// Try loading from environment variable first (production)
|
|
38
|
+
if (keypairJson) {
|
|
39
|
+
try {
|
|
40
|
+
const secretKey = JSON.parse(keypairJson);
|
|
41
|
+
treasuryKeypair = Keypair.fromSecretKey(Uint8Array.from(secretKey));
|
|
42
|
+
console.log(`[PromoTreasury] โ
Treasury loaded from environment variable`);
|
|
43
|
+
console.log(`[PromoTreasury] Address: ${treasuryKeypair.publicKey.toBase58()}`);
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('[PromoTreasury] โ Error loading from env:', error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fall back to file-based loading (development)
|
|
51
|
+
if (fs.existsSync(keypairPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const secretKey = JSON.parse(fs.readFileSync(keypairPath, 'utf-8'));
|
|
54
|
+
treasuryKeypair = Keypair.fromSecretKey(Uint8Array.from(secretKey));
|
|
55
|
+
console.log(`[PromoTreasury] โ
Treasury loaded from file: ${keypairPath}`);
|
|
56
|
+
console.log(`[PromoTreasury] Address: ${treasuryKeypair.publicKey.toBase58()}`);
|
|
57
|
+
return true;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`[PromoTreasury] โ Error loading from file:`, error.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.warn(`[PromoTreasury] โ ๏ธ Treasury not initialized - promo codes will not work`);
|
|
64
|
+
console.warn(`[PromoTreasury] Set PROMO_TREASURY_KEYPAIR env var or provide keypair file`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if treasury is initialized and ready
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
function isReady() {
|
|
73
|
+
return treasuryKeypair !== null && connection !== null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get treasury public key
|
|
78
|
+
* @returns {PublicKey|null}
|
|
79
|
+
*/
|
|
80
|
+
function getPublicKey() {
|
|
81
|
+
return treasuryKeypair?.publicKey || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get treasury keypair for signing transactions
|
|
86
|
+
* WARNING: Handle with care - this returns the full keypair with private key
|
|
87
|
+
* @returns {Keypair|null}
|
|
88
|
+
*/
|
|
89
|
+
function getKeypair() {
|
|
90
|
+
return treasuryKeypair;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get current treasury balance
|
|
95
|
+
* @returns {Object} - Balance in lamports and SOL
|
|
96
|
+
*/
|
|
97
|
+
async function getBalance() {
|
|
98
|
+
if (!isReady()) {
|
|
99
|
+
throw new Error('Treasury not initialized');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const lamports = await connection.getBalance(treasuryKeypair.publicKey);
|
|
104
|
+
const sol = lamports / LAMPORTS_PER_SOL;
|
|
105
|
+
|
|
106
|
+
// Log warning if balance is low
|
|
107
|
+
if (sol < MIN_BALANCE_WARNING_SOL) {
|
|
108
|
+
console.warn(`[PromoTreasury] โ ๏ธ LOW BALANCE: ${sol.toFixed(4)} SOL`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
lamports,
|
|
113
|
+
sol,
|
|
114
|
+
isLow: sol < MIN_BALANCE_WARNING_SOL,
|
|
115
|
+
address: treasuryKeypair.publicKey.toBase58()
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('[PromoTreasury] Error getting balance:', error.message);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if treasury has enough balance for a sponsored join
|
|
126
|
+
* @param {number} amountLamports - Required amount in lamports
|
|
127
|
+
* @returns {Object} - Check result
|
|
128
|
+
*/
|
|
129
|
+
async function hasEnoughBalance(amountLamports) {
|
|
130
|
+
if (!isReady()) {
|
|
131
|
+
return { enough: false, error: 'Treasury not initialized' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const balance = await getBalance();
|
|
136
|
+
|
|
137
|
+
// Need extra for transaction fees (~5000 lamports per tx + rent if realloc)
|
|
138
|
+
const requiredWithBuffer = amountLamports + 100_000; // 0.0001 SOL buffer for fees
|
|
139
|
+
|
|
140
|
+
if (balance.lamports < requiredWithBuffer) {
|
|
141
|
+
return {
|
|
142
|
+
enough: false,
|
|
143
|
+
error: 'Treasury has insufficient funds',
|
|
144
|
+
currentBalance: balance.lamports,
|
|
145
|
+
required: requiredWithBuffer,
|
|
146
|
+
shortfall: requiredWithBuffer - balance.lamports
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
enough: true,
|
|
152
|
+
currentBalance: balance.lamports,
|
|
153
|
+
required: requiredWithBuffer,
|
|
154
|
+
remaining: balance.lamports - requiredWithBuffer
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('[PromoTreasury] Error checking balance:', error.message);
|
|
159
|
+
return { enough: false, error: error.message };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get treasury info for admin dashboard
|
|
165
|
+
* @returns {Object} - Treasury wallet info
|
|
166
|
+
*/
|
|
167
|
+
async function getTreasuryInfo() {
|
|
168
|
+
if (!isReady()) {
|
|
169
|
+
throw new Error('Treasury not initialized');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const lamports = await connection.getBalance(treasuryKeypair.publicKey);
|
|
174
|
+
const sol = lamports / LAMPORTS_PER_SOL;
|
|
175
|
+
|
|
176
|
+
// Determine network from connection endpoint
|
|
177
|
+
const endpoint = connection.rpcEndpoint || '';
|
|
178
|
+
let network = 'unknown';
|
|
179
|
+
if (endpoint.includes('devnet')) network = 'devnet';
|
|
180
|
+
else if (endpoint.includes('mainnet')) network = 'mainnet-beta';
|
|
181
|
+
else if (endpoint.includes('localhost') || endpoint.includes('127.0.0.1')) network = 'localnet';
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
address: treasuryKeypair.publicKey.toBase58(),
|
|
185
|
+
balanceLamports: lamports,
|
|
186
|
+
balanceSOL: sol,
|
|
187
|
+
isLow: sol < MIN_BALANCE_WARNING_SOL,
|
|
188
|
+
minBalanceWarning: MIN_BALANCE_WARNING_SOL,
|
|
189
|
+
network,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('[PromoTreasury] Error getting treasury info:', error.message);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get treasury status/health check
|
|
200
|
+
* @returns {Object} - Status information
|
|
201
|
+
*/
|
|
202
|
+
async function getStatus() {
|
|
203
|
+
const status = {
|
|
204
|
+
initialized: isReady(),
|
|
205
|
+
address: getPublicKey()?.toBase58() || null,
|
|
206
|
+
balance: null,
|
|
207
|
+
healthy: false
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (status.initialized) {
|
|
211
|
+
try {
|
|
212
|
+
status.balance = await getBalance();
|
|
213
|
+
status.healthy = !status.balance.isLow;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
status.error = error.message;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return status;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
// Configuration
|
|
224
|
+
MIN_BALANCE_WARNING_SOL,
|
|
225
|
+
|
|
226
|
+
// Initialization
|
|
227
|
+
initialize,
|
|
228
|
+
isReady,
|
|
229
|
+
|
|
230
|
+
// Wallet access
|
|
231
|
+
getPublicKey,
|
|
232
|
+
getKeypair,
|
|
233
|
+
|
|
234
|
+
// Balance checks
|
|
235
|
+
getBalance,
|
|
236
|
+
hasEnoughBalance,
|
|
237
|
+
getStatus,
|
|
238
|
+
getTreasuryInfo,
|
|
239
|
+
};
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// ๐ Web Push Notification Service
|
|
2
|
+
// Sends push notifications to PWA/seeker mode users
|
|
3
|
+
|
|
4
|
+
const webpush = require('web-push');
|
|
5
|
+
const urlHelper = require('../utils/urlHelper');
|
|
6
|
+
|
|
7
|
+
// VAPID keys for Web Push authentication
|
|
8
|
+
const VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY;
|
|
9
|
+
const VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY;
|
|
10
|
+
const VAPID_SUBJECT = process.env.VAPID_SUBJECT || 'mailto:support@dubs.app';
|
|
11
|
+
|
|
12
|
+
let isInitialized = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize web-push with VAPID keys
|
|
16
|
+
* Call this once on server startup
|
|
17
|
+
*/
|
|
18
|
+
function initializeWebPush() {
|
|
19
|
+
if (!VAPID_PUBLIC_KEY || !VAPID_PRIVATE_KEY) {
|
|
20
|
+
console.log('[PushNotif] โ ๏ธ VAPID keys not set - push notifications disabled');
|
|
21
|
+
console.log('[PushNotif] โน๏ธ Generate keys with: npx web-push generate-vapid-keys');
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
webpush.setVapidDetails(
|
|
27
|
+
VAPID_SUBJECT,
|
|
28
|
+
VAPID_PUBLIC_KEY,
|
|
29
|
+
VAPID_PRIVATE_KEY
|
|
30
|
+
);
|
|
31
|
+
isInitialized = true;
|
|
32
|
+
console.log('[PushNotif] โ
Web Push initialized with VAPID keys');
|
|
33
|
+
return true;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('[PushNotif] โ Failed to initialize web-push:', error.message);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the VAPID public key (needed by client to subscribe)
|
|
42
|
+
*/
|
|
43
|
+
function getVapidPublicKey() {
|
|
44
|
+
return VAPID_PUBLIC_KEY;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Send a push notification to a subscription
|
|
49
|
+
* @param {object} subscription - Push subscription object { endpoint, keys: { p256dh, auth } }
|
|
50
|
+
* @param {object} payload - Notification payload { title, body, icon, data }
|
|
51
|
+
* @returns {Promise<boolean>} - true if sent successfully
|
|
52
|
+
*/
|
|
53
|
+
async function sendPushNotification(subscription, payload) {
|
|
54
|
+
if (!isInitialized) {
|
|
55
|
+
console.log('[PushNotif] โ ๏ธ Web Push not initialized - skipping notification');
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const pushPayload = JSON.stringify({
|
|
61
|
+
title: payload.title || 'Dubs',
|
|
62
|
+
body: payload.body || '',
|
|
63
|
+
icon: payload.icon || '/icon-192.png',
|
|
64
|
+
badge: payload.badge || '/badge-72.png',
|
|
65
|
+
data: payload.data || {},
|
|
66
|
+
tag: payload.tag || 'dubs-notification',
|
|
67
|
+
requireInteraction: payload.requireInteraction || false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(`[PushNotif] ๐ Sending push to endpoint: ${subscription.endpoint.substring(0, 50)}...`);
|
|
71
|
+
|
|
72
|
+
await webpush.sendNotification(subscription, pushPayload);
|
|
73
|
+
|
|
74
|
+
console.log('[PushNotif] โ
Push notification sent successfully');
|
|
75
|
+
return true;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Handle specific error codes
|
|
78
|
+
if (error.statusCode === 410 || error.statusCode === 404) {
|
|
79
|
+
// Subscription has expired or is invalid
|
|
80
|
+
console.log('[PushNotif] โ ๏ธ Subscription expired/invalid (will be cleaned up)');
|
|
81
|
+
return { expired: true, endpoint: subscription.endpoint };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.error('[PushNotif] โ Failed to send push notification:', error.message);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Forward notification to push if user has subscriptions
|
|
91
|
+
* Mirrors telegramNotifications.forwardChatNotification()
|
|
92
|
+
*
|
|
93
|
+
* @param {object} pool - Database pool
|
|
94
|
+
* @param {number} userId - User ID to send notification to
|
|
95
|
+
* @param {string} notificationType - Type of notification (game_invite, game_joined, etc.)
|
|
96
|
+
* @param {string} senderUsername - Username of the sender
|
|
97
|
+
* @param {string} message - Notification message text
|
|
98
|
+
* @param {object} metadata - Optional metadata (e.g., { gameId: 'sport-xxx' })
|
|
99
|
+
*/
|
|
100
|
+
async function forwardPushNotification(pool, userId, notificationType, senderUsername, message = '', metadata = {}) {
|
|
101
|
+
try {
|
|
102
|
+
console.log(`[PushNotif] ๐ค Attempting to forward ${notificationType} notification to user ${userId} from @${senderUsername}`);
|
|
103
|
+
|
|
104
|
+
// Get user's push subscriptions and preferences
|
|
105
|
+
const result = await pool.query(
|
|
106
|
+
`SELECT s.id, s.endpoint, s.p256dh, s.auth, s.device_type, u.username,
|
|
107
|
+
COALESCE(p.notify_reply, true) as notify_reply,
|
|
108
|
+
COALESCE(p.notify_reaction, true) as notify_reaction,
|
|
109
|
+
COALESCE(p.notify_friend_request, true) as notify_friend_request,
|
|
110
|
+
COALESCE(p.notify_friend_request_accepted, true) as notify_friend_request_accepted,
|
|
111
|
+
COALESCE(p.notify_friend_request_declined, true) as notify_friend_request_declined,
|
|
112
|
+
COALESCE(p.notify_referral, true) as notify_referral,
|
|
113
|
+
COALESCE(p.notify_mention, true) as notify_mention,
|
|
114
|
+
COALESCE(p.notify_friend_message, true) as notify_friend_message,
|
|
115
|
+
COALESCE(p.notify_game_joined, true) as notify_game_joined,
|
|
116
|
+
COALESCE(p.notify_game_invite, true) as notify_game_invite
|
|
117
|
+
FROM push_subscriptions s
|
|
118
|
+
JOIN users u ON s.user_id = u.id
|
|
119
|
+
LEFT JOIN push_notification_preferences p ON u.id = p.user_id
|
|
120
|
+
WHERE s.user_id = $1`,
|
|
121
|
+
[userId]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (result.rows.length === 0) {
|
|
125
|
+
console.log(`[PushNotif] โญ๏ธ User ${userId} doesn't have any push subscriptions - skipping`);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const subscriptions = result.rows;
|
|
130
|
+
const prefs = subscriptions[0]; // All rows have same prefs
|
|
131
|
+
const recipientUsername = prefs.username;
|
|
132
|
+
|
|
133
|
+
console.log(`[PushNotif] ๐ค Found user @${recipientUsername} with ${subscriptions.length} push subscription(s)`);
|
|
134
|
+
|
|
135
|
+
// Check if user wants this notification type
|
|
136
|
+
const prefMap = {
|
|
137
|
+
'reply': prefs.notify_reply,
|
|
138
|
+
'reaction': prefs.notify_reaction,
|
|
139
|
+
'friend_request': prefs.notify_friend_request,
|
|
140
|
+
'friend_request_accepted': prefs.notify_friend_request_accepted,
|
|
141
|
+
'friend_request_declined': prefs.notify_friend_request_declined,
|
|
142
|
+
'referral': prefs.notify_referral,
|
|
143
|
+
'mention': prefs.notify_mention,
|
|
144
|
+
'friend_message': prefs.notify_friend_message,
|
|
145
|
+
'game_joined': prefs.notify_game_joined,
|
|
146
|
+
'game_invite': prefs.notify_game_invite,
|
|
147
|
+
'dm_message': prefs.notify_friend_message, // Use friend_message pref for DMs
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (prefMap[notificationType] === false) {
|
|
151
|
+
console.log(`[PushNotif] ๐ User @${recipientUsername} has disabled ${notificationType} notifications - skipping`);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Format notification payload based on type
|
|
156
|
+
let title = 'Dubs';
|
|
157
|
+
let body = '';
|
|
158
|
+
let icon = '/icon-192.png';
|
|
159
|
+
let tag = 'dubs-notification';
|
|
160
|
+
let url = 'https://dubs.app';
|
|
161
|
+
|
|
162
|
+
switch (notificationType) {
|
|
163
|
+
case 'reply':
|
|
164
|
+
title = `${senderUsername} replied`;
|
|
165
|
+
body = message || 'Replied to your message';
|
|
166
|
+
tag = 'dubs-reply';
|
|
167
|
+
break;
|
|
168
|
+
case 'reaction':
|
|
169
|
+
title = `${senderUsername} reacted ${message}`;
|
|
170
|
+
body = 'To your message';
|
|
171
|
+
tag = 'dubs-reaction';
|
|
172
|
+
break;
|
|
173
|
+
case 'friend_request':
|
|
174
|
+
title = 'New Friend Request';
|
|
175
|
+
body = `${senderUsername} sent you a friend request`;
|
|
176
|
+
tag = 'dubs-friend-request';
|
|
177
|
+
break;
|
|
178
|
+
case 'friend_request_accepted':
|
|
179
|
+
title = 'Friend Request Accepted';
|
|
180
|
+
body = `${senderUsername} accepted your friend request!`;
|
|
181
|
+
tag = 'dubs-friend-accepted';
|
|
182
|
+
break;
|
|
183
|
+
case 'friend_request_declined':
|
|
184
|
+
title = 'Friend Request Declined';
|
|
185
|
+
body = `${senderUsername} declined your friend request`;
|
|
186
|
+
tag = 'dubs-friend-declined';
|
|
187
|
+
break;
|
|
188
|
+
case 'referral':
|
|
189
|
+
title = 'New Referral!';
|
|
190
|
+
body = `${senderUsername} joined using your referral code!`;
|
|
191
|
+
tag = 'dubs-referral';
|
|
192
|
+
break;
|
|
193
|
+
case 'game_joined':
|
|
194
|
+
title = `${senderUsername} joined your bet!`;
|
|
195
|
+
body = message || 'Someone joined your game';
|
|
196
|
+
tag = 'dubs-game-joined';
|
|
197
|
+
if (metadata.gameId) {
|
|
198
|
+
url = urlHelper.getGameShareUrl(metadata.gameId);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case 'game_invite':
|
|
202
|
+
title = `${senderUsername} invited you!`;
|
|
203
|
+
body = message || 'You\'ve been invited to join a bet';
|
|
204
|
+
tag = 'dubs-game-invite';
|
|
205
|
+
if (metadata.gameId) {
|
|
206
|
+
url = urlHelper.getGameShareUrl(metadata.gameId);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
case 'mention':
|
|
210
|
+
title = `${senderUsername} mentioned you`;
|
|
211
|
+
body = message || 'You were mentioned in a message';
|
|
212
|
+
tag = 'dubs-mention';
|
|
213
|
+
break;
|
|
214
|
+
case 'friend_message':
|
|
215
|
+
title = `Message from ${senderUsername}`;
|
|
216
|
+
body = message || 'New message';
|
|
217
|
+
tag = 'dubs-message';
|
|
218
|
+
break;
|
|
219
|
+
case 'dm_message':
|
|
220
|
+
title = `DM from ${senderUsername}`;
|
|
221
|
+
body = message || 'New direct message';
|
|
222
|
+
tag = 'dubs-dm';
|
|
223
|
+
break;
|
|
224
|
+
case 'game_won':
|
|
225
|
+
title = 'You Won!';
|
|
226
|
+
body = message || 'Congratulations on your win!';
|
|
227
|
+
tag = 'dubs-game-won';
|
|
228
|
+
if (metadata.gameId) {
|
|
229
|
+
url = urlHelper.getGameShareUrl(metadata.gameId);
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
case 'game_lost':
|
|
233
|
+
title = 'Game Finished';
|
|
234
|
+
body = message || 'Better luck next time!';
|
|
235
|
+
tag = 'dubs-game-lost';
|
|
236
|
+
if (metadata.gameId) {
|
|
237
|
+
url = urlHelper.getGameShareUrl(metadata.gameId);
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
case 'game_starting_soon':
|
|
241
|
+
title = 'Game Starting Soon!';
|
|
242
|
+
body = message || 'Your game is about to start';
|
|
243
|
+
tag = 'dubs-game-starting';
|
|
244
|
+
if (metadata.gameId) {
|
|
245
|
+
url = urlHelper.getGameShareUrl(metadata.gameId);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
case 'game_starting_now':
|
|
249
|
+
title = 'Game Starting NOW!';
|
|
250
|
+
body = message || 'Betting is now closed';
|
|
251
|
+
tag = 'dubs-game-live';
|
|
252
|
+
if (metadata.gameId) {
|
|
253
|
+
url = urlHelper.getGameShareUrl(metadata.gameId);
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
case 'whats_new':
|
|
257
|
+
title = "What's New on Dubs!";
|
|
258
|
+
body = message || 'Check out the latest updates';
|
|
259
|
+
tag = 'dubs-whats-new';
|
|
260
|
+
if (metadata.postId) {
|
|
261
|
+
url = urlHelper.getWhatsNewUrl(metadata.postId);
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
default:
|
|
265
|
+
title = 'Dubs Notification';
|
|
266
|
+
body = `${senderUsername}: ${notificationType}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const payload = {
|
|
270
|
+
title,
|
|
271
|
+
body,
|
|
272
|
+
icon,
|
|
273
|
+
tag,
|
|
274
|
+
data: {
|
|
275
|
+
url,
|
|
276
|
+
notificationType,
|
|
277
|
+
...metadata
|
|
278
|
+
},
|
|
279
|
+
requireInteraction: ['game_invite', 'friend_request', 'game_won'].includes(notificationType),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
console.log(`[PushNotif] ๐จ Sending ${notificationType} to ${subscriptions.length} device(s)...`);
|
|
283
|
+
|
|
284
|
+
// Send to all user's subscriptions
|
|
285
|
+
const expiredEndpoints = [];
|
|
286
|
+
let successCount = 0;
|
|
287
|
+
|
|
288
|
+
for (const sub of subscriptions) {
|
|
289
|
+
const subscription = {
|
|
290
|
+
endpoint: sub.endpoint,
|
|
291
|
+
keys: {
|
|
292
|
+
p256dh: sub.p256dh,
|
|
293
|
+
auth: sub.auth
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const result = await sendPushNotification(subscription, payload);
|
|
298
|
+
|
|
299
|
+
if (result === true) {
|
|
300
|
+
successCount++;
|
|
301
|
+
} else if (result && result.expired) {
|
|
302
|
+
expiredEndpoints.push(sub.id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Clean up expired subscriptions
|
|
307
|
+
if (expiredEndpoints.length > 0) {
|
|
308
|
+
console.log(`[PushNotif] ๐งน Cleaning up ${expiredEndpoints.length} expired subscription(s)`);
|
|
309
|
+
await pool.query(
|
|
310
|
+
'DELETE FROM push_subscriptions WHERE id = ANY($1)',
|
|
311
|
+
[expiredEndpoints]
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (successCount > 0) {
|
|
316
|
+
console.log(`[PushNotif] โ
Successfully sent ${notificationType} to ${successCount}/${subscriptions.length} device(s) for @${recipientUsername}`);
|
|
317
|
+
return true;
|
|
318
|
+
} else {
|
|
319
|
+
console.log(`[PushNotif] โ Failed to send ${notificationType} to any device for @${recipientUsername}`);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error(`[PushNotif] โ Error forwarding ${notificationType} notification:`, error.message);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Check if a user has push notifications enabled
|
|
330
|
+
* @param {object} pool - Database pool
|
|
331
|
+
* @param {number} userId - User ID to check
|
|
332
|
+
* @returns {Promise<boolean>}
|
|
333
|
+
*/
|
|
334
|
+
async function userHasPushSubscription(pool, userId) {
|
|
335
|
+
try {
|
|
336
|
+
const result = await pool.query(
|
|
337
|
+
'SELECT COUNT(*) as count FROM push_subscriptions WHERE user_id = $1',
|
|
338
|
+
[userId]
|
|
339
|
+
);
|
|
340
|
+
return parseInt(result.rows[0].count) > 0;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error('[PushNotif] Error checking push subscription:', error.message);
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = {
|
|
348
|
+
initializeWebPush,
|
|
349
|
+
getVapidPublicKey,
|
|
350
|
+
sendPushNotification,
|
|
351
|
+
forwardPushNotification,
|
|
352
|
+
userHasPushSubscription
|
|
353
|
+
};
|