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,461 @@
1
+ // 🔔 Telegram Notification Service for Oracle
2
+ // Sends notifications to Telegram users when games are resolved
3
+
4
+ const axios = require('axios');
5
+ const urlHelper = require('../utils/urlHelper');
6
+
7
+ const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
8
+ const TELEGRAM_API = `https://api.telegram.org/bot${BOT_TOKEN}`;
9
+
10
+ /**
11
+ * Escape special Markdown characters in user-generated text
12
+ * Prevents Telegram API parse errors from user messages containing *, _, `, [, etc.
13
+ */
14
+ function escapeMarkdown(text) {
15
+ if (!text) return '';
16
+ // Escape Markdown special characters: _ * ` [
17
+ return text
18
+ .replace(/\\/g, '\\\\') // Escape backslashes first
19
+ .replace(/_/g, '\\_')
20
+ .replace(/\*/g, '\\*')
21
+ .replace(/`/g, '\\`')
22
+ .replace(/\[/g, '\\[');
23
+ }
24
+
25
+ /**
26
+ * Send a Telegram message
27
+ */
28
+ async function sendTelegramMessage(chatId, message, options = {}) {
29
+ if (!BOT_TOKEN) {
30
+ console.log('[TelegramNotif] ⚠️ TELEGRAM_BOT_TOKEN not set - skipping notification');
31
+ return false;
32
+ }
33
+
34
+ try {
35
+ const payload = {
36
+ chat_id: chatId,
37
+ text: message,
38
+ parse_mode: 'Markdown',
39
+ disable_web_page_preview: true,
40
+ ...options
41
+ };
42
+
43
+ console.log(`[TelegramNotif] 🚀 Calling Telegram API for chat_id: ${chatId}`);
44
+ const response = await axios.post(`${TELEGRAM_API}/sendMessage`, payload);
45
+
46
+ if (response.data.ok) {
47
+ console.log(`[TelegramNotif] ✅ Telegram API returned OK for chat_id: ${chatId}`);
48
+ } else {
49
+ console.log(`[TelegramNotif] ⚠️ Telegram API returned not OK:`, response.data);
50
+ }
51
+
52
+ return response.data.ok;
53
+ } catch (error) {
54
+ // Log more details about the error
55
+ if (error.response) {
56
+ console.error(`[TelegramNotif] ❌ Telegram API error (${error.response.status}):`, error.response.data?.description || error.message);
57
+ } else {
58
+ console.error('[TelegramNotif] ❌ Failed to send Telegram message:', error.message);
59
+ }
60
+ return false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Notify user about game result (DEPRECATED - use notifyGroupGameResult instead)
66
+ */
67
+ async function notifyGameResult(telegramUserId, gameData, result) {
68
+ const { gameId, sportsEvent, participants } = gameData;
69
+ const { winner, homeScore, awayScore } = result;
70
+
71
+ // Find this user's participation
72
+ const userParticipant = participants?.find(p => p.telegramUserId === telegramUserId);
73
+
74
+ if (!userParticipant) {
75
+ return false; // User not in this game
76
+ }
77
+
78
+ const userTeam = userParticipant.teamChoice;
79
+ const didWin = userTeam === winner;
80
+
81
+ const emoji = didWin ? '🎉' : '😔';
82
+ const status = didWin ? '*YOU WON!*' : '*Game Over*';
83
+ const homeTeam = sportsEvent?.strHomeTeam || 'Home';
84
+ const awayTeam = sportsEvent?.strAwayTeam || 'Away';
85
+
86
+ const claimCommand = didWin ? `/claim ${gameId}` : '';
87
+
88
+ const message = `
89
+ ${emoji} *Game Result*
90
+
91
+ ${status}
92
+
93
+ *${awayTeam}* ${awayScore} - ${homeScore} *${homeTeam}*
94
+
95
+ ${didWin ? '🏆 You picked the winning team!' : '💪 Better luck next time!'}
96
+
97
+ Your team: ${userTeam === 'home' ? homeTeam : awayTeam}
98
+ Winner: ${winner === 'home' ? homeTeam : awayTeam}
99
+
100
+ ${didWin ? '💰 *Claim your winnings:*\n\`' + claimCommand + '\`\n\n_Tap to copy, then send as a message!_' : ''}
101
+ `.trim();
102
+
103
+ return await sendTelegramMessage(telegramUserId, message);
104
+ }
105
+
106
+ /**
107
+ * Notify group chat about game result with winners and losers
108
+ */
109
+ async function notifyGroupGameResult(gameData, result) {
110
+ const { gameId, sportsEvent, participants, telegramChatId } = gameData;
111
+ const { winner, homeScore, awayScore } = result;
112
+
113
+ if (!telegramChatId) {
114
+ console.log('⚠️ No telegramChatId found - cannot send group notification');
115
+ return false;
116
+ }
117
+
118
+ const homeTeam = sportsEvent?.strHomeTeam || 'Home';
119
+ const awayTeam = sportsEvent?.strAwayTeam || 'Away';
120
+
121
+ // Separate winners and losers
122
+ const winners = [];
123
+ const losers = [];
124
+
125
+ participants?.forEach(participant => {
126
+ const didWin = participant.teamChoice === winner;
127
+ const username = participant.username || 'User';
128
+
129
+ if (didWin) {
130
+ winners.push(username);
131
+ } else {
132
+ losers.push(username);
133
+ }
134
+ });
135
+
136
+ // Build the message
137
+ let message = `
138
+ 🏁 *GAME FINISHED!*
139
+
140
+ *${awayTeam}* ${awayScore} - ${homeScore} *${homeTeam}*
141
+
142
+ 🏆 *Winner:* ${winner === 'home' ? homeTeam : awayTeam}
143
+ `;
144
+
145
+ if (winners.length > 0) {
146
+ message += `\n\n🎉 *Winners:*\n`;
147
+ winners.forEach(username => {
148
+ message += `• ${username}\n`;
149
+ });
150
+ message += `\n💰 *Claim your winnings:*\n\`/claim ${gameId}\`\n\n_Tap to copy, then send as a message!_`;
151
+ }
152
+
153
+ if (losers.length > 0) {
154
+ message += `\n\n😔 *Better luck next time:*\n`;
155
+ losers.forEach(username => {
156
+ message += `• ${username}\n`;
157
+ });
158
+ }
159
+
160
+ message = message.trim();
161
+
162
+ return await sendTelegramMessage(telegramChatId, message);
163
+ }
164
+
165
+ /**
166
+ * Get Telegram user IDs from wallet addresses (requires mapping)
167
+ * For now, you'll need to store wallet -> telegramUserId mapping
168
+ */
169
+ async function getParticipantTelegramIds(gameData) {
170
+ // TODO: Implement wallet address -> Telegram ID mapping
171
+ // This could be done via:
172
+ // 1. Firebase lookup
173
+ // 2. Bot's userManager (needs to be accessible)
174
+ // 3. Separate database
175
+
176
+ // For now, return empty array
177
+ // You'll need to add this mapping when users create games
178
+ return [];
179
+ }
180
+
181
+ /**
182
+ * Notify group chat that game is starting soon (10 min warning)
183
+ */
184
+ async function notifyGameStartingSoon(gameData, minutesUntilStart) {
185
+ const { gameId, sportsEvent, participants, telegramChatId } = gameData;
186
+
187
+ if (!telegramChatId) {
188
+ console.log('⚠️ No telegramChatId found - cannot send group notification');
189
+ return false;
190
+ }
191
+
192
+ const homeTeam = sportsEvent?.strHomeTeam || 'Home';
193
+ const awayTeam = sportsEvent?.strAwayTeam || 'Away';
194
+
195
+ // Build the message
196
+ let message = `
197
+ ⏰ *GAME STARTING SOON!*
198
+
199
+ *${awayTeam}* @ *${homeTeam}*
200
+
201
+ 🔒 Betting closes in *${minutesUntilStart} minute${minutesUntilStart !== 1 ? 's' : ''}*!
202
+
203
+ `;
204
+
205
+ if (participants && participants.length > 0) {
206
+ message += `👥 *Current Bets:* ${participants.length} player${participants.length !== 1 ? 's' : ''}\n`;
207
+
208
+ const homeCount = participants.filter(p => p.teamChoice === 'home').length;
209
+ const awayCount = participants.filter(p => p.teamChoice === 'away').length;
210
+
211
+ if (homeCount > 0 || awayCount > 0) {
212
+ message += ` • ${homeTeam}: ${homeCount}\n`;
213
+ message += ` • ${awayTeam}: ${awayCount}\n`;
214
+ }
215
+ }
216
+
217
+ message += `\n⚡ Last chance to join: \`/join ${gameId}\``;
218
+ message = message.trim();
219
+
220
+ return await sendTelegramMessage(telegramChatId, message);
221
+ }
222
+
223
+ /**
224
+ * Notify group chat that game is starting NOW (betting closed)
225
+ */
226
+ async function notifyGameStartingNow(gameData) {
227
+ const { gameId, sportsEvent, participants, telegramChatId } = gameData;
228
+
229
+ if (!telegramChatId) {
230
+ console.log('⚠️ No telegramChatId found - cannot send group notification');
231
+ return false;
232
+ }
233
+
234
+ const homeTeam = sportsEvent?.strHomeTeam || 'Home';
235
+ const awayTeam = sportsEvent?.strAwayTeam || 'Away';
236
+
237
+ // Build the message
238
+ let message = `
239
+ 🚨 *GAME STARTING NOW!*
240
+
241
+ *${awayTeam}* @ *${homeTeam}*
242
+
243
+ 🔒 *Betting is now CLOSED*
244
+
245
+ `;
246
+
247
+ if (participants && participants.length > 0) {
248
+ const homeCount = participants.filter(p => p.teamChoice === 'home').length;
249
+ const awayCount = participants.filter(p => p.teamChoice === 'away').length;
250
+
251
+ message += `👥 *Final Bets:* ${participants.length} player${participants.length !== 1 ? 's' : ''}\n`;
252
+ message += ` 🏠 ${homeTeam}: ${homeCount}\n`;
253
+ message += ` ✈️ ${awayTeam}: ${awayCount}\n\n`;
254
+
255
+ message += `🎮 *Game is LIVE!* Good luck everyone! 🍀\n`;
256
+ message += `\n📊 Results will be posted automatically when the game finishes.`;
257
+ } else {
258
+ message += `_No bets placed on this game._`;
259
+ }
260
+
261
+ message = message.trim();
262
+
263
+ return await sendTelegramMessage(telegramChatId, message);
264
+ }
265
+
266
+ /**
267
+ * Forward chat notification to Telegram if user has connected their account
268
+ * @param {object} pool - Database pool
269
+ * @param {string} userId - User ID to send notification to
270
+ * @param {string} notificationType - Type of notification (game_invite, game_joined, etc.)
271
+ * @param {string} senderUsername - Username of the sender
272
+ * @param {string} message - Notification message text
273
+ * @param {object} metadata - Optional metadata (e.g., { gameId: 'sport-xxx' } for game invites)
274
+ */
275
+ async function forwardChatNotification(pool, userId, notificationType, senderUsername, message = '', metadata = {}) {
276
+ try {
277
+ console.log(`[TelegramNotif] 📤 Attempting to forward ${notificationType} notification to user ${userId} from @${senderUsername}`);
278
+
279
+ // Get user's telegram_user_id and preferences
280
+ const result = await pool.query(
281
+ `SELECT u.telegram_user_id, u.username,
282
+ COALESCE(p.notify_reply, true) as notify_reply,
283
+ COALESCE(p.notify_reaction, true) as notify_reaction,
284
+ COALESCE(p.notify_friend_request, true) as notify_friend_request,
285
+ COALESCE(p.notify_friend_request_accepted, true) as notify_friend_request_accepted,
286
+ COALESCE(p.notify_friend_request_declined, true) as notify_friend_request_declined,
287
+ COALESCE(p.notify_referral, true) as notify_referral,
288
+ COALESCE(p.notify_mention, true) as notify_mention,
289
+ COALESCE(p.notify_friend_message, true) as notify_friend_message,
290
+ COALESCE(p.notify_game_joined, true) as notify_game_joined,
291
+ COALESCE(p.notify_game_invite, true) as notify_game_invite
292
+ FROM users u
293
+ LEFT JOIN telegram_notification_preferences p ON u.id = p.user_id
294
+ WHERE u.id = $1 AND u.telegram_user_id IS NOT NULL`,
295
+ [userId]
296
+ );
297
+
298
+ if (result.rows.length === 0) {
299
+ console.log(`[TelegramNotif] ⏭️ User ${userId} doesn't have Telegram connected - skipping`);
300
+ return false; // User doesn't have Telegram connected
301
+ }
302
+
303
+ const prefs = result.rows[0];
304
+ const telegramUserId = prefs.telegram_user_id;
305
+ const recipientUsername = prefs.username;
306
+
307
+ console.log(`[TelegramNotif] 👤 Found user @${recipientUsername} with Telegram ID: ${telegramUserId}`);
308
+
309
+ // Check if user wants this notification type
310
+ const prefMap = {
311
+ 'reply': prefs.notify_reply,
312
+ 'reaction': prefs.notify_reaction,
313
+ 'friend_request': prefs.notify_friend_request,
314
+ 'friend_request_accepted': prefs.notify_friend_request_accepted,
315
+ 'friend_request_declined': prefs.notify_friend_request_declined,
316
+ 'referral': prefs.notify_referral,
317
+ 'mention': prefs.notify_mention,
318
+ 'friend_message': prefs.notify_friend_message,
319
+ 'game_joined': prefs.notify_game_joined,
320
+ 'game_invite': prefs.notify_game_invite,
321
+ 'dm_message': prefs.notify_friend_message, // Use friend_message pref for DMs
322
+ 'connect4_your_turn': prefs.notify_game_invite, // Use game_invite pref for Connect4 turn notifications
323
+ };
324
+
325
+ if (prefMap[notificationType] === false) {
326
+ console.log(`[TelegramNotif] 🔇 User @${recipientUsername} has disabled ${notificationType} notifications - skipping`);
327
+ return false; // User disabled this notification type
328
+ }
329
+
330
+ // Format notification message based on type
331
+ // Escape user-generated content to prevent Markdown parse errors
332
+ const safeSenderUsername = escapeMarkdown(senderUsername);
333
+ const safeMessage = escapeMarkdown(message);
334
+
335
+ let notificationText = '🔔 *Notification*\n\n';
336
+
337
+ switch (notificationType) {
338
+ case 'reply':
339
+ notificationText += `${safeSenderUsername} replied to your message`;
340
+ if (safeMessage) notificationText += `\n\n"${safeMessage}"`;
341
+ break;
342
+ case 'reaction':
343
+ notificationText += `${safeSenderUsername} reacted ${safeMessage} to your message`;
344
+ break;
345
+ case 'friend_request':
346
+ notificationText += `${safeSenderUsername} sent you a friend request`;
347
+ break;
348
+ case 'friend_request_accepted':
349
+ notificationText += `${safeSenderUsername} accepted your friend request!`;
350
+ break;
351
+ case 'friend_request_declined':
352
+ notificationText += `${safeSenderUsername} declined your friend request`;
353
+ break;
354
+ case 'referral':
355
+ notificationText += `🎉 ${safeSenderUsername} joined using your referral code!`;
356
+ break;
357
+ case 'game_joined':
358
+ notificationText += `🏆 ${safeSenderUsername} joined your bet!\n\n${safeMessage}`;
359
+ break;
360
+ case 'game_invite':
361
+ notificationText += `🎮 ${safeSenderUsername} invited you to join their bet!\n\n${safeMessage}`;
362
+ // CTA button will be added below if gameId is provided
363
+ break;
364
+ case 'mention':
365
+ notificationText += `${safeSenderUsername} mentioned you`;
366
+ if (safeMessage) notificationText += `\n\n"${safeMessage}"`;
367
+ break;
368
+ case 'friend_message':
369
+ notificationText += `${safeSenderUsername} sent you a message`;
370
+ if (safeMessage) notificationText += `\n\n"${safeMessage}"`;
371
+ break;
372
+ case 'dm_message':
373
+ notificationText += `💬 ${safeSenderUsername} sent you a direct message`;
374
+ if (safeMessage) notificationText += `\n\n"${safeMessage}"`;
375
+ break;
376
+ case 'game_won':
377
+ notificationText = `🏆 *You Won!*\n\n${safeMessage}`;
378
+ break;
379
+ case 'game_lost':
380
+ notificationText = `🎱 *Game Finished*\n\n${safeMessage}`;
381
+ break;
382
+ case 'game_starting_soon':
383
+ notificationText = `⏰ *Game Starting Soon!*\n\n${safeMessage}`;
384
+ break;
385
+ case 'game_starting_now':
386
+ notificationText = `🚨 *Game Starting NOW!*\n\n${safeMessage}`;
387
+ break;
388
+ case 'whats_new':
389
+ notificationText = `✨ *What's New on Dubs!*\n\n${safeMessage}`;
390
+ break;
391
+ case 'connect4_your_turn':
392
+ notificationText = `🔴🟡 *Your Turn!*\n\n${safeSenderUsername} made a move in 4 the Pot - it's your turn!`;
393
+ break;
394
+ default:
395
+ notificationText += `${safeSenderUsername} - ${notificationType}`;
396
+ }
397
+
398
+ console.log(`[TelegramNotif] 📨 Sending ${notificationType} notification to Telegram user ${telegramUserId}...`);
399
+
400
+ // Build message options (CTA buttons for certain notification types)
401
+ const messageOptions = {};
402
+
403
+ // Add CTA button for game-related notifications
404
+ if (metadata.gameId) {
405
+ const gameUrl = urlHelper.getGameShareUrl(metadata.gameId);
406
+ console.log(`[TelegramNotif] 🔗 Adding CTA button with URL: ${gameUrl}`);
407
+
408
+ // Different button text based on notification type
409
+ let buttonText = '🎮 View Game';
410
+ if (notificationType === 'game_invite') {
411
+ buttonText = '🎮 Join Game';
412
+ } else if (notificationType === 'game_joined') {
413
+ buttonText = '🎮 View Game';
414
+ } else if (notificationType === 'connect4_your_turn') {
415
+ buttonText = '🔴🟡 Make Your Move';
416
+ }
417
+
418
+ messageOptions.reply_markup = {
419
+ inline_keyboard: [[
420
+ { text: buttonText, url: gameUrl }
421
+ ]]
422
+ };
423
+ }
424
+
425
+ // Add CTA button for What's New notifications
426
+ if (notificationType === 'whats_new' && metadata.postId) {
427
+ const whatsNewUrl = urlHelper.getWhatsNewUrl(metadata.postId);
428
+ console.log(`[TelegramNotif] 🔗 Adding What's New CTA button with URL: ${whatsNewUrl}`);
429
+
430
+ messageOptions.reply_markup = {
431
+ inline_keyboard: [[
432
+ { text: '✨ See What\'s New', url: whatsNewUrl }
433
+ ]]
434
+ };
435
+ }
436
+
437
+ const sent = await sendTelegramMessage(telegramUserId, notificationText, messageOptions);
438
+
439
+ if (sent) {
440
+ console.log(`[TelegramNotif] ✅ Successfully sent ${notificationType} notification to @${recipientUsername}`);
441
+ } else {
442
+ console.log(`[TelegramNotif] ❌ Failed to send ${notificationType} notification to @${recipientUsername}`);
443
+ }
444
+
445
+ return sent;
446
+ } catch (error) {
447
+ console.error(`[TelegramNotif] ❌ Error forwarding ${notificationType} notification to Telegram:`, error.message);
448
+ return false;
449
+ }
450
+ }
451
+
452
+ module.exports = {
453
+ sendTelegramMessage,
454
+ notifyGameResult,
455
+ notifyGroupGameResult,
456
+ notifyGameStartingSoon,
457
+ notifyGameStartingNow,
458
+ getParticipantTelegramIds,
459
+ forwardChatNotification
460
+ };
461
+