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,85 @@
1
+ /**
2
+ * Avatar Proxy Routes
3
+ * Caches external avatar images in Redis to avoid rate limiting
4
+ */
5
+
6
+ const express = require('express');
7
+ const router = express.Router();
8
+ const axios = require('axios');
9
+ const redisService = require('../services/redisService');
10
+
11
+ // Cache TTL: 24 hours
12
+ const AVATAR_CACHE_TTL = 60 * 60 * 24;
13
+
14
+ /**
15
+ * GET /api/avatar/proxy?url=<encoded_url>
16
+ * Proxies and caches external avatar images
17
+ */
18
+ router.get('/proxy', async (req, res) => {
19
+ try {
20
+ const { url } = req.query;
21
+
22
+ if (!url) {
23
+ return res.status(400).json({ error: 'URL parameter required' });
24
+ }
25
+
26
+ const decodedUrl = decodeURIComponent(url);
27
+
28
+ // Validate URL is from allowed domains
29
+ const allowedDomains = ['api.dicebear.com', 'avatars.dicebear.com'];
30
+ const urlObj = new URL(decodedUrl);
31
+ if (!allowedDomains.some(domain => urlObj.hostname.includes(domain))) {
32
+ return res.status(403).json({ error: 'Domain not allowed' });
33
+ }
34
+
35
+ // Create cache key from URL
36
+ const cacheKey = `avatar:${Buffer.from(decodedUrl).toString('base64').slice(0, 100)}`;
37
+
38
+ // Try to get from Redis cache
39
+ if (redisService.isAvailable()) {
40
+ const cached = await redisService.get(cacheKey);
41
+ if (cached) {
42
+ const data = JSON.parse(cached);
43
+ res.set('Content-Type', data.contentType);
44
+ res.set('Cache-Control', 'public, max-age=86400'); // Browser cache 24h
45
+ res.set('X-Cache', 'HIT');
46
+ return res.send(Buffer.from(data.image, 'base64'));
47
+ }
48
+ }
49
+
50
+ // Fetch from external service
51
+ const response = await axios.get(decodedUrl, {
52
+ responseType: 'arraybuffer',
53
+ timeout: 5000,
54
+ headers: {
55
+ 'User-Agent': 'Dubs-Avatar-Proxy/1.0',
56
+ },
57
+ });
58
+
59
+ const contentType = response.headers['content-type'] || 'image/svg+xml';
60
+ const imageBuffer = Buffer.from(response.data);
61
+
62
+ // Cache in Redis
63
+ if (redisService.isAvailable()) {
64
+ const cacheData = {
65
+ contentType,
66
+ image: imageBuffer.toString('base64'),
67
+ };
68
+ await redisService.set(cacheKey, JSON.stringify(cacheData), AVATAR_CACHE_TTL);
69
+ }
70
+
71
+ // Return image
72
+ res.set('Content-Type', contentType);
73
+ res.set('Cache-Control', 'public, max-age=86400');
74
+ res.set('X-Cache', 'MISS');
75
+ res.send(imageBuffer);
76
+
77
+ } catch (error) {
78
+ console.error('[Avatar Proxy] Error:', error.message);
79
+
80
+ // Return default avatar on error
81
+ res.redirect('/default_avatar.png');
82
+ }
83
+ });
84
+
85
+ module.exports = router;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Bot Routes - Server-to-server endpoints for bot integration
3
+ * These endpoints are called BY the Telegram bot, not by users
4
+ */
5
+
6
+ const express = require('express');
7
+ const router = express.Router();
8
+ const { pool } = require('../services/db'); // Shared database pool
9
+
10
+ // Bot secret for authentication
11
+ const BOT_SECRET = process.env.BOT_SECRET || 'dev-secret-change-in-prod';
12
+
13
+ /**
14
+ * Middleware: Verify request comes from bot
15
+ */
16
+ function verifyBot(req, res, next) {
17
+ const botSecret = req.headers['x-bot-secret'];
18
+
19
+ if (!botSecret || botSecret !== BOT_SECRET) {
20
+ console.error('[BotRoutes] Unauthorized bot request');
21
+ return res.status(401).json({
22
+ success: false,
23
+ error: 'Unauthorized'
24
+ });
25
+ }
26
+
27
+ next();
28
+ }
29
+
30
+ /**
31
+ * POST /api/bot/telegram-connection
32
+ * Called by bot when user completes /connect CODE
33
+ * Links Telegram account to wallet address
34
+ */
35
+ router.post('/telegram-connection', verifyBot, async (req, res) => {
36
+ try {
37
+ const {
38
+ walletAddress,
39
+ telegramUserId,
40
+ telegramUsername,
41
+ telegramFirstName,
42
+ telegramLastName,
43
+ telegramPhotoUrl,
44
+ } = req.body;
45
+
46
+ console.log('[BotRoutes] Telegram connection request:', {
47
+ walletAddress: walletAddress?.slice(0, 8) + '...',
48
+ telegramUserId,
49
+ telegramUsername,
50
+ });
51
+
52
+ // Validate required fields
53
+ if (!walletAddress || !telegramUserId) {
54
+ return res.status(400).json({
55
+ success: false,
56
+ error: 'Missing walletAddress or telegramUserId'
57
+ });
58
+ }
59
+
60
+ // Check if this Telegram account is already linked to another wallet
61
+ const existingLink = await pool.query(
62
+ 'SELECT wallet_address FROM users WHERE telegram_user_id = $1 AND wallet_address != $2',
63
+ [telegramUserId, walletAddress]
64
+ );
65
+
66
+ if (existingLink.rows.length > 0) {
67
+ console.error('[BotRoutes] Telegram already linked to another wallet');
68
+ return res.status(400).json({
69
+ success: false,
70
+ error: 'This Telegram account is already linked to another wallet'
71
+ });
72
+ }
73
+
74
+ // Update user with Telegram info
75
+ const result = await pool.query(
76
+ `UPDATE users
77
+ SET telegram_user_id = $1,
78
+ telegram_username = $2,
79
+ telegram_first_name = $3,
80
+ telegram_last_name = $4,
81
+ telegram_photo_url = $5,
82
+ telegram_connected_at = NOW(),
83
+ updated_at = NOW()
84
+ WHERE wallet_address = $6
85
+ RETURNING id, wallet_address, telegram_user_id, telegram_username,
86
+ telegram_first_name, telegram_last_name, telegram_photo_url,
87
+ telegram_connected_at`,
88
+ [
89
+ telegramUserId,
90
+ telegramUsername || null,
91
+ telegramFirstName || null,
92
+ telegramLastName || null,
93
+ telegramPhotoUrl || null,
94
+ walletAddress
95
+ ]
96
+ );
97
+
98
+ if (result.rows.length === 0) {
99
+ console.error('[BotRoutes] User not found:', walletAddress);
100
+ return res.status(404).json({
101
+ success: false,
102
+ error: 'User not found'
103
+ });
104
+ }
105
+
106
+ const user = result.rows[0];
107
+
108
+ // Create default notification preferences if not exist
109
+ await pool.query(
110
+ `INSERT INTO telegram_notification_preferences (user_id)
111
+ VALUES ($1)
112
+ ON CONFLICT (user_id) DO NOTHING`,
113
+ [user.id]
114
+ );
115
+
116
+ console.log('[BotRoutes] ✅ Telegram linked successfully:', {
117
+ userId: user.id,
118
+ wallet: walletAddress.slice(0, 8) + '...',
119
+ telegramId: telegramUserId,
120
+ });
121
+
122
+ res.json({
123
+ success: true,
124
+ message: 'Telegram account linked successfully',
125
+ user: {
126
+ id: user.id,
127
+ walletAddress: user.wallet_address,
128
+ telegram: {
129
+ userId: user.telegram_user_id,
130
+ username: user.telegram_username,
131
+ firstName: user.telegram_first_name,
132
+ lastName: user.telegram_last_name,
133
+ photoUrl: user.telegram_photo_url,
134
+ connectedAt: user.telegram_connected_at
135
+ }
136
+ }
137
+ });
138
+ } catch (error) {
139
+ console.error('[BotRoutes] Error linking Telegram:', error);
140
+ res.status(500).json({
141
+ success: false,
142
+ error: error.message
143
+ });
144
+ }
145
+ });
146
+
147
+ /**
148
+ * POST /api/bot/telegram-disconnection
149
+ * Called by bot when user wants to disconnect
150
+ */
151
+ router.post('/telegram-disconnection', verifyBot, async (req, res) => {
152
+ try {
153
+ const { telegramUserId } = req.body;
154
+
155
+ if (!telegramUserId) {
156
+ return res.status(400).json({
157
+ success: false,
158
+ error: 'Missing telegramUserId'
159
+ });
160
+ }
161
+
162
+ const result = await pool.query(
163
+ `UPDATE users
164
+ SET telegram_user_id = NULL,
165
+ telegram_username = NULL,
166
+ telegram_first_name = NULL,
167
+ telegram_last_name = NULL,
168
+ telegram_photo_url = NULL,
169
+ telegram_connected_at = NULL,
170
+ updated_at = NOW()
171
+ WHERE telegram_user_id = $1
172
+ RETURNING wallet_address`,
173
+ [telegramUserId]
174
+ );
175
+
176
+ if (result.rows.length === 0) {
177
+ return res.status(404).json({
178
+ success: false,
179
+ error: 'User not found'
180
+ });
181
+ }
182
+
183
+ console.log('[BotRoutes] ✅ Telegram disconnected for user:', result.rows[0].wallet_address);
184
+
185
+ res.json({
186
+ success: true,
187
+ message: 'Telegram account disconnected successfully'
188
+ });
189
+ } catch (error) {
190
+ console.error('[BotRoutes] Error disconnecting Telegram:', error);
191
+ res.status(500).json({
192
+ success: false,
193
+ error: error.message
194
+ });
195
+ }
196
+ });
197
+
198
+ /**
199
+ * GET /api/bot/health
200
+ * Health check for bot integration
201
+ */
202
+ router.get('/health', (req, res) => {
203
+ res.json({
204
+ success: true,
205
+ status: 'ok',
206
+ service: 'bot-routes'
207
+ });
208
+ });
209
+
210
+ module.exports = router;
211
+
@@ -0,0 +1,377 @@
1
+ /**
2
+ * 💬 Chat API Routes (v2 - Production Ready)
3
+ *
4
+ * JWT-authenticated chat with replies, notifications, and relationships
5
+ */
6
+
7
+ const express = require('express');
8
+ const router = express.Router();
9
+ const { authenticate, optionalAuth } = require('../middleware/authenticate');
10
+ const { pool } = require('../services/db'); // Shared database pool
11
+
12
+ module.exports = (chatService, io) => {
13
+
14
+ /**
15
+ * GET /chat/messages
16
+ * Get recent chat messages (optionally authenticated to filter blocked users)
17
+ */
18
+ router.get('/messages', optionalAuth, async (req, res) => {
19
+ try {
20
+ const limit = parseInt(req.query.limit) || 50;
21
+ const before = req.query.before ? parseInt(req.query.before) : null;
22
+ const userId = req.user?.userId || null;
23
+
24
+ const result = await chatService.getRecentMessages(limit, userId, before);
25
+
26
+ // Backwards compatible: if result is array (old behavior), wrap it
27
+ const messages = Array.isArray(result) ? result : result.messages;
28
+ const hasMore = Array.isArray(result) ? messages.length === limit : result.hasMore;
29
+ const oldestId = messages.length > 0 ? messages[0].id : null;
30
+
31
+ res.json({
32
+ success: true,
33
+ messages,
34
+ count: messages.length,
35
+ hasMore,
36
+ oldestId
37
+ });
38
+ } catch (error) {
39
+ console.error('Error fetching messages:', error);
40
+ res.status(500).json({
41
+ success: false,
42
+ error: error.message
43
+ });
44
+ }
45
+ });
46
+
47
+ /**
48
+ * POST /chat/message
49
+ * Post a new message (requires authentication)
50
+ */
51
+ router.post('/message', authenticate, async (req, res) => {
52
+ try {
53
+ const { message, replyToId } = req.body;
54
+ const { userId, walletAddress } = req.user;
55
+
56
+ if (!message) {
57
+ return res.status(400).json({
58
+ success: false,
59
+ error: 'Message is required'
60
+ });
61
+ }
62
+
63
+ // Rate limiting: Max 5 messages per minute
64
+ const recentCount = await chatService.getRecentMessageCount(userId, 1);
65
+ if (recentCount >= 5) {
66
+ return res.status(429).json({
67
+ success: false,
68
+ error: 'Woah, slow down there 🤣'
69
+ });
70
+ }
71
+
72
+ // Get user info from database (using shared pool)
73
+ const userResult = await pool.query(
74
+ 'SELECT username, avatar FROM users WHERE id = $1',
75
+ [userId]
76
+ );
77
+
78
+ if (userResult.rows.length === 0) {
79
+ return res.status(404).json({
80
+ success: false,
81
+ error: 'User not found'
82
+ });
83
+ }
84
+
85
+ const { username, avatar } = userResult.rows[0];
86
+
87
+ // Add message to database
88
+ const savedMessage = await chatService.addMessage(
89
+ userId,
90
+ walletAddress,
91
+ username,
92
+ avatar,
93
+ message,
94
+ { replyToId }
95
+ );
96
+
97
+ // Broadcast to chat namespace (WebSocket handled separately)
98
+ // The WebSocket handler will emit this event
99
+
100
+ res.json({
101
+ success: true,
102
+ message: savedMessage
103
+ });
104
+ } catch (error) {
105
+ console.error('Error posting message:', error);
106
+ res.status(500).json({
107
+ success: false,
108
+ error: error.message
109
+ });
110
+ }
111
+ });
112
+
113
+ /**
114
+ * DELETE /chat/message/:id
115
+ * Delete your own message
116
+ */
117
+ router.delete('/message/:id', authenticate, async (req, res) => {
118
+ try {
119
+ const messageId = parseInt(req.params.id);
120
+ const { userId } = req.user;
121
+
122
+ const deleted = await chatService.deleteMessage(messageId, userId);
123
+
124
+ if (!deleted) {
125
+ return res.status(404).json({
126
+ success: false,
127
+ error: 'Message not found or unauthorized'
128
+ });
129
+ }
130
+
131
+ res.json({
132
+ success: true,
133
+ messageId
134
+ });
135
+ } catch (error) {
136
+ console.error('Error deleting message:', error);
137
+ res.status(500).json({
138
+ success: false,
139
+ error: error.message
140
+ });
141
+ }
142
+ });
143
+
144
+ /**
145
+ * PUT /chat/message/:id
146
+ * Edit your own message
147
+ */
148
+ router.put('/message/:id', authenticate, async (req, res) => {
149
+ try {
150
+ const messageId = parseInt(req.params.id);
151
+ const { userId } = req.user;
152
+ const { message } = req.body;
153
+
154
+ if (!message) {
155
+ return res.status(400).json({
156
+ success: false,
157
+ error: 'Message is required'
158
+ });
159
+ }
160
+
161
+ const edited = await chatService.editMessage(messageId, userId, message);
162
+
163
+ if (!edited) {
164
+ return res.status(404).json({
165
+ success: false,
166
+ error: 'Message not found or unauthorized'
167
+ });
168
+ }
169
+
170
+ res.json({
171
+ success: true,
172
+ message: edited
173
+ });
174
+ } catch (error) {
175
+ console.error('Error editing message:', error);
176
+ res.status(500).json({
177
+ success: false,
178
+ error: error.message
179
+ });
180
+ }
181
+ });
182
+
183
+ /**
184
+ * GET /chat/notifications
185
+ * Get your notifications with cursor-based pagination
186
+ *
187
+ * Query params:
188
+ * - limit: Number of notifications per page (default: 10, max: 50)
189
+ * - cursor: Timestamp cursor for pagination (optional, for "load more")
190
+ *
191
+ * Response:
192
+ * - notifications: Array of notification objects
193
+ * - unreadCount: Total unread count
194
+ * - nextCursor: Cursor for next page (null if no more)
195
+ * - hasMore: Boolean indicating if more notifications exist
196
+ */
197
+ router.get('/notifications', authenticate, async (req, res) => {
198
+ try {
199
+ const { userId } = req.user;
200
+ const limit = Math.min(parseInt(req.query.limit) || 10, 50); // Max 50 per request
201
+ const cursor = req.query.cursor ? parseInt(req.query.cursor) : null;
202
+
203
+ // Fetch paginated notifications
204
+ const result = await chatService.getNotifications(userId, { limit, cursor });
205
+
206
+ // Fetch unread count (uses Redis cache when available)
207
+ const unreadCount = await chatService.getUnreadCount(userId);
208
+
209
+ res.json({
210
+ success: true,
211
+ notifications: result.notifications,
212
+ unreadCount,
213
+ nextCursor: result.nextCursor,
214
+ hasMore: result.hasMore,
215
+ });
216
+ } catch (error) {
217
+ console.error('Error fetching notifications:', error);
218
+ res.status(500).json({
219
+ success: false,
220
+ error: error.message
221
+ });
222
+ }
223
+ });
224
+
225
+ /**
226
+ * POST /chat/notifications/read
227
+ * Mark notifications as read
228
+ */
229
+ router.post('/notifications/read', authenticate, async (req, res) => {
230
+ try {
231
+ const { userId } = req.user;
232
+ const { notificationIds } = req.body;
233
+
234
+ if (!Array.isArray(notificationIds)) {
235
+ return res.status(400).json({
236
+ success: false,
237
+ error: 'notificationIds must be an array'
238
+ });
239
+ }
240
+
241
+ await chatService.markNotificationsRead(userId, notificationIds);
242
+
243
+ res.json({
244
+ success: true
245
+ });
246
+ } catch (error) {
247
+ console.error('Error marking notifications read:', error);
248
+ res.status(500).json({
249
+ success: false,
250
+ error: error.message
251
+ });
252
+ }
253
+ });
254
+
255
+ /**
256
+ * POST /chat/block/:targetUserId
257
+ * Block a user
258
+ */
259
+ router.post('/block/:targetUserId', authenticate, async (req, res) => {
260
+ try {
261
+ const { userId } = req.user;
262
+ const targetUserId = parseInt(req.params.targetUserId);
263
+
264
+ if (userId === targetUserId) {
265
+ return res.status(400).json({
266
+ success: false,
267
+ error: 'Cannot block yourself'
268
+ });
269
+ }
270
+
271
+ await chatService.blockUser(userId, targetUserId);
272
+
273
+ res.json({
274
+ success: true
275
+ });
276
+ } catch (error) {
277
+ console.error('Error blocking user:', error);
278
+ res.status(500).json({
279
+ success: false,
280
+ error: error.message
281
+ });
282
+ }
283
+ });
284
+
285
+ /**
286
+ * DELETE /chat/block/:targetUserId
287
+ * Unblock a user
288
+ */
289
+ router.delete('/block/:targetUserId', authenticate, async (req, res) => {
290
+ try {
291
+ const { userId } = req.user;
292
+ const targetUserId = parseInt(req.params.targetUserId);
293
+
294
+ await chatService.unblockUser(userId, targetUserId);
295
+
296
+ res.json({
297
+ success: true
298
+ });
299
+ } catch (error) {
300
+ console.error('Error unblocking user:', error);
301
+ res.status(500).json({
302
+ success: false,
303
+ error: error.message
304
+ });
305
+ }
306
+ });
307
+
308
+ /**
309
+ * DELETE /chat/cleanup
310
+ * Cleanup old messages (protected - requires authentication)
311
+ * Note: Better to run as cron job instead of exposing as endpoint
312
+ */
313
+ router.delete('/cleanup', authenticate, async (req, res) => {
314
+ try {
315
+ // Optional: Add admin check here if you want only admins to cleanup
316
+ // if (!req.user.isAdmin) {
317
+ // return res.status(403).json({ error: 'Admin only' });
318
+ // }
319
+
320
+ await chatService.cleanup();
321
+ res.json({
322
+ success: true,
323
+ message: 'Old messages cleaned up'
324
+ });
325
+ } catch (error) {
326
+ console.error('Error cleaning up:', error);
327
+ res.status(500).json({
328
+ success: false,
329
+ error: error.message
330
+ });
331
+ }
332
+ });
333
+
334
+ /**
335
+ * GET /chat/payments/user/:walletAddress
336
+ * Get all payment transactions for a user (sent and received)
337
+ * Used by transaction history to show chat payment context
338
+ */
339
+ router.get('/payments/user/:walletAddress', async (req, res) => {
340
+ try {
341
+ const { walletAddress } = req.params;
342
+
343
+ // Fetch payments where user is sender OR recipient (using shared pool)
344
+ const result = await pool.query(`
345
+ SELECT
346
+ cp.*,
347
+ sender.username as sender_username,
348
+ sender.avatar as sender_avatar,
349
+ recipient.username as recipient_username,
350
+ recipient.avatar as recipient_avatar
351
+ FROM chat_payments cp
352
+ LEFT JOIN users sender ON cp.sender_user_id = sender.id
353
+ LEFT JOIN users recipient ON cp.recipient_user_id = recipient.id
354
+ WHERE cp.sender_wallet = $1 OR cp.recipient_wallet = $1
355
+ ORDER BY cp.created_at DESC
356
+ LIMIT 100
357
+ `, [walletAddress]);
358
+
359
+ console.log(`[chat/payments/user] Found ${result.rows.length} payments for ${walletAddress.slice(0, 8)}...`);
360
+
361
+ res.status(200).json({
362
+ success: true,
363
+ payments: result.rows
364
+ });
365
+
366
+ } catch (error) {
367
+ console.error('[chat/payments/user] Error:', error);
368
+ res.status(500).json({
369
+ success: false,
370
+ error: error.message
371
+ });
372
+ }
373
+ });
374
+
375
+ return router;
376
+ };
377
+