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.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. 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
+