hansaka01 commited on
Commit
871deff
·
verified ·
1 Parent(s): 347b0af

Upload 50 files

Browse files
Files changed (4) hide show
  1. .dockerignore +10 -9
  2. api.js +109 -269
  3. bot.js +2 -7
  4. scripts/downloadGifs.js +26 -105
.dockerignore CHANGED
@@ -1,9 +1,10 @@
1
- node_modules
2
- .env
3
- .git
4
- .gitignore
5
- *.md
6
- LICENSE
7
- Dockerfile
8
- .dockerignore
9
- npm-debug.log
 
 
1
+ node_modules
2
+ .env
3
+ .git
4
+ .gitignore
5
+ *.md
6
+ LICENSE
7
+ Dockerfile
8
+ .dockerignore
9
+ npm-debug.log
10
+ gifs
api.js CHANGED
@@ -6,92 +6,64 @@ const User = require('./models/User');
6
  const app = express();
7
  app.use(express.json());
8
 
9
- // ── CORS — MUST be before static files and routes ──
10
- const WEBAPP_ORIGIN = process.env.WEBAPP_ORIGIN || '*';
11
  app.use((req, res, next) => {
12
- res.setHeader('Access-Control-Allow-Origin', WEBAPP_ORIGIN);
13
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Init-Data');
14
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
15
  if (req.method === 'OPTIONS') return res.sendStatus(204);
16
  next();
17
  });
18
 
19
- // ── Serve Static WebApp ──
20
  app.use(express.static(path.join(__dirname, 'webapp')));
21
-
22
- // ── Explicit routes for game pages (fallback if static doesn't catch) ──
23
  app.get('/carrom', (req, res) => res.sendFile(path.join(__dirname, 'webapp', 'carrom.html')));
24
  app.get('/cards', (req, res) => res.sendFile(path.join(__dirname, 'webapp', 'cards.html')));
25
 
26
- // ── Telegram initData verification ──
27
- function verifyInitData(initDataRaw) {
 
 
 
 
 
 
 
 
28
  try {
29
- const params = new URLSearchParams(initDataRaw);
30
- const hash = params.get('hash');
31
- if (!hash) return null;
32
  params.delete('hash');
33
- const checkString = [...params.entries()]
34
- .sort(([a], [b]) => a.localeCompare(b))
35
- .map(([k, v]) => `${k}=${v}`)
36
- .join('\n');
37
- const secret = crypto
38
- .createHmac('sha256', 'WebAppData')
39
- .update(process.env.BOT_TOKEN)
40
- .digest();
41
- const expectedHash = crypto
42
- .createHmac('sha256', secret)
43
- .update(checkString)
44
- .digest('hex');
45
- if (expectedHash !== hash) return null;
46
- const userStr = params.get('user');
47
- return userStr ? JSON.parse(userStr) : null;
48
  } catch { return null; }
49
  }
50
 
51
- // ── Auth middleware ──
52
  function auth(req, res, next) {
53
- const initDataRaw = req.headers['x-init-data'] || req.query.initData;
54
- if (!initDataRaw) {
55
- console.log('[API] Auth fail: Missing initData on', req.method, req.path);
56
- return res.status(401).json({ ok: false, error: 'Missing initData' });
57
- }
58
- const tgUser = verifyInitData(initDataRaw);
59
- if (!tgUser) {
60
- console.log('[API] Auth fail: Invalid signature on', req.method, req.path);
61
- return res.status(403).json({ ok: false, error: 'Invalid initData' });
62
- }
63
- req.tgUser = tgUser;
64
- next();
65
  }
66
 
67
  // ── GET /status ──
68
  app.get('/status', auth, async (req, res) => {
69
  try {
70
- const { type } = req.query;
71
  const userId = req.tgUser.id.toString();
72
  const user = await User.findOne({ userId });
73
- const now = new Date();
74
- const todayStr = now.toISOString().split('T')[0];
75
-
76
  if (!user) return res.json({ canClaim: true, hoursLeft: null, adsLeft: 50 });
77
-
78
- if (type === 'mission_inpage') {
79
- const lastAdDate = user.lastAdWatchDate ? user.lastAdWatchDate.toISOString().split('T')[0] : null;
80
- let currentCount = user.dailyAdsCount || 0;
81
- if (lastAdDate !== todayStr) currentCount = 0;
82
- const adsLeft = Math.max(0, 50 - currentCount);
83
- return res.json({ canClaim: adsLeft > 0, hoursLeft: adsLeft > 0 ? null : 24, adsLeft });
84
  }
85
-
86
- const lastClaim = user.lastDaily;
87
- if (!lastClaim) return res.json({ canClaim: true, hoursLeft: null });
88
- const lastStr = lastClaim.toISOString().split('T')[0];
89
- if (lastStr !== todayStr) return res.json({ canClaim: true, hoursLeft: null });
90
- return res.json({ canClaim: false, hoursLeft: 24 });
91
- } catch (err) {
92
- console.error('[API] /status error:', err);
93
- res.status(500).json({ error: 'Server error' });
94
- }
95
  });
96
 
97
  // ── POST /claim ──
@@ -99,62 +71,40 @@ app.post('/claim', auth, async (req, res) => {
99
  try {
100
  const { type } = req.body;
101
  const userId = req.tgUser.id.toString();
102
- let user = await User.findOne({ userId });
103
- if (!user) user = await User.create({ userId, username: req.tgUser.username });
104
- const now = new Date();
105
- const todayStr = now.toISOString().split('T')[0];
106
-
107
  if (type === 'daily') {
108
- const lastStr = user.lastDaily ? user.lastDaily.toISOString().split('T')[0] : null;
109
- if (lastStr === todayStr) return res.json({ ok: false, error: 'Already claimed today' });
110
- const reward = 3000, xpGain = 100;
111
- user.wallet += reward; user.xp = (user.xp || 0) + xpGain; user.lastDaily = now;
112
- await user.save();
113
- console.log(`[API] daily reward: ${userId} +$${reward}`);
114
- if (global._tgClient) {
115
- try { await global._tgClient.sendMessage(userId, { message: `🎉 <b>Daily Reward Claimed!</b>\n\n💰 <b>+$${reward}</b>\n✨ <b>+${xpGain} XP</b>\n\nCome back tomorrow! 🌟` }); } catch (e) {}
116
- }
117
- return res.json({ ok: true, reward, xp: xpGain, wallet: user.wallet });
118
  } else if (type === 'mission') {
119
- const lastAdDate = user.lastAdWatchDate ? user.lastAdWatchDate.toISOString().split('T')[0] : null;
120
- if (lastAdDate !== todayStr) user.dailyAdsCount = 0;
121
- if (user.dailyAdsCount >= 50) return res.json({ ok: false, error: 'Daily limit reached (50)' });
122
- const reward = 200;
123
- user.wallet += reward; user.dailyAdsCount += 1;
124
- user.lastAdWatchDate = now; user.lastInpageMissionClaim = now; user.lastMissionClaim = now;
125
  await user.save();
126
- const adsLeft = 50 - user.dailyAdsCount;
127
- if (global._tgClient) {
128
- try { await global._tgClient.sendMessage(userId, { message: `🎯 <b>Mission Complete!</b>\n\nYou've earned <b>$200</b>! (${adsLeft} left today) 🎖️` }); } catch (e) {}
129
- }
130
- return res.json({ ok: true, reward, adsLeft, wallet: user.wallet });
131
  }
132
- return res.status(400).json({ ok: false, error: 'Invalid type' });
133
- } catch (err) {
134
- console.error('[API] /claim error:', err);
135
- res.status(500).json({ ok: false, error: 'Server error' });
136
- }
137
  });
138
 
139
- // ── GET /leaderboard ──
140
  app.get('/leaderboard', auth, async (req, res) => {
141
  try {
142
  const { type = 'wallet', period = 'all' } = req.query;
143
- const sortFields = { bank: { bank: -1 }, wallet: { wallet: -1 }, xp: { xp: -1 }, kills: { kills: -1 }, robs: { robs: -1 } };
144
- const sort = sortFields[type] || { wallet: -1 };
145
- let dateFilter = {};
146
  const now = new Date();
147
- if (period === 'daily') dateFilter = { lastDaily: { $gte: new Date(now.toISOString().split('T')[0]) } };
148
- else if (period === 'weekly') dateFilter = { lastDaily: { $gte: new Date(now.getTime() - 7 * 86400000) } };
149
- else if (period === 'monthly') dateFilter = { lastDaily: { $gte: new Date(now.getTime() - 30 * 86400000) } };
150
- const users = await User.find(dateFilter).sort(sort).limit(50).select('userId username wallet bank xp level kills robs totalEarned').lean();
151
- if (global._tgClient) {
152
- for (const u of users) {
153
- if (!u.username) { try { const ent = await global._tgClient.getEntity(u.userId); u.username = ent.username || ent.firstName || `User${u.userId.slice(-4)}`; } catch { u.username = `User${u.userId.slice(-4)}`; } }
154
- }
155
- } else { for (const u of users) if (!u.username) u.username = `User${u.userId.slice(-4)}`; }
156
  res.json({ ok: true, leaderboard: users, type, period });
157
- } catch (err) { console.error('[API] /leaderboard error:', err); res.status(500).json({ error: 'Server error' }); }
158
  });
159
 
160
  // ── GET /me ──
@@ -162,234 +112,124 @@ app.get('/me', auth, async (req, res) => {
162
  try {
163
  const userId = req.tgUser.id.toString();
164
  let user = await User.findOne({ userId }).lean();
165
- if (!user) { user = await User.create({ userId, username: req.tgUser.username }); user = user.toObject(); }
166
  res.json({ ok: true, user });
167
- } catch (err) { console.error('[API] /me error:', err); res.status(500).json({ error: 'Server error' }); }
168
  });
169
 
170
- // ── POST /carrom/start — Host starts game from Mini App ──
171
  app.post('/carrom/start', auth, async (req, res) => {
172
  try {
173
  const { gameId } = req.body;
174
- if (!gameId) return res.json({ ok: false, error: 'Missing gameId' });
175
  const sessions = require('./games/sessions');
176
  const game = sessions.get(gameId);
177
- if (!game) return res.json({ ok: false, error: 'Game not found' });
178
- if (game.status !== 'lobby') return res.json({ ok: false, error: 'Game already started' });
179
- const userId = req.tgUser.id.toString();
180
- if (game.hostId !== userId) return res.json({ ok: false, error: 'Only host can start' });
181
- if (game.players.length < 2) return res.json({ ok: false, error: 'Need at least 2 players' });
182
-
183
- // Kick 3rd player in 4-player mode
184
- if (game.maxPlayers === 4 && game.players.length === 3) {
185
- const kicked = game.players.pop(); game.scores.pop();
186
- if (global._tgClient && game.chatId) {
187
- try { await global._tgClient.sendMessage(game.chatId, { message: `😔 Sorry <b>${kicked.name}</b>, need 2 or 4 players. Join next round!` }); } catch (e) {}
188
- }
189
- }
190
-
191
- if (game._timer) { clearTimeout(game._timer); game._timer = null; }
192
- if (game._autoStart) { clearTimeout(game._autoStart); game._autoStart = null; }
193
-
194
- // Charge all
195
- for (const p of game.players) {
196
- let u = await User.findOne({ userId: p.userId }) || await User.create({ userId: p.userId });
197
- u.wallet -= game.bet; await u.save();
198
- }
199
-
200
  game.start();
201
-
202
- if (global._tgClient && game.chatId) {
203
- const { getBotUsername } = require('./utils/miniAppButton');
204
- const playUrl = `https://t.me/${getBotUsername()}?startapp=carrom_${gameId.replace(/:/g, '_0_')}`;
205
- const pot = game.bet * game.players.length;
206
- const pl = game.players.map((p, i) => `${i + 1}. ${p.name}`).join('\n');
207
- try {
208
- await global._tgClient.sendMessage(game.chatId, {
209
- message: `🎯 <b>CARROM POOL — GAME ON!</b>\nPot: <b>$${pot}</b>\n\n${pl}`,
210
- buttons: new (require('telegram').Api).ReplyInlineMarkup({ rows: [
211
- new (require('telegram').Api).KeyboardButtonRow({ buttons: [
212
- new (require('telegram').Api).KeyboardButtonUrl({ text: "🎮 Play / Watch", url: playUrl })
213
- ] })
214
- ] })
215
- });
216
- } catch (e) {}
217
- }
218
-
219
- res.json({ ok: true, state: game.getState(userId) });
220
- } catch (err) {
221
- console.error('[API] /carrom/start error:', err);
222
- res.status(500).json({ ok: false, error: 'Server error' });
223
- }
224
  });
225
 
226
- // ── GET /carrom/state ──
227
  app.get('/carrom/state', auth, async (req, res) => {
228
  try {
229
- const { gameId } = req.query;
230
- if (!gameId) return res.json({ ok: false, error: 'Missing gameId' });
231
  const sessions = require('./games/sessions');
232
- const game = sessions.get(gameId);
233
- if (!game || !game.getState) {
234
- console.log('[API] /carrom/state: Game not found:', gameId);
235
- return res.json({ ok: false, error: 'Game not found' });
236
- }
237
- const userId = req.tgUser.id.toString();
238
- res.json({ ok: true, state: game.getState(userId) });
239
- } catch (err) {
240
- console.error('[API] /carrom/state error:', err);
241
- res.status(500).json({ ok: false, error: 'Server error' });
242
- }
243
  });
244
 
245
- // ── POST /carrom/move — Move striker along baseline before shooting ──
246
  app.post('/carrom/move', auth, async (req, res) => {
247
  try {
248
- const { gameId, x } = req.body;
249
- if (!gameId) return res.json({ ok: false, error: 'Missing gameId' });
250
  const sessions = require('./games/sessions');
251
- const game = sessions.get(gameId);
252
- if (!game || !game.moveStriker) return res.json({ ok: false, error: 'Game not found' });
253
- const userId = req.tgUser.id.toString();
254
- const result = game.moveStriker(userId, x);
255
- if (result.error) return res.json({ ok: false, error: result.error });
256
- res.json({ ok: true });
257
- } catch (err) {
258
- res.status(500).json({ ok: false, error: 'Server error' });
259
- }
260
  });
261
 
262
- // ── POST /carrom/shoot ──
263
  app.post('/carrom/shoot', auth, async (req, res) => {
264
  try {
265
- const { gameId, vx, vy } = req.body;
266
- if (!gameId) return res.json({ ok: false, error: 'Missing gameId' });
267
  const sessions = require('./games/sessions');
268
  const leveling = require('./utils/leveling');
269
- const game = sessions.get(gameId);
270
- if (!game || !game.shoot) {
271
- console.log('[API] /carrom/shoot: Game not found:', gameId);
272
- return res.json({ ok: false, error: 'Game not found' });
273
- }
274
- const userId = req.tgUser.id.toString();
275
- const result = game.shoot(userId, vx, vy);
276
- if (result.error) return res.json({ ok: false, error: result.error });
277
-
278
  if (game.status === 'ended') {
279
  const winners = game.getWinnerPlayers();
280
  const pot = game.bet * game.players.length;
281
  const share = Math.floor(pot / winners.length);
282
- for (const wp of winners) {
283
- let w = await User.findOne({ userId: wp.userId }) || await User.create({ userId: wp.userId });
284
- w.wallet += share;
285
- await leveling.addXP(wp.userId, 200);
286
- await w.save();
287
- }
288
- const winNames = winners.map(w => w.name).join(' & ');
289
- if (global._tgClient && game.chatId) {
290
- try { await global._tgClient.sendMessage(game.chatId, { message: `🏆 <b>CARROM — GAME OVER!</b>\n\n${winNames} win <b>$${pot}</b>! 🎯` }); } catch (e) {}
291
- }
292
- sessions.delete(gameId);
293
  }
294
- res.json({ ok: true, state: game.getState(userId) });
295
- } catch (err) {
296
- console.error('[API] /carrom/shoot error:', err);
297
- res.status(500).json({ ok: false, error: 'Server error' });
298
- }
299
  });
300
 
301
  // ── Card Web Game API ──
302
  app.get('/wcards/state', auth, async (req, res) => {
303
  try {
304
- const { gameId } = req.query;
305
  const sessions = require('./games/sessions');
306
- const game = sessions.get(gameId);
307
- if (!game || !game.getState) return res.json({ ok: false, error: 'Game not found' });
308
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
309
- } catch (err) { console.error('[API] wcards/state:', err); res.status(500).json({ ok: false, error: 'Error' }); }
310
  });
311
 
312
  app.post('/wcards/start', auth, async (req, res) => {
313
  try {
314
- const { gameId } = req.body;
315
  const sessions = require('./games/sessions');
316
- const game = sessions.get(gameId);
317
- if (!game) return res.json({ ok: false, error: 'Game not found' });
318
  if (game.hostId !== req.tgUser.id.toString()) return res.json({ ok: false, error: 'Only host' });
319
- if (!game.start()) return res.json({ ok: false, error: 'Need 2+ players' });
320
- if (game._timer) { clearTimeout(game._timer); game._timer = null; }
321
- if (game._autoStart) { clearTimeout(game._autoStart); game._autoStart = null; }
322
- // Charge
323
- for (const p of game.players) {
324
- let u = await User.findOne({ userId: p.userId }) || await User.create({ userId: p.userId });
325
- u.wallet -= game.bet; await u.save();
326
- }
327
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
328
- } catch (err) { console.error('[API] wcards/start:', err); res.status(500).json({ ok: false, error: 'Error' }); }
329
  });
330
 
331
  app.post('/wcards/play', auth, async (req, res) => {
332
  try {
333
- const { gameId, cardIndex } = req.body;
334
  const sessions = require('./games/sessions');
335
  const leveling = require('./utils/leveling');
336
- const game = sessions.get(gameId);
337
- if (!game) return res.json({ ok: false, error: 'Game not found' });
338
- const userId = req.tgUser.id.toString();
339
- const result = game.playCard(userId, cardIndex);
340
- if (result.error) return res.json({ ok: false, error: result.error });
341
-
342
- // If game ended, pay out
343
  if (game.status === 'ended') {
344
  const winners = game.getWinners();
345
  const pot = game.bet * game.players.length;
346
  const share = Math.floor(pot / winners.length);
347
- for (const w of winners) {
348
- let u = await User.findOne({ userId: w.userId }) || await User.create({ userId: w.userId });
349
- u.wallet += share;
350
- await leveling.addXP(w.userId, 150);
351
- await u.save();
352
- }
353
- const wNames = winners.map(w => w.name).join(' & ');
354
- if (global._tgClient && game.chatId) {
355
- try { await global._tgClient.sendMessage(game.chatId, { message: `🃏 <b>CARD GAME — OVER!</b>\n\n🏆 ${wNames} wins <b>$${pot}</b>!` }); } catch (e) {}
356
- }
357
- setTimeout(() => sessions.delete(gameId), 5000);
358
  }
359
-
360
- res.json({ ok: true, state: game.getState(userId) });
361
- } catch (err) { console.error('[API] wcards/play:', err); res.status(500).json({ ok: false, error: 'Error' }); }
362
  });
363
 
364
  app.post('/wcards/next', auth, async (req, res) => {
365
  try {
366
- const { gameId } = req.body;
367
  const sessions = require('./games/sessions');
368
- const game = sessions.get(gameId);
369
- if (!game) return res.json({ ok: false, error: 'Game not found' });
370
  game.nextRound();
371
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
372
  } catch (err) { res.status(500).json({ ok: false, error: 'Error' }); }
373
  });
374
 
375
- // ── Compat: GET /daily/status ──
376
- app.get('/daily/status', auth, async (req, res) => {
377
- const userId = req.tgUser.id.toString();
378
- const user = await User.findOne({ userId });
379
- const now = new Date();
380
- const todayStr = now.toISOString().split('T')[0];
381
- if (!user) return res.json({ canClaim: true, hoursLeft: null });
382
- const lastClaim = user.lastDaily;
383
- if (!lastClaim) return res.json({ canClaim: true, hoursLeft: null });
384
- const lastStr = lastClaim.toISOString().split('T')[0];
385
- if (lastStr !== todayStr) return res.json({ canClaim: true, hoursLeft: null });
386
- return res.json({ canClaim: false, hoursLeft: 24 });
387
- });
388
-
389
  // ── Start ──
390
- const PORT = parseInt(process.env.PORT || process.env.API_PORT || '7860');
391
- function startApi() {
392
- app.listen(PORT, () => console.log(`[API] Listening on port ${PORT}`));
393
- }
394
-
395
  module.exports = { startApi };
 
6
  const app = express();
7
  app.use(express.json());
8
 
9
+ // ── CORS ──
 
10
  app.use((req, res, next) => {
11
+ res.setHeader('Access-Control-Allow-Origin', process.env.WEBAPP_ORIGIN || '*');
12
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Init-Data');
13
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
14
  if (req.method === 'OPTIONS') return res.sendStatus(204);
15
  next();
16
  });
17
 
18
+ // ── Static files ──
19
  app.use(express.static(path.join(__dirname, 'webapp')));
 
 
20
  app.get('/carrom', (req, res) => res.sendFile(path.join(__dirname, 'webapp', 'carrom.html')));
21
  app.get('/cards', (req, res) => res.sendFile(path.join(__dirname, 'webapp', 'cards.html')));
22
 
23
+ // ── Request timeout prevent hanging ──
24
+ app.use((req, res, next) => {
25
+ res.setTimeout(10000, () => { // 10 second timeout
26
+ if (!res.headersSent) res.status(408).json({ ok: false, error: 'Request timeout' });
27
+ });
28
+ next();
29
+ });
30
+
31
+ // ── Auth ──
32
+ function verifyInitData(raw) {
33
  try {
34
+ const params = new URLSearchParams(raw);
35
+ const hash = params.get('hash'); if (!hash) return null;
 
36
  params.delete('hash');
37
+ const cs = [...params.entries()].sort(([a],[b])=>a.localeCompare(b)).map(([k,v])=>`${k}=${v}`).join('\n');
38
+ const secret = crypto.createHmac('sha256','WebAppData').update(process.env.BOT_TOKEN).digest();
39
+ if (crypto.createHmac('sha256',secret).update(cs).digest('hex') !== hash) return null;
40
+ const u = params.get('user'); return u ? JSON.parse(u) : null;
 
 
 
 
 
 
 
 
 
 
 
41
  } catch { return null; }
42
  }
43
 
 
44
  function auth(req, res, next) {
45
+ const raw = req.headers['x-init-data'] || req.query.initData;
46
+ if (!raw) return res.status(401).json({ ok: false, error: 'Missing auth' });
47
+ const user = verifyInitData(raw);
48
+ if (!user) return res.status(403).json({ ok: false, error: 'Invalid auth' });
49
+ req.tgUser = user; next();
 
 
 
 
 
 
 
50
  }
51
 
52
  // ── GET /status ──
53
  app.get('/status', auth, async (req, res) => {
54
  try {
 
55
  const userId = req.tgUser.id.toString();
56
  const user = await User.findOne({ userId });
57
+ const today = new Date().toISOString().split('T')[0];
 
 
58
  if (!user) return res.json({ canClaim: true, hoursLeft: null, adsLeft: 50 });
59
+ if (req.query.type === 'mission_inpage') {
60
+ const ld = user.lastAdWatchDate?.toISOString().split('T')[0];
61
+ let cnt = ld !== today ? 0 : (user.dailyAdsCount || 0);
62
+ return res.json({ canClaim: 50 - cnt > 0, adsLeft: Math.max(0, 50 - cnt) });
 
 
 
63
  }
64
+ const ld = user.lastDaily?.toISOString().split('T')[0];
65
+ res.json({ canClaim: ld !== today, hoursLeft: ld === today ? 24 : null });
66
+ } catch (err) { res.status(500).json({ error: 'Error' }); }
 
 
 
 
 
 
 
67
  });
68
 
69
  // ── POST /claim ──
 
71
  try {
72
  const { type } = req.body;
73
  const userId = req.tgUser.id.toString();
74
+ let user = await User.findOne({ userId }) || await User.create({ userId, username: req.tgUser.username });
75
+ const today = new Date().toISOString().split('T')[0];
 
 
 
76
  if (type === 'daily') {
77
+ if (user.lastDaily?.toISOString().split('T')[0] === today) return res.json({ ok: false, error: 'Already claimed' });
78
+ user.wallet += 3000; user.xp += 100; user.lastDaily = new Date(); await user.save();
79
+ if (global._tgClient) try { await global._tgClient.sendMessage(userId, { message: `🎉 <b>+$3000</b> daily reward!` }); } catch {}
80
+ return res.json({ ok: true, reward: 3000, wallet: user.wallet });
 
 
 
 
 
 
81
  } else if (type === 'mission') {
82
+ const ld = user.lastAdWatchDate?.toISOString().split('T')[0];
83
+ if (ld !== today) user.dailyAdsCount = 0;
84
+ if (user.dailyAdsCount >= 50) return res.json({ ok: false, error: 'Limit reached' });
85
+ user.wallet += 200; user.dailyAdsCount++; user.lastAdWatchDate = new Date(); user.lastMissionClaim = new Date();
 
 
86
  await user.save();
87
+ return res.json({ ok: true, reward: 200, adsLeft: 50 - user.dailyAdsCount, wallet: user.wallet });
 
 
 
 
88
  }
89
+ res.status(400).json({ ok: false, error: 'Invalid type' });
90
+ } catch (err) { res.status(500).json({ ok: false, error: 'Error' }); }
 
 
 
91
  });
92
 
93
+ // ── GET /leaderboard — NO getEntity calls (was causing server hang) ──
94
  app.get('/leaderboard', auth, async (req, res) => {
95
  try {
96
  const { type = 'wallet', period = 'all' } = req.query;
97
+ const sorts = { bank:{bank:-1}, wallet:{wallet:-1}, xp:{xp:-1}, kills:{kills:-1}, robs:{robs:-1} };
98
+ let df = {};
 
99
  const now = new Date();
100
+ if (period === 'daily') df = { lastDaily: { $gte: new Date(now.toISOString().split('T')[0]) } };
101
+ else if (period === 'weekly') df = { lastDaily: { $gte: new Date(now - 7*86400000) } };
102
+ else if (period === 'monthly') df = { lastDaily: { $gte: new Date(now - 30*86400000) } };
103
+ const users = await User.find(df).sort(sorts[type]||{wallet:-1}).limit(50).select('userId username wallet bank xp level kills robs').lean();
104
+ // Just use stored username — NO getEntity calls
105
+ for (const u of users) if (!u.username) u.username = `User${u.userId.slice(-4)}`;
 
 
 
106
  res.json({ ok: true, leaderboard: users, type, period });
107
+ } catch (err) { res.status(500).json({ error: 'Error' }); }
108
  });
109
 
110
  // ── GET /me ──
 
112
  try {
113
  const userId = req.tgUser.id.toString();
114
  let user = await User.findOne({ userId }).lean();
115
+ if (!user) { user = (await User.create({ userId, username: req.tgUser.username })).toObject(); }
116
  res.json({ ok: true, user });
117
+ } catch (err) { res.status(500).json({ error: 'Error' }); }
118
  });
119
 
120
+ // ── Carrom API ──
121
  app.post('/carrom/start', auth, async (req, res) => {
122
  try {
123
  const { gameId } = req.body;
 
124
  const sessions = require('./games/sessions');
125
  const game = sessions.get(gameId);
126
+ if (!game) return res.json({ ok: false, error: 'Not found' });
127
+ if (game.status !== 'lobby') return res.json({ ok: false, error: 'Already started' });
128
+ if (game.hostId !== req.tgUser.id.toString()) return res.json({ ok: false, error: 'Only host' });
129
+ if (game.players.length < 2) return res.json({ ok: false, error: 'Need 2+' });
130
+ if (game.players.length === 3) { const k = game.players.pop(); game.scores.pop(); }
131
+ if (game._timer) clearTimeout(game._timer);
132
+ if (game._autoStart) clearTimeout(game._autoStart);
133
+ for (const p of game.players) { let u = await User.findOne({userId:p.userId})||await User.create({userId:p.userId}); u.wallet-=game.bet; await u.save(); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  game.start();
135
+ res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
136
+ } catch (err) { console.error('[API] carrom/start:', err.message); res.status(500).json({ ok: false, error: 'Error' }); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  });
138
 
 
139
  app.get('/carrom/state', auth, async (req, res) => {
140
  try {
 
 
141
  const sessions = require('./games/sessions');
142
+ const game = sessions.get(req.query.gameId);
143
+ if (!game?.getState) return res.json({ ok: false, error: 'Not found' });
144
+ res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
145
+ } catch (err) { res.status(500).json({ ok: false, error: 'Error' }); }
 
 
 
 
 
 
 
146
  });
147
 
 
148
  app.post('/carrom/move', auth, async (req, res) => {
149
  try {
 
 
150
  const sessions = require('./games/sessions');
151
+ const game = sessions.get(req.body.gameId);
152
+ if (!game?.moveStriker) return res.json({ ok: false, error: 'Not found' });
153
+ const r = game.moveStriker(req.tgUser.id.toString(), req.body.x);
154
+ res.json(r.error ? { ok: false, error: r.error } : { ok: true });
155
+ } catch (err) { res.status(500).json({ ok: false, error: 'Error' }); }
 
 
 
 
156
  });
157
 
 
158
  app.post('/carrom/shoot', auth, async (req, res) => {
159
  try {
 
 
160
  const sessions = require('./games/sessions');
161
  const leveling = require('./utils/leveling');
162
+ const game = sessions.get(req.body.gameId);
163
+ if (!game?.shoot) return res.json({ ok: false, error: 'Not found' });
164
+ const r = game.shoot(req.tgUser.id.toString(), req.body.vx, req.body.vy);
165
+ if (r.error) return res.json({ ok: false, error: r.error });
 
 
 
 
 
166
  if (game.status === 'ended') {
167
  const winners = game.getWinnerPlayers();
168
  const pot = game.bet * game.players.length;
169
  const share = Math.floor(pot / winners.length);
170
+ for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,200); await u.save(); }
171
+ if (global._tgClient && game.chatId) try { await global._tgClient.sendMessage(game.chatId, { message: `🏆 <b>CARROM OVER!</b> ${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!` }); } catch {}
172
+ setTimeout(() => sessions.delete(req.body.gameId), 3000);
 
 
 
 
 
 
 
 
173
  }
174
+ res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
175
+ } catch (err) { console.error('[API] carrom/shoot:', err.message); res.status(500).json({ ok: false, error: 'Error' }); }
 
 
 
176
  });
177
 
178
  // ── Card Web Game API ──
179
  app.get('/wcards/state', auth, async (req, res) => {
180
  try {
 
181
  const sessions = require('./games/sessions');
182
+ const game = sessions.get(req.query.gameId);
183
+ if (!game?.getState) return res.json({ ok: false, error: 'Not found' });
184
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
185
+ } catch (err) { res.status(500).json({ ok: false, error: 'Error' }); }
186
  });
187
 
188
  app.post('/wcards/start', auth, async (req, res) => {
189
  try {
 
190
  const sessions = require('./games/sessions');
191
+ const game = sessions.get(req.body.gameId);
192
+ if (!game) return res.json({ ok: false, error: 'Not found' });
193
  if (game.hostId !== req.tgUser.id.toString()) return res.json({ ok: false, error: 'Only host' });
194
+ if (!game.start()) return res.json({ ok: false, error: 'Need 2+' });
195
+ if (game._timer) clearTimeout(game._timer);
196
+ if (game._autoStart) clearTimeout(game._autoStart);
197
+ for (const p of game.players) { let u = await User.findOne({userId:p.userId})||await User.create({userId:p.userId}); u.wallet-=game.bet; await u.save(); }
 
 
 
 
198
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
199
+ } catch (err) { res.status(500).json({ ok: false, error: 'Error' }); }
200
  });
201
 
202
  app.post('/wcards/play', auth, async (req, res) => {
203
  try {
 
204
  const sessions = require('./games/sessions');
205
  const leveling = require('./utils/leveling');
206
+ const game = sessions.get(req.body.gameId);
207
+ if (!game) return res.json({ ok: false, error: 'Not found' });
208
+ const r = game.playCard(req.tgUser.id.toString(), req.body.cardIndex);
209
+ if (r.error) return res.json({ ok: false, error: r.error });
 
 
 
210
  if (game.status === 'ended') {
211
  const winners = game.getWinners();
212
  const pot = game.bet * game.players.length;
213
  const share = Math.floor(pot / winners.length);
214
+ for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,150); await u.save(); }
215
+ if (global._tgClient && game.chatId) try { await global._tgClient.sendMessage(game.chatId, { message: `🃏 <b>CARDS OVER!</b> ${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!` }); } catch {}
216
+ setTimeout(() => sessions.delete(req.body.gameId), 5000);
 
 
 
 
 
 
 
 
217
  }
218
+ res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
219
+ } catch (err) { res.status(500).json({ ok: false, error: 'Error' }); }
 
220
  });
221
 
222
  app.post('/wcards/next', auth, async (req, res) => {
223
  try {
 
224
  const sessions = require('./games/sessions');
225
+ const game = sessions.get(req.body.gameId);
226
+ if (!game) return res.json({ ok: false, error: 'Not found' });
227
  game.nextRound();
228
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
229
  } catch (err) { res.status(500).json({ ok: false, error: 'Error' }); }
230
  });
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  // ── Start ──
233
+ const PORT = parseInt(process.env.PORT || '7860');
234
+ function startApi() { app.listen(PORT, () => console.log(`[API] Port ${PORT}`)); }
 
 
 
235
  module.exports = { startApi };
bot.js CHANGED
@@ -23,7 +23,6 @@ const inlineHandler = require('./handlers/inlineHandler');
23
  const boardGames = require('./handlers/boardGames');
24
  const boardHandler = require('./handlers/boardHandler');
25
  const blackjack = require('./handlers/blackjack');
26
- const cardGame = require('./handlers/cardGameHandler');
27
  const strategy = require('./handlers/strategyGames');
28
  const shopHandler = require('./handlers/shop');
29
  const admin = require('./handlers/admin');
@@ -182,8 +181,7 @@ const MP_FALLBACK = "⚠️ This command only works in groups! Add me to a group
182
  "🎲 /dice &lt;bet&gt; — Dice duel\n" +
183
  "🎯 /carrom &lt;bet&gt; — Carrom Pool\n" +
184
  "🔓 /hack &lt;bet&gt; &lt;len&gt; — PIN hack\n" +
185
- "🃏 /cards &lt;bet&gt; — Card flip (text)\n" +
186
- "🃏 /wcards &lt;bet&gt; — Card game (Mini App)\n\n" +
187
  "<b>🎰 Solo Casino</b>\n" +
188
  "/bj &lt;bet&gt; — Blackjack\n" +
189
  "/slots — Slots | /mines — Mines\n" +
@@ -248,18 +246,15 @@ const MP_FALLBACK = "⚠️ This command only works in groups! Add me to a group
248
  if (cmdStart("/xox", text)) { if (requireGroup(event) && await isAlive(event)) await boardGames.startXOX(client, event); }
249
  if (cmdStart("/c4", text)) { if (requireGroup(event) && await isAlive(event)) await boardGames.startC4(client, event); }
250
  if (cmdStart("/dice", text)) { if (requireGroup(event) && await isAlive(event)) await games.startDice(client, event); }
251
- if (cmdStart("/wcards", text)) { if (requireGroup(event) && await isAlive(event)) await webCards.startWebCards(client, event); }
252
  if (cmdStart("/carrom", text)) { if (requireGroup(event) && await isAlive(event)) await carrom.startCarrom(client, event); }
253
- if (cmdStart("/cards", text)) { if (requireGroup(event) && await isAlive(event)) await cardGame.initCardGame(client, event); }
254
  if (cmdStart("/hack", text)) { if (requireGroup(event) && await isAlive(event)) await multiHack.initHack(client, event); }
255
  if (cmd("/join", text)) {
256
  if (isGroupChat(event.chatId)) {
257
  await multiHack.joinHack(client, event);
258
- await cardGame.joinCardGame(client, event);
259
  }
260
  }
261
  if (cmdStart("/guess", text)) await multiHack.processGuess(client, event);
262
- if (cmdStart("/flip", text)) await cardGame.processFlip(client, event);
263
  if (cmd("/surrender", text)) await strategy.surrender(client, event);
264
 
265
  }, new NewMessage({}));
 
23
  const boardGames = require('./handlers/boardGames');
24
  const boardHandler = require('./handlers/boardHandler');
25
  const blackjack = require('./handlers/blackjack');
 
26
  const strategy = require('./handlers/strategyGames');
27
  const shopHandler = require('./handlers/shop');
28
  const admin = require('./handlers/admin');
 
181
  "🎲 /dice &lt;bet&gt; — Dice duel\n" +
182
  "🎯 /carrom &lt;bet&gt; — Carrom Pool\n" +
183
  "🔓 /hack &lt;bet&gt; &lt;len&gt; — PIN hack\n" +
184
+ "🃏 /cards &lt;bet&gt; — Card Game\n\n" +
 
185
  "<b>🎰 Solo Casino</b>\n" +
186
  "/bj &lt;bet&gt; — Blackjack\n" +
187
  "/slots — Slots | /mines — Mines\n" +
 
246
  if (cmdStart("/xox", text)) { if (requireGroup(event) && await isAlive(event)) await boardGames.startXOX(client, event); }
247
  if (cmdStart("/c4", text)) { if (requireGroup(event) && await isAlive(event)) await boardGames.startC4(client, event); }
248
  if (cmdStart("/dice", text)) { if (requireGroup(event) && await isAlive(event)) await games.startDice(client, event); }
249
+ if (cmdStart("/cards", text) || cmdStart("/wcards", text)) { if (requireGroup(event) && await isAlive(event)) await webCards.startWebCards(client, event); }
250
  if (cmdStart("/carrom", text)) { if (requireGroup(event) && await isAlive(event)) await carrom.startCarrom(client, event); }
 
251
  if (cmdStart("/hack", text)) { if (requireGroup(event) && await isAlive(event)) await multiHack.initHack(client, event); }
252
  if (cmd("/join", text)) {
253
  if (isGroupChat(event.chatId)) {
254
  await multiHack.joinHack(client, event);
 
255
  }
256
  }
257
  if (cmdStart("/guess", text)) await multiHack.processGuess(client, event);
 
258
  if (cmd("/surrender", text)) await strategy.surrender(client, event);
259
 
260
  }, new NewMessage({}));
scripts/downloadGifs.js CHANGED
@@ -1,135 +1,56 @@
1
  #!/usr/bin/env node
2
- /**
3
- * Downloads GIFs from Giphy API for each shop item.
4
- * Run during Docker build or manually: node scripts/downloadGifs.js
5
- *
6
- * Uses GIPHY_API_KEY env var or falls back to hardcoded key.
7
- * Downloads 5 GIFs per item into ./gifs/<itemDir>/0.gif, 1.gif, etc.
8
- */
9
-
10
  const https = require('https');
11
- const http = require('http');
12
  const fs = require('fs');
13
  const path = require('path');
14
 
15
  const API_KEY = process.env.GIPHY_API_KEY || 'Ei3iNGXxdCwjwyxwvSaLq7BgNaOLPFEh';
16
  const GIFS_PER_ITEM = 5;
17
-
18
- // Search terms mapped to each shop item gifDir
19
  const SEARCH_MAP = {
20
- 'rose': 'rose flower',
21
- 'teddy': 'teddy bear cute',
22
- 'ring': 'diamond ring',
23
- 'crown': 'crown king queen',
24
- 'chocolate': 'chocolate candy',
25
- 'star': 'star gold sparkle',
26
- 'heart': 'heart love red',
27
- 'trophy': 'trophy cup winner',
28
- 'fire': 'fire flame',
29
- 'rocket': 'rocket launch',
30
  };
31
-
32
  const GIFS_DIR = path.join(__dirname, '..', 'gifs');
33
 
34
  function fetch(url) {
35
  return new Promise((resolve, reject) => {
36
- const client = url.startsWith('https') ? https : http;
37
- client.get(url, { headers: { 'User-Agent': 'AlexaGameBot/1.0' } }, (res) => {
38
- // Follow redirects
39
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
40
  return fetch(res.headers.location).then(resolve).catch(reject);
41
- }
42
- const chunks = [];
43
- res.on('data', c => chunks.push(c));
44
- res.on('end', () => resolve(Buffer.concat(chunks)));
45
- res.on('error', reject);
46
  }).on('error', reject);
47
  });
48
  }
49
 
50
- async function searchGiphy(query, limit) {
51
- const url = `https://api.giphy.com/v1/gifs/search?api_key=${API_KEY}&q=${encodeURIComponent(query)}&limit=${limit}&rating=g&lang=en`;
52
- const data = await fetch(url);
53
- const json = JSON.parse(data.toString());
54
- if (!json.data || json.data.length === 0) {
55
- console.log(` ⚠ No results for "${query}"`);
56
- return [];
57
- }
58
- // Get the downsized GIF URLs (smaller, good for Telegram)
59
- return json.data.map(g => {
60
- // Prefer fixed_height_small or downsized
61
- const img = g.images;
62
- return img.fixed_height_small?.url || img.downsized?.url || img.original?.url;
63
- }).filter(Boolean);
64
- }
65
-
66
- async function downloadFile(url, dest) {
67
- const data = await fetch(url);
68
- fs.writeFileSync(dest, data);
69
- }
70
-
71
  async function main() {
72
  console.log('🎬 Downloading GIFs from Giphy...');
73
- console.log(` API Key: ${API_KEY.slice(0, 8)}...`);
74
- console.log(` GIFs per item: ${GIFS_PER_ITEM}`);
75
- console.log(` Output: ${GIFS_DIR}\n`);
76
-
77
- // Create base dir
78
  if (!fs.existsSync(GIFS_DIR)) fs.mkdirSync(GIFS_DIR, { recursive: true });
79
-
80
  for (const [dir, query] of Object.entries(SEARCH_MAP)) {
81
  const itemDir = path.join(GIFS_DIR, dir);
82
  if (!fs.existsSync(itemDir)) fs.mkdirSync(itemDir, { recursive: true });
83
-
84
- // Skip if already has enough GIFs
85
  const existing = fs.readdirSync(itemDir).filter(f => f.endsWith('.gif'));
86
- if (existing.length >= GIFS_PER_ITEM) {
87
- console.log(` ${dir}/ — already has ${existing.length} GIFs, skipping`);
88
- continue;
89
- }
90
-
91
- console.log(`📥 ${dir}/ — searching "${query}"...`);
92
  try {
93
- const urls = await searchGiphy(query, GIFS_PER_ITEM);
94
- let downloaded = 0;
95
- for (let i = 0; i < urls.length && downloaded < GIFS_PER_ITEM; i++) {
96
- const dest = path.join(itemDir, `${downloaded}.gif`);
 
 
 
97
  try {
98
- await downloadFile(urls[i], dest);
99
- const size = fs.statSync(dest).size;
100
- if (size < 1000) {
101
- // Too small, probably an error
102
- fs.unlinkSync(dest);
103
- continue;
104
- }
105
- console.log(` ✅ ${downloaded}.gif (${(size / 1024).toFixed(1)}KB)`);
106
- downloaded++;
107
- } catch (e) {
108
- console.log(` ❌ Failed to download: ${e.message}`);
109
- }
110
  }
111
- if (downloaded === 0) {
112
- console.log(` ⚠ No GIFs downloaded for ${dir}`);
113
- }
114
- } catch (e) {
115
- console.log(` ❌ Search failed: ${e.message}`);
116
- }
117
  }
118
-
119
- console.log('\n🎉 GIF download complete!');
120
-
121
- // Summary
122
- let total = 0;
123
- for (const dir of Object.keys(SEARCH_MAP)) {
124
- const itemDir = path.join(GIFS_DIR, dir);
125
- const count = fs.existsSync(itemDir) ? fs.readdirSync(itemDir).filter(f => f.endsWith('.gif')).length : 0;
126
- total += count;
127
- console.log(` ${dir}: ${count} GIFs`);
128
- }
129
- console.log(` Total: ${total} GIFs`);
130
  }
131
-
132
- main().catch(e => {
133
- console.error('Fatal error:', e.message);
134
- process.exit(0); // Don't fail the build
135
- });
 
1
  #!/usr/bin/env node
 
 
 
 
 
 
 
 
2
  const https = require('https');
 
3
  const fs = require('fs');
4
  const path = require('path');
5
 
6
  const API_KEY = process.env.GIPHY_API_KEY || 'Ei3iNGXxdCwjwyxwvSaLq7BgNaOLPFEh';
7
  const GIFS_PER_ITEM = 5;
 
 
8
  const SEARCH_MAP = {
9
+ 'rose':'rose flower','teddy':'teddy bear cute','ring':'diamond ring',
10
+ 'crown':'crown king queen','chocolate':'chocolate candy','star':'star gold sparkle',
11
+ 'heart':'heart love red','trophy':'trophy cup winner','fire':'fire flame','rocket':'rocket launch',
 
 
 
 
 
 
 
12
  };
 
13
  const GIFS_DIR = path.join(__dirname, '..', 'gifs');
14
 
15
  function fetch(url) {
16
  return new Promise((resolve, reject) => {
17
+ const client = url.startsWith('https') ? https : require('http');
18
+ client.get(url, { headers: { 'User-Agent': 'Bot/1.0' } }, (res) => {
19
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
 
20
  return fetch(res.headers.location).then(resolve).catch(reject);
21
+ const chunks = []; res.on('data', c => chunks.push(c));
22
+ res.on('end', () => resolve(Buffer.concat(chunks))); res.on('error', reject);
 
 
 
23
  }).on('error', reject);
24
  });
25
  }
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  async function main() {
28
  console.log('🎬 Downloading GIFs from Giphy...');
 
 
 
 
 
29
  if (!fs.existsSync(GIFS_DIR)) fs.mkdirSync(GIFS_DIR, { recursive: true });
 
30
  for (const [dir, query] of Object.entries(SEARCH_MAP)) {
31
  const itemDir = path.join(GIFS_DIR, dir);
32
  if (!fs.existsSync(itemDir)) fs.mkdirSync(itemDir, { recursive: true });
 
 
33
  const existing = fs.readdirSync(itemDir).filter(f => f.endsWith('.gif'));
34
+ if (existing.length >= GIFS_PER_ITEM) { console.log(`✅ ${dir}/ — ${existing.length} GIFs`); continue; }
35
+ console.log(`📥 ${dir}/ — "${query}"...`);
 
 
 
 
36
  try {
37
+ const data = await fetch(`https://api.giphy.com/v1/gifs/search?api_key=${API_KEY}&q=${encodeURIComponent(query)}&limit=${GIFS_PER_ITEM}&rating=g`);
38
+ const json = JSON.parse(data.toString());
39
+ if (!json.data?.length) { console.log(` ⚠ No results`); continue; }
40
+ let dl = 0;
41
+ for (let i = 0; i < json.data.length && dl < GIFS_PER_ITEM; i++) {
42
+ const url = json.data[i].images?.fixed_height_small?.url || json.data[i].images?.downsized?.url;
43
+ if (!url) continue;
44
  try {
45
+ const gif = await fetch(url);
46
+ if (gif.length < 1000) continue;
47
+ fs.writeFileSync(path.join(itemDir, `${dl}.gif`), gif);
48
+ console.log(` ✅ ${dl}.gif (${(gif.length/1024).toFixed(1)}KB)`);
49
+ dl++;
50
+ } catch (e) { console.log(` ❌ ${e.message}`); }
 
 
 
 
 
 
51
  }
52
+ } catch (e) { console.log(` ❌ ${e.message}`); }
 
 
 
 
 
53
  }
54
+ console.log('🎉 Done!');
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
+ main().catch(e => { console.error(e.message); process.exit(0); });