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,389 @@
1
+ /**
2
+ * 💬 Direct Message Service
3
+ *
4
+ * Private 1:1 messaging between users
5
+ */
6
+
7
+ const { pool } = require('./db'); // Shared database pool
8
+ const { forwardChatNotification } = require('./telegramNotifications');
9
+
10
+ class DirectMessageService {
11
+ constructor() {
12
+ // Use shared pool from services/db.js
13
+ this.pool = pool;
14
+
15
+ this.initialized = false;
16
+ }
17
+
18
+ async initializeTable() {
19
+ try {
20
+ await this.pool.query(`
21
+ -- Direct messages table
22
+ CREATE TABLE IF NOT EXISTS direct_messages (
23
+ id SERIAL PRIMARY KEY,
24
+ sender_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL,
25
+ recipient_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL,
26
+ message TEXT NOT NULL,
27
+ metadata JSONB DEFAULT NULL,
28
+ read BOOLEAN DEFAULT FALSE,
29
+ deleted_by_sender BOOLEAN DEFAULT FALSE,
30
+ deleted_by_recipient BOOLEAN DEFAULT FALSE,
31
+ created_at TIMESTAMP DEFAULT NOW()
32
+ );
33
+
34
+ -- Indexes for fast queries
35
+ CREATE INDEX IF NOT EXISTS idx_dm_sender ON direct_messages(sender_id);
36
+ CREATE INDEX IF NOT EXISTS idx_dm_recipient ON direct_messages(recipient_id);
37
+ CREATE INDEX IF NOT EXISTS idx_dm_conversation ON direct_messages(
38
+ LEAST(sender_id, recipient_id),
39
+ GREATEST(sender_id, recipient_id)
40
+ );
41
+ CREATE INDEX IF NOT EXISTS idx_dm_created ON direct_messages(created_at DESC);
42
+ CREATE INDEX IF NOT EXISTS idx_dm_unread ON direct_messages(recipient_id, read) WHERE read = false;
43
+ `);
44
+
45
+ // Add metadata column if it doesn't exist (for existing tables)
46
+ await this.pool.query(`
47
+ ALTER TABLE direct_messages
48
+ ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT NULL
49
+ `).catch(() => {});
50
+
51
+ // NOTE: Notification type constraint is now managed by migration file:
52
+ // scripts/migrations/004_add_whats_new_notification_type.sql
53
+ // Do not update the constraint here - run the migration instead.
54
+
55
+ this.initialized = true;
56
+ console.log('✅ Direct messages table initialized');
57
+ } catch (error) {
58
+ console.error('❌ Failed to initialize DM table:', error.message);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Generate a unique conversation ID for two users (always sorted)
64
+ */
65
+ getConversationKey(userId1, userId2) {
66
+ return [Math.min(userId1, userId2), Math.max(userId1, userId2)].join('_');
67
+ }
68
+
69
+ /**
70
+ * Send a direct message
71
+ * @param {number} senderId - Sender user ID
72
+ * @param {number} recipientId - Recipient user ID
73
+ * @param {string} message - Message text
74
+ * @param {object} metadata - Optional metadata (gameInvite, etc.)
75
+ */
76
+ async sendMessage(senderId, recipientId, message, metadata = null) {
77
+ try {
78
+ // Validate message
79
+ const sanitized = message.trim().substring(0, 1000);
80
+ if (!sanitized || sanitized.length === 0) {
81
+ throw new Error('Message cannot be empty');
82
+ }
83
+
84
+ // Check if recipient has blocked sender
85
+ const blockCheck = await this.pool.query(
86
+ `SELECT 1 FROM user_relationships
87
+ WHERE user_id = $1 AND target_user_id = $2 AND relationship_type = 'block'`,
88
+ [recipientId, senderId]
89
+ );
90
+
91
+ if (blockCheck.rows.length > 0) {
92
+ throw new Error('Cannot send message to this user');
93
+ }
94
+
95
+ // Insert message with optional metadata
96
+ const result = await this.pool.query(
97
+ `INSERT INTO direct_messages (sender_id, recipient_id, message, metadata, created_at)
98
+ VALUES ($1, $2, $3, $4, NOW())
99
+ RETURNING *`,
100
+ [senderId, recipientId, sanitized, metadata ? JSON.stringify(metadata) : null]
101
+ );
102
+
103
+ const dm = result.rows[0];
104
+
105
+ // Get sender info for response
106
+ const senderInfo = await this.pool.query(
107
+ 'SELECT username, avatar, wallet_address FROM users WHERE id = $1',
108
+ [senderId]
109
+ );
110
+
111
+ const senderUsername = senderInfo.rows[0]?.username || 'Someone';
112
+
113
+ // Check if this is a game invite
114
+ const isGameInvite = metadata?.gameInvite != null;
115
+
116
+ if (isGameInvite) {
117
+ const gameInvite = metadata.gameInvite;
118
+ // Create game_invite notification for recipient
119
+ await this.pool.query(
120
+ `INSERT INTO chat_notifications (user_id, sender_user_id, notification_type, notification_data, created_at)
121
+ VALUES ($1, $2, 'game_invite', $3, NOW())`,
122
+ [recipientId, senderId, JSON.stringify({
123
+ messageId: dm.id,
124
+ gameInvite,
125
+ preview: sanitized.substring(0, 50)
126
+ })]
127
+ );
128
+
129
+ // Forward to Telegram with game invite details
130
+ const gameDetails = `🎱 *${gameInvite.title || 'Pool Room'}*\n💰 Buy-in: ${gameInvite.buyIn || 0.25} SOL`;
131
+ forwardChatNotification(this.pool, recipientId, 'game_invite', senderUsername, gameDetails)
132
+ .catch(err => console.error('[DM] Error forwarding game invite to Telegram:', err.message));
133
+
134
+ console.log(`🎮 Game invite notification sent from ${senderUsername} to user ${recipientId}`);
135
+ } else {
136
+ // Create standard dm_message notification
137
+ await this.pool.query(
138
+ `INSERT INTO chat_notifications (user_id, sender_user_id, notification_type, notification_data, created_at)
139
+ VALUES ($1, $2, 'dm_message', $3, NOW())`,
140
+ [recipientId, senderId, JSON.stringify({ messageId: dm.id, preview: sanitized.substring(0, 50) })]
141
+ );
142
+
143
+ // Forward to Telegram if connected
144
+ forwardChatNotification(this.pool, recipientId, 'dm_message', senderUsername, sanitized.substring(0, 100))
145
+ .catch(err => console.error('[DM] Error forwarding to Telegram:', err.message));
146
+ }
147
+
148
+ console.log(`💬 DM sent from ${senderId} to ${recipientId}${metadata?.gameInvite ? ' (with game invite)' : ''}`);
149
+
150
+ // Parse metadata if it was stored
151
+ const parsedMetadata = dm.metadata ? (typeof dm.metadata === 'string' ? JSON.parse(dm.metadata) : dm.metadata) : null;
152
+
153
+ return {
154
+ id: dm.id,
155
+ senderId: dm.sender_id,
156
+ recipientId: dm.recipient_id,
157
+ senderUsername: senderInfo.rows[0]?.username || 'Unknown',
158
+ senderAvatar: senderInfo.rows[0]?.avatar || null,
159
+ senderWallet: senderInfo.rows[0]?.wallet_address || '',
160
+ message: dm.message,
161
+ read: dm.read,
162
+ createdAt: dm.created_at,
163
+ isOwn: true,
164
+ gameInvite: parsedMetadata?.gameInvite || null,
165
+ };
166
+ } catch (error) {
167
+ console.error('[DM] Error sending message:', error);
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Get conversation history between two users
174
+ */
175
+ async getConversation(userId1, userId2, limit = 50, beforeId = null) {
176
+ try {
177
+ let query = `
178
+ SELECT
179
+ dm.*,
180
+ sender.username as sender_username,
181
+ sender.avatar as sender_avatar,
182
+ sender.wallet_address as sender_wallet
183
+ FROM direct_messages dm
184
+ JOIN users sender ON dm.sender_id = sender.id
185
+ WHERE (
186
+ (dm.sender_id = $1 AND dm.recipient_id = $2 AND dm.deleted_by_sender = false)
187
+ OR
188
+ (dm.sender_id = $2 AND dm.recipient_id = $1 AND dm.deleted_by_recipient = false)
189
+ )
190
+ `;
191
+
192
+ const params = [userId1, userId2];
193
+
194
+ if (beforeId) {
195
+ query += ` AND dm.id < $3`;
196
+ params.push(beforeId);
197
+ }
198
+
199
+ query += ` ORDER BY dm.created_at DESC LIMIT $${params.length + 1}`;
200
+ params.push(limit);
201
+
202
+ const result = await this.pool.query(query, params);
203
+
204
+ return result.rows.map(row => {
205
+ // Parse metadata if present
206
+ const metadata = row.metadata ? (typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata) : null;
207
+
208
+ return {
209
+ id: row.id,
210
+ senderId: row.sender_id,
211
+ recipientId: row.recipient_id,
212
+ senderUsername: row.sender_username,
213
+ senderAvatar: row.sender_avatar,
214
+ senderWallet: row.sender_wallet,
215
+ message: row.message,
216
+ read: row.read,
217
+ createdAt: row.created_at,
218
+ isOwn: parseInt(row.sender_id) === parseInt(userId1),
219
+ gameInvite: metadata?.gameInvite || null,
220
+ };
221
+ }).reverse(); // Reverse so oldest is first
222
+ } catch (error) {
223
+ console.error('[DM] Error getting conversation:', error);
224
+ return [];
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Get all conversations for a user (for inbox view)
230
+ */
231
+ async getConversations(userId, limit = 20) {
232
+ try {
233
+ // Get the latest message from each conversation
234
+ const result = await this.pool.query(
235
+ `WITH latest_messages AS (
236
+ SELECT DISTINCT ON (
237
+ LEAST(sender_id, recipient_id),
238
+ GREATEST(sender_id, recipient_id)
239
+ )
240
+ id,
241
+ sender_id,
242
+ recipient_id,
243
+ message,
244
+ read,
245
+ created_at,
246
+ CASE
247
+ WHEN sender_id = $1 THEN recipient_id
248
+ ELSE sender_id
249
+ END as other_user_id
250
+ FROM direct_messages
251
+ WHERE (sender_id = $1 AND deleted_by_sender = false)
252
+ OR (recipient_id = $1 AND deleted_by_recipient = false)
253
+ ORDER BY
254
+ LEAST(sender_id, recipient_id),
255
+ GREATEST(sender_id, recipient_id),
256
+ created_at DESC
257
+ )
258
+ SELECT
259
+ lm.*,
260
+ u.username as other_username,
261
+ u.avatar as other_avatar,
262
+ u.wallet_address as other_wallet,
263
+ (SELECT COUNT(*) FROM direct_messages
264
+ WHERE sender_id = lm.other_user_id
265
+ AND recipient_id = $1
266
+ AND read = false) as unread_count
267
+ FROM latest_messages lm
268
+ JOIN users u ON lm.other_user_id = u.id
269
+ ORDER BY lm.created_at DESC
270
+ LIMIT $2`,
271
+ [userId, limit]
272
+ );
273
+
274
+ return result.rows.map(row => ({
275
+ otherUserId: row.other_user_id,
276
+ otherUsername: row.other_username,
277
+ otherAvatar: row.other_avatar,
278
+ otherWallet: row.other_wallet,
279
+ lastMessage: {
280
+ id: row.id,
281
+ message: row.message,
282
+ senderId: row.sender_id,
283
+ createdAt: row.created_at,
284
+ isOwn: row.sender_id === userId,
285
+ },
286
+ unreadCount: parseInt(row.unread_count),
287
+ }));
288
+ } catch (error) {
289
+ console.error('[DM] Error getting conversations:', error);
290
+ return [];
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Mark messages as read
296
+ */
297
+ async markAsRead(userId, senderId) {
298
+ try {
299
+ const result = await this.pool.query(
300
+ `UPDATE direct_messages
301
+ SET read = true
302
+ WHERE recipient_id = $1 AND sender_id = $2 AND read = false
303
+ RETURNING id`,
304
+ [userId, senderId]
305
+ );
306
+
307
+ console.log(`[DM] Marked ${result.rows.length} messages as read`);
308
+ return result.rows.length;
309
+ } catch (error) {
310
+ console.error('[DM] Error marking as read:', error);
311
+ return 0;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Get unread DM count for a user
317
+ */
318
+ async getUnreadCount(userId) {
319
+ try {
320
+ const result = await this.pool.query(
321
+ `SELECT COUNT(DISTINCT sender_id) as count
322
+ FROM direct_messages
323
+ WHERE recipient_id = $1 AND read = false`,
324
+ [userId]
325
+ );
326
+
327
+ return parseInt(result.rows[0].count);
328
+ } catch (error) {
329
+ console.error('[DM] Error getting unread count:', error);
330
+ return 0;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Delete a conversation (soft delete for one user)
336
+ */
337
+ async deleteConversation(userId, otherUserId) {
338
+ try {
339
+ // Mark messages as deleted for this user
340
+ await this.pool.query(
341
+ `UPDATE direct_messages
342
+ SET deleted_by_sender = true
343
+ WHERE sender_id = $1 AND recipient_id = $2`,
344
+ [userId, otherUserId]
345
+ );
346
+
347
+ await this.pool.query(
348
+ `UPDATE direct_messages
349
+ SET deleted_by_recipient = true
350
+ WHERE sender_id = $2 AND recipient_id = $1`,
351
+ [userId, otherUserId]
352
+ );
353
+
354
+ console.log(`[DM] Conversation deleted for user ${userId}`);
355
+ return true;
356
+ } catch (error) {
357
+ console.error('[DM] Error deleting conversation:', error);
358
+ return false;
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Get user info by wallet address
364
+ */
365
+ async getUserByWallet(walletAddress) {
366
+ try {
367
+ const result = await this.pool.query(
368
+ 'SELECT id, username, avatar, wallet_address FROM users WHERE wallet_address = $1',
369
+ [walletAddress]
370
+ );
371
+
372
+ if (result.rows.length > 0) {
373
+ return {
374
+ id: result.rows[0].id,
375
+ username: result.rows[0].username,
376
+ avatar: result.rows[0].avatar,
377
+ walletAddress: result.rows[0].wallet_address,
378
+ };
379
+ }
380
+ return null;
381
+ } catch (error) {
382
+ console.error('[DM] Error getting user by wallet:', error);
383
+ return null;
384
+ }
385
+ }
386
+ }
387
+
388
+ module.exports = DirectMessageService;
389
+
@@ -0,0 +1,160 @@
1
+ // Discord Notification Service
2
+ // Posts new game announcements to a Discord channel via webhook
3
+
4
+ const axios = require('axios');
5
+ const urlHelper = require('../utils/urlHelper');
6
+
7
+ const DISCORD_NEW_GAME_WEBHOOK_URL = process.env.DISCORD_NEW_GAME_WEBHOOK_URL;
8
+
9
+ // Sport/league to emoji mapping
10
+ const SPORT_EMOJIS = {
11
+ // Leagues
12
+ 'NBA': '🏀',
13
+ 'NFL': '🏈',
14
+ 'NHL': '🏒',
15
+ 'MLB': '⚾',
16
+ 'MLS': '⚽',
17
+ 'EPL': '⚽',
18
+ 'English Premier League': '⚽',
19
+ 'Premier League': '⚽',
20
+ 'La Liga': '⚽',
21
+ 'Serie A': '⚽',
22
+ 'Bundesliga': '⚽',
23
+ 'UEFA Champions League': '⚽',
24
+ 'NCAA': '🏈',
25
+ // Sports (fallback)
26
+ 'Basketball': '🏀',
27
+ 'Football': '🏈',
28
+ 'Hockey': '🏒',
29
+ 'Baseball': '⚾',
30
+ 'Soccer': '⚽',
31
+ 'default': '🎮'
32
+ };
33
+
34
+ /**
35
+ * Get emoji for a sport/league
36
+ */
37
+ function getSportEmoji(sportsEvent) {
38
+ if (!sportsEvent) return SPORT_EMOJIS.default;
39
+
40
+ const league = sportsEvent.strLeague || '';
41
+ const sport = sportsEvent.strSport || '';
42
+
43
+ // Check league first
44
+ if (SPORT_EMOJIS[league]) return SPORT_EMOJIS[league];
45
+
46
+ // Check sport
47
+ if (SPORT_EMOJIS[sport]) return SPORT_EMOJIS[sport];
48
+
49
+ return SPORT_EMOJIS.default;
50
+ }
51
+
52
+ /**
53
+ * Post a new game announcement to Discord
54
+ * @param {object} gameData - The game data from the save endpoint
55
+ * @returns {Promise<boolean>} - Success status
56
+ */
57
+ async function notifyNewGame(gameData) {
58
+ if (!DISCORD_NEW_GAME_WEBHOOK_URL) {
59
+ console.log('[Discord] ⚠️ DISCORD_NEW_GAME_WEBHOOK_URL not set - skipping notification');
60
+ return false;
61
+ }
62
+
63
+ try {
64
+ const { gameId, title, buyIn, sportsEvent, creatorUsername, matchupImageUrl } = gameData;
65
+
66
+ // Extract team and league info
67
+ const homeTeam = sportsEvent?.strHomeTeam || 'TBD';
68
+ const awayTeam = sportsEvent?.strAwayTeam || 'TBD';
69
+ const league = sportsEvent?.strLeague || sportsEvent?.strSport || 'Sports';
70
+ // Use matchup image if provided, otherwise fall back to event thumbnail
71
+ const eventThumb = matchupImageUrl || sportsEvent?.strThumb || null;
72
+ const emoji = getSportEmoji(sportsEvent);
73
+
74
+ // Format lock time (betting closes 10 minutes before game start)
75
+ let lockTimeStr = 'TBD';
76
+ if (sportsEvent?.strTimestamp) {
77
+ const gameStartDate = new Date(sportsEvent.strTimestamp + 'Z');
78
+ const bettingClosesAt = new Date(gameStartDate.getTime() - 10 * 60 * 1000); // 10 minutes before
79
+ lockTimeStr = bettingClosesAt.toLocaleString('en-US', {
80
+ weekday: 'short',
81
+ month: 'short',
82
+ day: 'numeric',
83
+ hour: 'numeric',
84
+ minute: '2-digit',
85
+ timeZoneName: 'short'
86
+ });
87
+ }
88
+
89
+ // Get proper game URL using urlHelper
90
+ const gameUrl = urlHelper.getGameShareUrl(gameId);
91
+
92
+ // Build Discord embed
93
+ const embed = {
94
+ title: `${emoji} New Game Created!`,
95
+ description: `**${awayTeam}** @ **${homeTeam}**`,
96
+ color: 0x5865F2,
97
+ fields: [
98
+ {
99
+ name: '💰 Buy-in',
100
+ value: `${buyIn} SOL`,
101
+ inline: true
102
+ },
103
+ {
104
+ name: '🏆 League',
105
+ value: league,
106
+ inline: true
107
+ },
108
+ {
109
+ name: '🔒 Betting Closes',
110
+ value: lockTimeStr,
111
+ inline: true
112
+ },
113
+ {
114
+ name: '👤 Created By',
115
+ value: creatorUsername || 'Anonymous',
116
+ inline: true
117
+ },
118
+ {
119
+ name: '🔗 Join Game',
120
+ value: `[Click here to join](${gameUrl})`,
121
+ inline: false
122
+ }
123
+ ],
124
+ url: gameUrl,
125
+ footer: {
126
+ text: 'Dubs - Bet with friends on Solana'
127
+ },
128
+ timestamp: new Date().toISOString()
129
+ };
130
+
131
+ // Add event thumbnail if available
132
+ if (eventThumb) {
133
+ embed.image = { url: eventThumb };
134
+ }
135
+
136
+ const payload = { embeds: [embed] };
137
+
138
+ console.log(`[Discord] 🚀 Posting new game ${gameId} to Discord...`);
139
+ const response = await axios.post(DISCORD_NEW_GAME_WEBHOOK_URL, payload);
140
+
141
+ if (response.status === 204 || response.status === 200) {
142
+ console.log(`[Discord] ✅ Successfully posted game ${gameId} to Discord`);
143
+ return true;
144
+ }
145
+
146
+ console.log(`[Discord] ⚠️ Unexpected response status: ${response.status}`);
147
+ return false;
148
+ } catch (error) {
149
+ if (error.response) {
150
+ console.error(`[Discord] ❌ Webhook error (${error.response.status}):`, error.response.data);
151
+ } else {
152
+ console.error('[Discord] ❌ Failed to post to Discord:', error.message);
153
+ }
154
+ return false;
155
+ }
156
+ }
157
+
158
+ module.exports = {
159
+ notifyNewGame
160
+ };