dubs-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +280 -0
- package/CLAUDE.md +46 -0
- package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
- package/CURRENT_SESSION.md +171 -0
- package/CURRENT_SESSION_DRAW.md +516 -0
- package/MARCH_MADNESS_SURVIVOR.md +254 -0
- package/PANDA.md +166 -0
- package/Procfile +4 -0
- package/README.md +476 -0
- package/controllers/livescoresController.js +376 -0
- package/controllers/pickemController.js +554 -0
- package/controllers/survivorAdminController.js +887 -0
- package/controllers/survivorController.js +623 -0
- package/cron/oracleMonitor.js +77 -0
- package/cron/pickemOracleMonitor.js +73 -0
- package/data/jackpot-history.json +952 -0
- package/data/ncaaTeams.js +406 -0
- package/documentation/API_SECURITY_GUIDE.md +327 -0
- package/documentation/ARCADE_API.md +593 -0
- package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
- package/documentation/ARCADE_QUICKSTART.md +242 -0
- package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
- package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
- package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
- package/documentation/CLAIM_STATUS_FIX.md +67 -0
- package/documentation/CLI_TOOL_GUIDE.md +372 -0
- package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
- package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
- package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
- package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
- package/documentation/DATABASE_QUERIES.md +269 -0
- package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
- package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
- package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
- package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
- package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
- package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
- package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
- package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
- package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
- package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
- package/documentation/HEROKU_DEPLOYMENT.md +134 -0
- package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
- package/documentation/JACKPOT_API.md +521 -0
- package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
- package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
- package/documentation/JWT_QUICK_SETUP.md +268 -0
- package/documentation/JWT_TESTING_GUIDE.md +404 -0
- package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
- package/documentation/KEEPER_SETUP.md +206 -0
- package/documentation/KEEPER_STATE_MACHINE.md +423 -0
- package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
- package/documentation/LOCAL_VOTING_TEST.md +279 -0
- package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
- package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
- package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
- package/documentation/PNL_TRACKING_SETUP.md +189 -0
- package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
- package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
- package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
- package/documentation/QUICK_AUTH_SETUP.md +99 -0
- package/documentation/QUICK_DEPLOY.md +224 -0
- package/documentation/QUICK_FIX.md +114 -0
- package/documentation/QUICK_START.md +152 -0
- package/documentation/REFEREE_MODE_GUIDE.md +392 -0
- package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
- package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
- package/documentation/RUN_MIGRATION_NOW.md +39 -0
- package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
- package/documentation/SETUP_GUIDE.md +184 -0
- package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
- package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
- package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
- package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
- package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
- package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
- package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
- package/ecosystem.config.js +65 -0
- package/env.template +125 -0
- package/middleware/apiKeyAuth.js +136 -0
- package/middleware/authenticate.js +214 -0
- package/middleware/developerUserAuth.js +76 -0
- package/middleware/socketAuth.js +69 -0
- package/package.json +49 -0
- package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
- package/postman/Dubs-API-v1.postman_collection.json +205 -0
- package/postman/Dubs_Developer_API.postman_collection.json +662 -0
- package/postman/QUICKSTART.md +118 -0
- package/postman/QUICK_REFERENCE.md +246 -0
- package/postman/README.md +71 -0
- package/postman/VOTING_API_GUIDE.md +426 -0
- package/refactor/Animations.md +148 -0
- package/refactor/Chat.md +252 -0
- package/routes/actionsRoutes.js +699 -0
- package/routes/adminRoutes.js +370 -0
- package/routes/analyticsRoutes.js +1262 -0
- package/routes/arcadeRoutes.js +557 -0
- package/routes/authRoutes.js +2310 -0
- package/routes/avatarRoutes.js +85 -0
- package/routes/botRoutes.js +211 -0
- package/routes/chatRoutes.js +377 -0
- package/routes/cryptoPriceRoutes.js +105 -0
- package/routes/developerRoutes.js +4201 -0
- package/routes/deviceRoutes.js +214 -0
- package/routes/dmRoutes.js +167 -0
- package/routes/esportsRoutes.js +806 -0
- package/routes/exchangeRateRoutes.js +233 -0
- package/routes/gamesRoutes.js +3028 -0
- package/routes/jackpotRoutes.js +754 -0
- package/routes/keeperMonitoringRoutes.js +156 -0
- package/routes/keeperWebhookRoutes.js +466 -0
- package/routes/livescoresRoutes.js +31 -0
- package/routes/pickemAdminRoutes.js +199 -0
- package/routes/pickemRoutes.js +231 -0
- package/routes/playerStatsRoutes.js +147 -0
- package/routes/portfolioRoutes.js +217 -0
- package/routes/promoRoutes.js +418 -0
- package/routes/referralEarningsRoutes.js +392 -0
- package/routes/socialRoutes.js +459 -0
- package/routes/sportsRoutes.js +1271 -0
- package/routes/survivorAdminRoutes.js +345 -0
- package/routes/survivorRoutes.js +756 -0
- package/routes/uploadRoutes.js +256 -0
- package/routes/userProfileRoutes.js +244 -0
- package/routes/whatsNewRoutes.js +331 -0
- package/scripts/.claude/settings.local.json +15 -0
- package/scripts/README.md +170 -0
- package/scripts/RESTART_EVERYTHING.sh +104 -0
- package/scripts/add-claim-columns.sql +48 -0
- package/scripts/add-crypto-prices-cache.sql +27 -0
- package/scripts/add-exchange-rates-cache.sql +40 -0
- package/scripts/add-game-invite-column.sql +23 -0
- package/scripts/add-game-invite-notification.sql +33 -0
- package/scripts/add-game-invite-telegram-pref.sql +16 -0
- package/scripts/add-game-joined-notification.sql +16 -0
- package/scripts/add-game-joined-pref.js +40 -0
- package/scripts/add-game-joined-preference.sql +6 -0
- package/scripts/add-game-start-notifications.sql +41 -0
- package/scripts/add-notification-flags-to-games.sql +55 -0
- package/scripts/add-pending-game-dismissals.sql +19 -0
- package/scripts/add-preferred-currency.sql +34 -0
- package/scripts/add-winner-columns.js +61 -0
- package/scripts/add_mention_system.sql +53 -0
- package/scripts/add_payment_system.sql +96 -0
- package/scripts/add_sports_event_id_column.sql +22 -0
- package/scripts/analyze-cohort-data-heroku.js +276 -0
- package/scripts/analyze-cohort-data.js +295 -0
- package/scripts/analyze-prod-cohorts.sh +10 -0
- package/scripts/backfill-matchup-images.js +245 -0
- package/scripts/backfill-missing-signatures.js +175 -0
- package/scripts/backfill-referral-earnings.js +202 -0
- package/scripts/check-chat-schema.js +130 -0
- package/scripts/check-db.sh +14 -0
- package/scripts/check_oracle_in_game.js +54 -0
- package/scripts/cleanup-database.js +193 -0
- package/scripts/clear-notification-cache.js +85 -0
- package/scripts/convert-mnemonic.js +50 -0
- package/scripts/create-users-table.sql +44 -0
- package/scripts/debug-cohort-counts.js +248 -0
- package/scripts/debug-winner-calc.js +84 -0
- package/scripts/deploy-payment-system.sh +118 -0
- package/scripts/deploy-to-heroku.sh +63 -0
- package/scripts/diagnose-locked-round.js +143 -0
- package/scripts/dubs-cli.js +720 -0
- package/scripts/dump-account.js +65 -0
- package/scripts/find-vrf-offset.js +48 -0
- package/scripts/fix-chat-notifications-constraint.sql +122 -0
- package/scripts/fix-claim-columns.js +124 -0
- package/scripts/fix-constraint-now.js +44 -0
- package/scripts/fix-lock-timestamps.js +96 -0
- package/scripts/fix-locked-round.sh +126 -0
- package/scripts/fix-missing-badges.sql +91 -0
- package/scripts/fix-payment-notifications.sql +41 -0
- package/scripts/force-new-round.js +55 -0
- package/scripts/force-resolve-and-claim.js +278 -0
- package/scripts/important/README.md +115 -0
- package/scripts/important/authority-force-lock.js +197 -0
- package/scripts/important/authority-resolve-game.js +267 -0
- package/scripts/important/check-game-status.js +373 -0
- package/scripts/important/list-pending-games-by-version.js +270 -0
- package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
- package/scripts/initialize-jackpot.js +111 -0
- package/scripts/jackpot/.claude/settings.local.json +10 -0
- package/scripts/jackpot/force-reset.js +84 -0
- package/scripts/jackpot/initialize-mainnet.js +100 -0
- package/scripts/jackpot/keeper.js +742 -0
- package/scripts/jackpot/status.js +107 -0
- package/scripts/jackpot/update-round-duration.js +143 -0
- package/scripts/keeper-bot.js +112 -0
- package/scripts/list-pending-games.js +131 -0
- package/scripts/migrate-chat-v2.js +127 -0
- package/scripts/migrate-chat-winners.js +84 -0
- package/scripts/migrate-chat.sh +17 -0
- package/scripts/migrate-game-invite.js +83 -0
- package/scripts/migrate-heroku-game-notifications.sh +159 -0
- package/scripts/migrations/001_analytics_tables.sql +422 -0
- package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
- package/scripts/migrations/003_referral_earnings.sql +208 -0
- package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
- package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
- package/scripts/migrations/005_push_notifications.sql +55 -0
- package/scripts/migrations/006_add_draw_team_players.sql +28 -0
- package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
- package/scripts/migrations/007_add_gif_url.sql +8 -0
- package/scripts/migrations/008_add_connect4_columns.sql +139 -0
- package/scripts/migrations/008_add_pool_tracking.sql +22 -0
- package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
- package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
- package/scripts/migrations/011_create_developer_tables.sql +67 -0
- package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
- package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
- package/scripts/migrations/013_add_network_mode.sql +18 -0
- package/scripts/migrations/014_create_developer_app_users.sql +19 -0
- package/scripts/migrations/015_add_ui_config.sql +4 -0
- package/scripts/migrations/016_add_resolution_secret.sql +4 -0
- package/scripts/migrations/017_add_external_game_id.sql +3 -0
- package/scripts/migrations/018_create_pickem_tables.sql +115 -0
- package/scripts/migrations/019_expo_push_tokens.sql +19 -0
- package/scripts/migrations/create_whats_new_tables.sql +88 -0
- package/scripts/migrations/drop_live_games_tables.sql +34 -0
- package/scripts/open-jackpot-round.js +85 -0
- package/scripts/purge-all-data.sh +329 -0
- package/scripts/purge-all-data.sql +142 -0
- package/scripts/purge-heroku-data.sh +149 -0
- package/scripts/purge-heroku-data.sql +62 -0
- package/scripts/rebuild-heroku-database.sh +113 -0
- package/scripts/recover-funds.js +357 -0
- package/scripts/regenerate-epl-images.js +278 -0
- package/scripts/resize-s3-matchup-images.js +374 -0
- package/scripts/resolve-direct.js +88 -0
- package/scripts/resolve-mock-game.js +124 -0
- package/scripts/resolve-pickem-game.js +55 -0
- package/scripts/resolve-round-manual.js +83 -0
- package/scripts/resolve-stuck-game.js +382 -0
- package/scripts/resolve-stuck-round.js +42 -0
- package/scripts/run-connect4-migration.sh +16 -0
- package/scripts/run-mention-migration.sh +32 -0
- package/scripts/run-payment-migration.sh +51 -0
- package/scripts/run-preferred-currency-migration.sh +31 -0
- package/scripts/run-referral-earnings-migration.sh +32 -0
- package/scripts/run-survivor-outcome-migration.sh +16 -0
- package/scripts/seed-test-users.js +346 -0
- package/scripts/setup-auth-tables.js +78 -0
- package/scripts/setup-complete-database.sql +992 -0
- package/scripts/setup-database-fresh.sh +359 -0
- package/scripts/setup-heroku-keeper.sh +48 -0
- package/scripts/setup-keeper-database.js +83 -0
- package/scripts/setup-keeper-state-db.sql +110 -0
- package/scripts/setup-oracle.sh +39 -0
- package/scripts/setup-pnl-tracking.js +111 -0
- package/scripts/start-devnet.sh +14 -0
- package/scripts/test-arcade-devnet.sh +160 -0
- package/scripts/test-arcade-match.sh +109 -0
- package/scripts/test-automatic-mode.sh +239 -0
- package/scripts/test-connect4-cancel-claim.js +370 -0
- package/scripts/test-connect4-e2e.js +369 -0
- package/scripts/test-connect4-resolve.js +369 -0
- package/scripts/test-game-state-endpoint.js +136 -0
- package/scripts/test-invite-notification.js +86 -0
- package/scripts/test-jackpot-api.sh +71 -0
- package/scripts/test-poll-confirmation.js +267 -0
- package/scripts/test-resolve-game.js +271 -0
- package/scripts/test-resolve-signature.js +223 -0
- package/scripts/test-signature-preservation.js +124 -0
- package/scripts/test-state-machine.js +291 -0
- package/scripts/test-webhook-receiver.js +60 -0
- package/scripts/update-notification-constraint.js +52 -0
- package/scripts/verify-account-layout.js +145 -0
- package/scripts/verify-winner-algorithm.js +278 -0
- package/server.js +5259 -0
- package/services/arcadeMatchService.js +763 -0
- package/services/automaticGameOracle.js +1596 -0
- package/services/chatService.js +1612 -0
- package/services/connect4GameService.js +1049 -0
- package/services/connect4NotificationService.js +374 -0
- package/services/cryptoPriceService.js +223 -0
- package/services/customGameResolver.js +260 -0
- package/services/db.js +79 -0
- package/services/directMessageService.js +389 -0
- package/services/discordNotifications.js +160 -0
- package/services/exchangeRateService.js +289 -0
- package/services/expoPushService.js +314 -0
- package/services/gamesCacheService.js +539 -0
- package/services/jackpotHistory.js +331 -0
- package/services/jackpotService.js +856 -0
- package/services/keeperStateService.js +355 -0
- package/services/matchupImageService.js +591 -0
- package/services/notificationCacheService.js +407 -0
- package/services/pickemOracle.js +440 -0
- package/services/playerStatsService.js +389 -0
- package/services/portfolioService.js +555 -0
- package/services/promoService.js +757 -0
- package/services/promoTreasuryService.js +239 -0
- package/services/pushNotifications.js +353 -0
- package/services/redisService.js +422 -0
- package/services/referralEarningsService.js +728 -0
- package/services/s3Service.js +396 -0
- package/services/socialService.js +1202 -0
- package/services/survivorOracle.js +469 -0
- package/services/survivorSimulator.js +475 -0
- package/services/telegramNotifications.js +461 -0
- package/services/userProfileStatsService.js +1185 -0
- package/services/whatsNewService.js +388 -0
- package/utils/urlHelper.js +95 -0
|
@@ -0,0 +1,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
|
+
};
|