Spaces:
Running
Running
Upload 50 files
Browse files- .dockerignore +10 -9
- api.js +109 -269
- bot.js +2 -7
- 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
|
| 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 |
-
// ──
|
| 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 |
-
// ──
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
try {
|
| 29 |
-
const params = new URLSearchParams(
|
| 30 |
-
const hash = params.get('hash');
|
| 31 |
-
if (!hash) return null;
|
| 32 |
params.delete('hash');
|
| 33 |
-
const
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 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
|
| 54 |
-
if (!
|
| 55 |
-
|
| 56 |
-
|
| 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
|
| 74 |
-
const todayStr = now.toISOString().split('T')[0];
|
| 75 |
-
|
| 76 |
if (!user) return res.json({ canClaim: true, hoursLeft: null, adsLeft: 50 });
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 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 |
-
|
| 87 |
-
|
| 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 |
-
|
| 104 |
-
const now = new Date();
|
| 105 |
-
const todayStr = now.toISOString().split('T')[0];
|
| 106 |
-
|
| 107 |
if (type === 'daily') {
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 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
|
| 120 |
-
if (
|
| 121 |
-
if (user.dailyAdsCount >= 50) return res.json({ ok: false, error: '
|
| 122 |
-
|
| 123 |
-
user.wallet += reward; user.dailyAdsCount += 1;
|
| 124 |
-
user.lastAdWatchDate = now; user.lastInpageMissionClaim = now; user.lastMissionClaim = now;
|
| 125 |
await user.save();
|
| 126 |
-
|
| 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 |
-
|
| 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
|
| 144 |
-
|
| 145 |
-
let dateFilter = {};
|
| 146 |
const now = new Date();
|
| 147 |
-
if (period === 'daily')
|
| 148 |
-
else if (period === 'weekly')
|
| 149 |
-
else if (period === 'monthly')
|
| 150 |
-
const users = await User.find(
|
| 151 |
-
|
| 152 |
-
|
| 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) {
|
| 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 })
|
| 166 |
res.json({ ok: true, user });
|
| 167 |
-
} catch (err) {
|
| 168 |
});
|
| 169 |
|
| 170 |
-
// ──
|
| 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: '
|
| 178 |
-
if (game.status !== 'lobby') return res.json({ ok: false, error: '
|
| 179 |
-
|
| 180 |
-
if (game.
|
| 181 |
-
if (game.players.length
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 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 |
-
|
| 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
|
| 234 |
-
|
| 235 |
-
|
| 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
|
| 253 |
-
const
|
| 254 |
-
|
| 255 |
-
|
| 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
|
| 271 |
-
|
| 272 |
-
|
| 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
|
| 283 |
-
|
| 284 |
-
|
| 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(
|
| 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
|
| 308 |
res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
|
| 309 |
-
} catch (err) {
|
| 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: '
|
| 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+
|
| 320 |
-
if (game._timer)
|
| 321 |
-
if (game._autoStart)
|
| 322 |
-
|
| 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) {
|
| 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: '
|
| 338 |
-
const
|
| 339 |
-
|
| 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 |
-
|
| 349 |
-
|
| 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 |
-
|
| 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: '
|
| 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 ||
|
| 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 <bet> — Dice duel\n" +
|
| 183 |
"🎯 /carrom <bet> — Carrom Pool\n" +
|
| 184 |
"🔓 /hack <bet> <len> — PIN hack\n" +
|
| 185 |
-
"🃏 /cards <bet> — Card
|
| 186 |
-
"🃏 /wcards <bet> — Card game (Mini App)\n\n" +
|
| 187 |
"<b>🎰 Solo Casino</b>\n" +
|
| 188 |
"/bj <bet> — 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 <bet> — Dice duel\n" +
|
| 182 |
"🎯 /carrom <bet> — Carrom Pool\n" +
|
| 183 |
"🔓 /hack <bet> <len> — PIN hack\n" +
|
| 184 |
+
"🃏 /cards <bet> — Card Game\n\n" +
|
|
|
|
| 185 |
"<b>🎰 Solo Casino</b>\n" +
|
| 186 |
"/bj <bet> — 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':
|
| 21 |
-
'
|
| 22 |
-
'
|
| 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': '
|
| 38 |
-
|
| 39 |
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
| 40 |
return fetch(res.headers.location).then(resolve).catch(reject);
|
| 41 |
-
|
| 42 |
-
|
| 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 |
-
|
| 88 |
-
continue;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
console.log(`📥 ${dir}/ — searching "${query}"...`);
|
| 92 |
try {
|
| 93 |
-
const
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
| 97 |
try {
|
| 98 |
-
await
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 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 |
-
|
| 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); });
|
|
|
|
|
|
|
|
|
|
|
|