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,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 📢 What's New Service
|
|
3
|
+
*
|
|
4
|
+
* Manages feature announcements and user read tracking
|
|
5
|
+
* Admin wallet: Hvv1ctqHLR5wonuuRguefS6EpGUe7tFRBX2YWHGr3mes
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { pool } = require('./db');
|
|
9
|
+
const notificationCacheService = require('./notificationCacheService');
|
|
10
|
+
const { forwardChatNotification } = require('./telegramNotifications');
|
|
11
|
+
|
|
12
|
+
// Admin wallet - only this address can create/edit/delete posts
|
|
13
|
+
const ADMIN_WALLET = 'Hvv1ctqHLR5wonuuRguefS6EpGUe7tFRBX2YWHGr3mes';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if a wallet address is an admin
|
|
17
|
+
*/
|
|
18
|
+
function isAdmin(walletAddress) {
|
|
19
|
+
return walletAddress === ADMIN_WALLET;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get all published posts (for users)
|
|
24
|
+
* Ordered by: pinned first, then by created_at desc
|
|
25
|
+
*/
|
|
26
|
+
async function getPosts(userId = null) {
|
|
27
|
+
try {
|
|
28
|
+
let query;
|
|
29
|
+
let params;
|
|
30
|
+
|
|
31
|
+
if (userId) {
|
|
32
|
+
// Include read status for authenticated users
|
|
33
|
+
query = `
|
|
34
|
+
SELECT
|
|
35
|
+
p.*,
|
|
36
|
+
CASE WHEN r.id IS NOT NULL THEN TRUE ELSE FALSE END as is_read
|
|
37
|
+
FROM whats_new_posts p
|
|
38
|
+
LEFT JOIN user_whats_new_reads r ON r.post_id = p.id AND r.user_id = $1
|
|
39
|
+
WHERE p.is_published = TRUE
|
|
40
|
+
ORDER BY p.is_pinned DESC, p.created_at DESC
|
|
41
|
+
LIMIT 50
|
|
42
|
+
`;
|
|
43
|
+
params = [userId];
|
|
44
|
+
} else {
|
|
45
|
+
// For unauthenticated users, just return posts
|
|
46
|
+
query = `
|
|
47
|
+
SELECT p.*, FALSE as is_read
|
|
48
|
+
FROM whats_new_posts p
|
|
49
|
+
WHERE p.is_published = TRUE
|
|
50
|
+
ORDER BY p.is_pinned DESC, p.created_at DESC
|
|
51
|
+
LIMIT 50
|
|
52
|
+
`;
|
|
53
|
+
params = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = await pool.query(query, params);
|
|
57
|
+
return result.rows;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('[WhatsNew] Error fetching posts:', error);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get all posts including unpublished (for admin)
|
|
66
|
+
*/
|
|
67
|
+
async function getAllPostsAdmin() {
|
|
68
|
+
try {
|
|
69
|
+
const query = `
|
|
70
|
+
SELECT *
|
|
71
|
+
FROM whats_new_posts
|
|
72
|
+
ORDER BY is_pinned DESC, created_at DESC
|
|
73
|
+
LIMIT 100
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const result = await pool.query(query);
|
|
77
|
+
return result.rows;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('[WhatsNew] Error fetching admin posts:', error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get unread count for a user
|
|
86
|
+
*/
|
|
87
|
+
async function getUnreadCount(userId) {
|
|
88
|
+
try {
|
|
89
|
+
const query = `
|
|
90
|
+
SELECT COUNT(*) as count
|
|
91
|
+
FROM whats_new_posts p
|
|
92
|
+
LEFT JOIN user_whats_new_reads r ON r.post_id = p.id AND r.user_id = $1
|
|
93
|
+
WHERE p.is_published = TRUE AND r.id IS NULL
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
const result = await pool.query(query, [userId]);
|
|
97
|
+
return parseInt(result.rows[0].count, 10) || 0;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('[WhatsNew] Error getting unread count:', error);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Mark posts as read for a user
|
|
106
|
+
*/
|
|
107
|
+
async function markAsRead(userId, postIds) {
|
|
108
|
+
if (!postIds || postIds.length === 0) return;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Use INSERT ... ON CONFLICT to handle duplicates gracefully
|
|
112
|
+
const values = postIds.map((postId, idx) => `($1, $${idx + 2})`).join(', ');
|
|
113
|
+
const query = `
|
|
114
|
+
INSERT INTO user_whats_new_reads (user_id, post_id)
|
|
115
|
+
VALUES ${values}
|
|
116
|
+
ON CONFLICT (user_id, post_id) DO NOTHING
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
await pool.query(query, [userId, ...postIds]);
|
|
120
|
+
console.log(`[WhatsNew] Marked ${postIds.length} posts as read for user ${userId}`);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('[WhatsNew] Error marking posts as read:', error);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Mark all posts as read for a user
|
|
129
|
+
*/
|
|
130
|
+
async function markAllAsRead(userId) {
|
|
131
|
+
try {
|
|
132
|
+
const query = `
|
|
133
|
+
INSERT INTO user_whats_new_reads (user_id, post_id)
|
|
134
|
+
SELECT $1, p.id
|
|
135
|
+
FROM whats_new_posts p
|
|
136
|
+
LEFT JOIN user_whats_new_reads r ON r.post_id = p.id AND r.user_id = $1
|
|
137
|
+
WHERE p.is_published = TRUE AND r.id IS NULL
|
|
138
|
+
ON CONFLICT (user_id, post_id) DO NOTHING
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
const result = await pool.query(query, [userId]);
|
|
142
|
+
console.log(`[WhatsNew] Marked all posts as read for user ${userId}`);
|
|
143
|
+
return result.rowCount;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('[WhatsNew] Error marking all as read:', error);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Broadcast notification to all users when a new post is published
|
|
152
|
+
*/
|
|
153
|
+
async function broadcastNotification(post) {
|
|
154
|
+
try {
|
|
155
|
+
console.log(`[WhatsNew] 📢 Broadcasting notification for post: "${post.title}" (id: ${post.id})`);
|
|
156
|
+
|
|
157
|
+
// Get all active users
|
|
158
|
+
const usersResult = await pool.query(`
|
|
159
|
+
SELECT id, wallet_address, username
|
|
160
|
+
FROM users
|
|
161
|
+
WHERE onboarding_complete = TRUE
|
|
162
|
+
`);
|
|
163
|
+
|
|
164
|
+
const users = usersResult.rows;
|
|
165
|
+
console.log(`[WhatsNew] Found ${users.length} users to notify`);
|
|
166
|
+
|
|
167
|
+
if (users.length === 0) {
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Build notification data
|
|
172
|
+
const notificationData = {
|
|
173
|
+
postId: post.id,
|
|
174
|
+
title: post.title,
|
|
175
|
+
category: post.category,
|
|
176
|
+
version: post.version,
|
|
177
|
+
gifUrl: post.gif_url,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Batch insert notifications for all users
|
|
181
|
+
// Using a CTE to generate values for all users
|
|
182
|
+
const insertQuery = `
|
|
183
|
+
INSERT INTO chat_notifications (user_id, notification_type, notification_data, read, created_at)
|
|
184
|
+
SELECT
|
|
185
|
+
u.id,
|
|
186
|
+
'whats_new',
|
|
187
|
+
$1::jsonb,
|
|
188
|
+
FALSE,
|
|
189
|
+
NOW()
|
|
190
|
+
FROM users u
|
|
191
|
+
WHERE u.onboarding_complete = TRUE
|
|
192
|
+
RETURNING id, user_id
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
const insertResult = await pool.query(insertQuery, [JSON.stringify(notificationData)]);
|
|
196
|
+
const insertedCount = insertResult.rowCount;
|
|
197
|
+
|
|
198
|
+
console.log(`[WhatsNew] ✅ Created ${insertedCount} notifications for post "${post.title}"`);
|
|
199
|
+
|
|
200
|
+
// Get chatNamespace for WebSocket emissions
|
|
201
|
+
const chatNamespace = global.chatNamespace;
|
|
202
|
+
|
|
203
|
+
// Cache notifications to Redis and emit WebSocket events for each user
|
|
204
|
+
for (const row of insertResult.rows) {
|
|
205
|
+
const notificationPayload = {
|
|
206
|
+
id: row.id,
|
|
207
|
+
type: 'whats_new',
|
|
208
|
+
read: false,
|
|
209
|
+
messageId: null,
|
|
210
|
+
message: post.title,
|
|
211
|
+
senderUsername: 'Dubs Team',
|
|
212
|
+
senderWallet: ADMIN_WALLET,
|
|
213
|
+
senderAvatar: null,
|
|
214
|
+
createdAt: new Date().toISOString(),
|
|
215
|
+
notificationData: notificationData,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Cache to Redis (non-blocking)
|
|
219
|
+
notificationCacheService.cacheNotification(row.user_id, notificationPayload)
|
|
220
|
+
.catch(err => console.error('[WhatsNew] Cache error for user', row.user_id, ':', err.message));
|
|
221
|
+
|
|
222
|
+
// Emit WebSocket event to connected users
|
|
223
|
+
if (chatNamespace) {
|
|
224
|
+
chatNamespace.to(`user-${row.user_id}`).emit('notification', notificationPayload);
|
|
225
|
+
console.log(`[WhatsNew] 🔔 WebSocket notification sent to user-${row.user_id}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Send Telegram notification (non-blocking)
|
|
229
|
+
forwardChatNotification(
|
|
230
|
+
pool,
|
|
231
|
+
row.user_id,
|
|
232
|
+
'whats_new',
|
|
233
|
+
'Dubs Team',
|
|
234
|
+
post.title,
|
|
235
|
+
{ postId: post.id }
|
|
236
|
+
).catch(err => console.error('[WhatsNew] Telegram error for user', row.user_id, ':', err.message));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return insertedCount;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('[WhatsNew] Error broadcasting notification:', error);
|
|
242
|
+
// Don't throw - this shouldn't block post creation
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Create a new post (admin only)
|
|
249
|
+
*/
|
|
250
|
+
async function createPost({ title, content, gifUrl, category, version, isPinned, isPublished, createdBy }) {
|
|
251
|
+
try {
|
|
252
|
+
const query = `
|
|
253
|
+
INSERT INTO whats_new_posts (title, content, gif_url, category, version, is_pinned, is_published, created_by)
|
|
254
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
255
|
+
RETURNING *
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
const result = await pool.query(query, [
|
|
259
|
+
title,
|
|
260
|
+
content,
|
|
261
|
+
gifUrl || null,
|
|
262
|
+
category || 'feature',
|
|
263
|
+
version || null,
|
|
264
|
+
isPinned || false,
|
|
265
|
+
isPublished !== false, // Default to true
|
|
266
|
+
createdBy,
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
const post = result.rows[0];
|
|
270
|
+
console.log('[WhatsNew] Created post:', post.id, title);
|
|
271
|
+
|
|
272
|
+
// If the post is published, broadcast notifications to all users
|
|
273
|
+
if (post.is_published) {
|
|
274
|
+
const notifiedCount = await broadcastNotification(post);
|
|
275
|
+
console.log(`[WhatsNew] Notified ${notifiedCount} users about new post`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return post;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error('[WhatsNew] Error creating post:', error);
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Update a post (admin only)
|
|
287
|
+
*/
|
|
288
|
+
async function updatePost(postId, { title, content, gifUrl, category, version, isPinned, isPublished }) {
|
|
289
|
+
try {
|
|
290
|
+
// First, check the current published status
|
|
291
|
+
const currentPost = await getPostById(postId);
|
|
292
|
+
const wasPublished = currentPost ? currentPost.is_published : false;
|
|
293
|
+
|
|
294
|
+
const query = `
|
|
295
|
+
UPDATE whats_new_posts
|
|
296
|
+
SET
|
|
297
|
+
title = COALESCE($1, title),
|
|
298
|
+
content = COALESCE($2, content),
|
|
299
|
+
gif_url = $3,
|
|
300
|
+
category = COALESCE($4, category),
|
|
301
|
+
version = $5,
|
|
302
|
+
is_pinned = COALESCE($6, is_pinned),
|
|
303
|
+
is_published = COALESCE($7, is_published)
|
|
304
|
+
WHERE id = $8
|
|
305
|
+
RETURNING *
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
const result = await pool.query(query, [
|
|
309
|
+
title,
|
|
310
|
+
content,
|
|
311
|
+
gifUrl,
|
|
312
|
+
category,
|
|
313
|
+
version,
|
|
314
|
+
isPinned,
|
|
315
|
+
isPublished,
|
|
316
|
+
postId,
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
if (result.rows.length === 0) {
|
|
320
|
+
throw new Error('Post not found');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const updatedPost = result.rows[0];
|
|
324
|
+
console.log('[WhatsNew] Updated post:', postId);
|
|
325
|
+
|
|
326
|
+
// If the post was just published (changed from unpublished to published), broadcast notifications
|
|
327
|
+
if (!wasPublished && updatedPost.is_published) {
|
|
328
|
+
console.log('[WhatsNew] Post was just published, broadcasting notifications...');
|
|
329
|
+
const notifiedCount = await broadcastNotification(updatedPost);
|
|
330
|
+
console.log(`[WhatsNew] Notified ${notifiedCount} users about newly published post`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return updatedPost;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error('[WhatsNew] Error updating post:', error);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Delete a post (admin only)
|
|
342
|
+
*/
|
|
343
|
+
async function deletePost(postId) {
|
|
344
|
+
try {
|
|
345
|
+
const query = `DELETE FROM whats_new_posts WHERE id = $1 RETURNING id`;
|
|
346
|
+
const result = await pool.query(query, [postId]);
|
|
347
|
+
|
|
348
|
+
if (result.rows.length === 0) {
|
|
349
|
+
throw new Error('Post not found');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log('[WhatsNew] Deleted post:', postId);
|
|
353
|
+
return true;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error('[WhatsNew] Error deleting post:', error);
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get a single post by ID
|
|
362
|
+
*/
|
|
363
|
+
async function getPostById(postId) {
|
|
364
|
+
try {
|
|
365
|
+
const query = `SELECT * FROM whats_new_posts WHERE id = $1`;
|
|
366
|
+
const result = await pool.query(query, [postId]);
|
|
367
|
+
return result.rows[0] || null;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error('[WhatsNew] Error fetching post:', error);
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
module.exports = {
|
|
375
|
+
ADMIN_WALLET,
|
|
376
|
+
isAdmin,
|
|
377
|
+
getPosts,
|
|
378
|
+
getAllPostsAdmin,
|
|
379
|
+
getUnreadCount,
|
|
380
|
+
markAsRead,
|
|
381
|
+
markAllAsRead,
|
|
382
|
+
createPost,
|
|
383
|
+
updatePost,
|
|
384
|
+
deletePost,
|
|
385
|
+
getPostById,
|
|
386
|
+
broadcastNotification,
|
|
387
|
+
};
|
|
388
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Helper Utilities
|
|
3
|
+
* Generates correct URLs for game sharing based on game type and environment
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the base app URL based on environment
|
|
8
|
+
* @returns {string} Base URL (e.g., 'https://dubs.app' or 'https://dev.dubs.app')
|
|
9
|
+
*/
|
|
10
|
+
function getAppBaseUrl() {
|
|
11
|
+
// Allow explicit override via environment variable
|
|
12
|
+
if (process.env.DUBS_APP_URL) {
|
|
13
|
+
return process.env.DUBS_APP_URL.replace(/\/$/, ''); // Remove trailing slash
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check NODE_ENV - production server has NODE_ENV=production
|
|
17
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
18
|
+
|
|
19
|
+
return isProduction ? 'https://dubs.app' : 'https://dev.dubs.app';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Determine if a game ID is a sports game
|
|
24
|
+
* Sports games have IDs starting with 'sport-'
|
|
25
|
+
* @param {string} gameId - The game ID
|
|
26
|
+
* @returns {boolean} True if sports game
|
|
27
|
+
*/
|
|
28
|
+
function isSportsGame(gameId) {
|
|
29
|
+
return gameId && gameId.startsWith('sport-');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Determine if a game ID is a pool/billiards game
|
|
34
|
+
* Pool games have UUID format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
|
|
35
|
+
* @param {string} gameId - The game ID
|
|
36
|
+
* @returns {boolean} True if pool game
|
|
37
|
+
*/
|
|
38
|
+
function isPoolGame(gameId) {
|
|
39
|
+
if (!gameId) return false;
|
|
40
|
+
// UUID pattern check
|
|
41
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
42
|
+
return uuidPattern.test(gameId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generate the correct web share link for a game
|
|
47
|
+
* - Sports games: /game/{gameId}
|
|
48
|
+
* - Pool games: /pool/{gameId}
|
|
49
|
+
* @param {string} gameId - The game ID
|
|
50
|
+
* @returns {string} Full web URL for the game
|
|
51
|
+
*/
|
|
52
|
+
function getGameShareUrl(gameId) {
|
|
53
|
+
const baseUrl = getAppBaseUrl();
|
|
54
|
+
|
|
55
|
+
if (isSportsGame(gameId)) {
|
|
56
|
+
return `${baseUrl}/game/${gameId}`;
|
|
57
|
+
} else if (isPoolGame(gameId)) {
|
|
58
|
+
return `${baseUrl}/pool/${gameId}`;
|
|
59
|
+
} else {
|
|
60
|
+
// Default to game path for unknown formats
|
|
61
|
+
return `${baseUrl}/game/${gameId}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate the claim URL for a game (used in winner notifications)
|
|
67
|
+
* @param {string} gameId - The game ID
|
|
68
|
+
* @returns {string} Full web URL for claiming winnings
|
|
69
|
+
*/
|
|
70
|
+
function getClaimUrl(gameId) {
|
|
71
|
+
const baseUrl = getAppBaseUrl();
|
|
72
|
+
return `${baseUrl}/claim/${gameId}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generate the What's New URL
|
|
77
|
+
* Just opens the What's New overlay (no specific post highlighting)
|
|
78
|
+
* @param {number} postId - Ignored (kept for compatibility)
|
|
79
|
+
* @returns {string} Full web URL for What's New
|
|
80
|
+
*/
|
|
81
|
+
function getWhatsNewUrl(postId) {
|
|
82
|
+
const baseUrl = getAppBaseUrl();
|
|
83
|
+
// Simple URL with flag to open What's New overlay
|
|
84
|
+
return `${baseUrl}/v2?openWhatsNew=true`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
getAppBaseUrl,
|
|
89
|
+
isSportsGame,
|
|
90
|
+
isPoolGame,
|
|
91
|
+
getGameShareUrl,
|
|
92
|
+
getClaimUrl,
|
|
93
|
+
getWhatsNewUrl
|
|
94
|
+
};
|
|
95
|
+
|