mrpoddaa commited on
Commit
8af9fb7
·
verified ·
1 Parent(s): e26eaaf

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +28 -0
  2. index.js +339 -0
  3. package.json +19 -0
  4. strings.html +206 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ──────────────────────────────────────────────
2
+ # TG Store · Dockerfile for Hugging Face Spaces
3
+ # ──────────────────────────────────────────────
4
+ FROM node:20-slim
5
+
6
+ # HF Spaces runs containers as uid 1000
7
+ RUN useradd -m -u 1000 appuser
8
+
9
+ WORKDIR /app
10
+
11
+ # Install dependencies first (better layer caching)
12
+ COPY package.json ./
13
+ RUN npm install --omit=dev
14
+
15
+ # Copy application source
16
+ COPY --chown=appuser:appuser . .
17
+
18
+ # Hugging Face Spaces requires port 7860
19
+ ENV PORT=7860
20
+ EXPOSE 7860
21
+
22
+ USER appuser
23
+
24
+ # HF pings /system to verify the app is healthy
25
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=20s \
26
+ CMD node -e "require('http').get('http://localhost:7860/system',r=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"
27
+
28
+ CMD ["node", "index.js"]
index.js ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const { MongoClient } = require('mongodb');
5
+ const Busboy = require('busboy');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+ const { TelegramClient, Api } = require('telegram');
9
+ const { StringSession } = require('telegram/sessions');
10
+ const { PassThrough } = require('stream');
11
+
12
+ // ─────────────────────────────────────────────
13
+ // CONFIG
14
+ // ─────────────────────────────────────────────
15
+ const PORT = process.env.PORT || 7860;
16
+ const MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/tgstore';
17
+ const CHANNEL_ID = BigInt(process.env.CHANNEL_ID || '0');
18
+ const CHUNK_SIZE = 1.9 * 1024 * 1024 * 1024; // 1.9 GB in bytes
19
+ const DL_WORKERS = parseInt(process.env.DL_WORKERS) || 4;
20
+ const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
21
+
22
+ // ─────────────────────────────────────────────
23
+ // MONGODB
24
+ // ─────────────────────────────────────────────
25
+ let Sessions, Files;
26
+
27
+ async function connectMongo() {
28
+ const client = new MongoClient(MONGO_URI, { maxPoolSize: 20 });
29
+ await client.connect();
30
+ const db = client.db();
31
+ Sessions = db.collection('sessions');
32
+ Files = db.collection('files');
33
+ await Files.createIndex({ fileId: 1 }, { unique: true });
34
+ console.log('[MongoDB] Connected');
35
+ }
36
+
37
+ // ─────────────────────────────────────────────
38
+ // SESSION POOL (round-robin)
39
+ // ─────────────────────────────────────────────
40
+ const clientPool = [];
41
+ let poolIndex = 0;
42
+
43
+ async function buildClient(apiId, apiHash, sessionString = '') {
44
+ const session = new StringSession(sessionString);
45
+ const client = new TelegramClient(session, Number(apiId), apiHash, {
46
+ connectionRetries: 5,
47
+ retryDelay: 1000,
48
+ autoReconnect: true,
49
+ maxConcurrentDownloads: DL_WORKERS,
50
+ });
51
+ await client.connect();
52
+ return client;
53
+ }
54
+
55
+ async function loadSessions() {
56
+ const docs = await Sessions.find({ active: true }).toArray();
57
+ for (const doc of docs) {
58
+ try {
59
+ const client = await buildClient(doc.apiId, doc.apiHash, doc.session);
60
+ clientPool.push(client);
61
+ console.log(`[Pool] Loaded session for ${doc.phone}`);
62
+ } catch (e) {
63
+ console.warn(`[Pool] Failed session ${doc._id}: ${e.message}`);
64
+ }
65
+ }
66
+ console.log(`[Pool] ${clientPool.length} session(s) active`);
67
+ }
68
+
69
+ function getClient() {
70
+ if (!clientPool.length)
71
+ throw new Error('No active Telegram sessions. Visit /strings to add one.');
72
+ const client = clientPool[poolIndex % clientPool.length];
73
+ poolIndex++;
74
+ return client;
75
+ }
76
+
77
+ // ─────────────────────────────────────────────
78
+ // PENDING AUTH MAP phone → { client, hash }
79
+ // ─────────────────────────────────────────────
80
+ const authMap = new Map();
81
+
82
+ // ─────────────────────────────────────────────
83
+ // EXPRESS
84
+ // ─────────────────────────────────────────────
85
+ const app = express();
86
+ app.use(express.json());
87
+ app.use(express.urlencoded({ extended: true }));
88
+ app.use(express.static(path.join(__dirname, 'public')));
89
+
90
+ // ── GET /system ──────────────────────────────
91
+ app.get('/system', (_req, res) => {
92
+ const m = process.memoryUsage();
93
+ res.json({
94
+ status: 'ok',
95
+ uptime: process.uptime(),
96
+ memory: {
97
+ rss: m.rss,
98
+ heapUsed: m.heapUsed,
99
+ heapTotal: m.heapTotal,
100
+ external: m.external,
101
+ },
102
+ sessions: clientPool.length,
103
+ node: process.version,
104
+ ts: new Date().toISOString(),
105
+ });
106
+ });
107
+
108
+ // ── GET /strings ─────────────────────────────
109
+ app.get('/strings', (_req, res) => {
110
+ res.sendFile(path.join(__dirname, 'public', 'strings.html'));
111
+ });
112
+
113
+ // ── POST /strings/send-code ──────────────────
114
+ app.post('/strings/send-code', async (req, res) => {
115
+ const { apiId, apiHash, phone } = req.body;
116
+ if (!apiId || !apiHash || !phone)
117
+ return res.status(400).json({ error: 'apiId, apiHash and phone are required.' });
118
+
119
+ try {
120
+ const client = await buildClient(apiId, apiHash, '');
121
+ const result = await client.sendCode({ apiId: Number(apiId), apiHash }, phone);
122
+ authMap.set(phone, { client, apiId, apiHash, phoneCodeHash: result.phoneCodeHash });
123
+ res.json({ ok: true, message: 'OTP sent to your Telegram app.' });
124
+ } catch (e) {
125
+ res.status(500).json({ error: e.message });
126
+ }
127
+ });
128
+
129
+ // ── POST /strings/verify ─────────────────────
130
+ app.post('/strings/verify', async (req, res) => {
131
+ const { phone, code, password } = req.body;
132
+ const entry = authMap.get(phone);
133
+ if (!entry)
134
+ return res.status(400).json({ error: 'No pending auth for this phone. Send code first.' });
135
+
136
+ const { client, apiId, apiHash, phoneCodeHash } = entry;
137
+
138
+ try {
139
+ await client.invoke(new Api.auth.SignIn({ phoneNumber: phone, phoneCodeHash, phoneCode: code }));
140
+ } catch (e) {
141
+ if (e.errorMessage === 'SESSION_PASSWORD_NEEDED') {
142
+ if (!password)
143
+ return res.status(400).json({ error: '2FA password required.', twoFA: true });
144
+ try {
145
+ const pwdInfo = await client.invoke(new Api.account.GetPassword());
146
+ const check = await require('telegram/utils/Password').computeCheck(pwdInfo, password);
147
+ await client.invoke(new Api.auth.CheckPassword({ password: check }));
148
+ } catch (e2) {
149
+ authMap.delete(phone);
150
+ return res.status(400).json({ error: e2.message });
151
+ }
152
+ } else {
153
+ authMap.delete(phone);
154
+ return res.status(400).json({ error: e.message });
155
+ }
156
+ }
157
+
158
+ const sessionString = client.session.save();
159
+ await Sessions.insertOne({
160
+ apiId, apiHash, phone,
161
+ session: sessionString,
162
+ active: true,
163
+ createdAt: new Date(),
164
+ });
165
+ clientPool.push(client);
166
+ authMap.delete(phone);
167
+
168
+ res.json({ ok: true, session: sessionString, message: 'Session saved and added to pool!' });
169
+ });
170
+
171
+ // ── POST /upload ─────────────────────────────
172
+ app.post('/upload', (req, res) => {
173
+ let client;
174
+ try { client = getClient(); }
175
+ catch (e) { return res.status(503).json({ error: e.message }); }
176
+
177
+ const busboy = Busboy({ headers: req.headers, limits: { files: 1, fileSize: Infinity } });
178
+ let responded = false;
179
+ const done = (code, body) => { if (!responded) { responded = true; res.status(code).json(body); } };
180
+
181
+ busboy.on('file', async (_field, fileStream, info) => {
182
+ const { filename, mimeType } = info;
183
+ const fileId = crypto.randomBytes(16).toString('hex');
184
+
185
+ const chunks = []; // { chunkIndex, messageId, size }
186
+ let chunkIndex = 0;
187
+ let totalSize = 0;
188
+ let chunkBufs = [];
189
+ let chunkBytes = 0;
190
+ let uploadError = null;
191
+
192
+ const flushChunk = async () => {
193
+ if (!chunkBufs.length) return;
194
+ const buf = Buffer.concat(chunkBufs);
195
+ chunkBufs = [];
196
+ chunkBytes = 0;
197
+ const idx = chunkIndex++;
198
+ const msgId = await uploadBuffer(client, buf, `${fileId}_${idx}`);
199
+ chunks.push({ chunkIndex: idx, messageId: msgId, size: buf.length });
200
+ };
201
+
202
+ // Backpressure-aware data handler
203
+ fileStream.on('data', async (data) => {
204
+ if (uploadError) return;
205
+ fileStream.pause();
206
+ totalSize += data.length;
207
+ chunkBufs.push(data);
208
+ chunkBytes += data.length;
209
+ try {
210
+ if (chunkBytes >= CHUNK_SIZE) await flushChunk();
211
+ } catch (e) {
212
+ uploadError = e;
213
+ fileStream.destroy();
214
+ }
215
+ fileStream.resume();
216
+ });
217
+
218
+ fileStream.on('end', async () => {
219
+ if (uploadError) return done(500, { error: uploadError.message });
220
+ try {
221
+ await flushChunk(); // flush remainder
222
+ await Files.insertOne({ fileId, filename, mimeType, totalSize, chunks, uploadedAt: new Date() });
223
+ done(200, {
224
+ ok: true, fileId, filename,
225
+ size: totalSize,
226
+ chunks: chunks.length,
227
+ downloadUrl: `${BASE_URL}/download/${fileId}`,
228
+ });
229
+ } catch (e) {
230
+ done(500, { error: e.message });
231
+ }
232
+ });
233
+
234
+ fileStream.on('error', (e) => done(500, { error: e.message }));
235
+ });
236
+
237
+ busboy.on('error', (e) => done(500, { error: e.message }));
238
+ req.pipe(busboy);
239
+ });
240
+
241
+ // ── GET /download/:fileId ────────────────────
242
+ app.get('/download/:fileId', async (req, res) => {
243
+ const doc = await Files.findOne({ fileId: req.params.fileId });
244
+ if (!doc) return res.status(404).json({ error: 'File not found.' });
245
+
246
+ let client;
247
+ try { client = getClient(); }
248
+ catch (e) { return res.status(503).json({ error: e.message }); }
249
+
250
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(doc.filename)}"`);
251
+ res.setHeader('Content-Type', doc.mimeType || 'application/octet-stream');
252
+ if (doc.totalSize) res.setHeader('Content-Length', String(doc.totalSize));
253
+
254
+ const sorted = [...doc.chunks].sort((a, b) => a.chunkIndex - b.chunkIndex);
255
+
256
+ for (const chunk of sorted) {
257
+ try {
258
+ await streamChunk(client, chunk.messageId, res);
259
+ } catch (e) {
260
+ console.error('[Download] chunk error:', e.message);
261
+ if (!res.headersSent) res.status(500).json({ error: e.message });
262
+ else res.destroy();
263
+ return;
264
+ }
265
+ }
266
+ res.end();
267
+ });
268
+
269
+ // ─────────────────────────────────────────────
270
+ // TELEGRAM HELPERS
271
+ // ─────────────────────────────────────────────
272
+
273
+ // Upload a Buffer to the Telegram channel and return the message ID
274
+ async function uploadBuffer(client, buffer, caption) {
275
+ const file = await client.uploadFile({
276
+ file: new BufferFile(caption, buffer),
277
+ workers: DL_WORKERS,
278
+ });
279
+ const msg = await client.sendFile(CHANNEL_ID, {
280
+ file,
281
+ caption,
282
+ forceDocument: true,
283
+ workers: DL_WORKERS,
284
+ });
285
+ return msg.id;
286
+ }
287
+
288
+ // Stream a Telegram message's media directly to the HTTP response
289
+ async function streamChunk(client, messageId, res) {
290
+ const [msg] = await client.getMessages(CHANNEL_ID, { ids: [messageId] });
291
+ if (!msg?.media) throw new Error(`No media in message ${messageId}`);
292
+
293
+ const pass = new PassThrough();
294
+
295
+ pass.on('data', (chunk) => {
296
+ const ok = res.write(chunk);
297
+ if (!ok) {
298
+ pass.pause();
299
+ res.once('drain', () => pass.resume()); // handle backpressure
300
+ }
301
+ });
302
+
303
+ await new Promise((resolve, reject) => {
304
+ pass.on('end', resolve);
305
+ pass.on('error', reject);
306
+ client.downloadMedia(msg, { outputFile: pass, workers: DL_WORKERS })
307
+ .then(() => pass.end())
308
+ .catch(reject);
309
+ });
310
+ }
311
+
312
+ // GramJS-compatible file object that reads from a Buffer
313
+ class BufferFile {
314
+ constructor(name, buffer) {
315
+ this.name = name;
316
+ this.size = buffer.length;
317
+ this.path = name;
318
+ this._buf = buffer;
319
+ }
320
+ async *[Symbol.asyncIterator]() {
321
+ const PART = 512 * 1024;
322
+ for (let i = 0; i < this._buf.length; i += PART)
323
+ yield this._buf.slice(i, i + PART);
324
+ }
325
+ }
326
+
327
+ // ─────────────────────────────────────────────
328
+ // BOOT
329
+ // ─────────────────────────────────────────────
330
+ (async () => {
331
+ await connectMongo();
332
+ await loadSessions();
333
+ app.listen(PORT, '0.0.0.0', () => {
334
+ console.log(`\n🚀 Server ready at http://0.0.0.0:${PORT}`);
335
+ console.log(`🔗 Base URL : ${BASE_URL}`);
336
+ console.log(`📦 Sessions : ${clientPool.length}`);
337
+ console.log(`🗄️ MongoDB : ${MONGO_URI}\n`);
338
+ });
339
+ })();
package.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "tg-store",
3
+ "version": "1.0.0",
4
+ "description": "Telegram File Storage & Direct Download System for Hugging Face Spaces",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "dev": "node --watch index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "dependencies": {
14
+ "busboy": "^1.6.0",
15
+ "express": "^4.18.2",
16
+ "mongodb": "^6.3.0",
17
+ "telegram": "^2.22.2"
18
+ }
19
+ }
strings.html ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>TG Store · Add Session</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ :root {
10
+ --bg: #0d1117; --surface: #161b22; --border: #30363d;
11
+ --accent: #2f81f7; --accent2: #388bfd; --text: #e6edf3;
12
+ --muted: #8b949e; --success: #3fb950; --error: #f85149;
13
+ }
14
+ body {
15
+ background: var(--bg); color: var(--text);
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
17
+ min-height: 100vh; display: flex; align-items: center;
18
+ justify-content: center; padding: 24px;
19
+ }
20
+ .card {
21
+ background: var(--surface); border: 1px solid var(--border);
22
+ border-radius: 10px; padding: 36px 40px;
23
+ width: 100%; max-width: 460px; box-shadow: 0 8px 32px #0009;
24
+ }
25
+ .logo { display: flex; align-items: center; gap: 12px; margin-bottom: 28px; }
26
+ .logo svg { color: var(--accent); flex-shrink: 0; }
27
+ .logo h1 { font-size: 1.25rem; font-weight: 700; }
28
+ .logo span { color: var(--muted); font-weight: 400; }
29
+ .steps { display: flex; gap: 8px; margin-bottom: 28px; }
30
+ .step { height: 4px; flex: 1; background: var(--border); border-radius: 2px; transition: background .3s; }
31
+ .step.on { background: var(--accent); }
32
+ label {
33
+ display: block; font-size: .78rem; color: var(--muted);
34
+ text-transform: uppercase; letter-spacing: .06em;
35
+ margin-bottom: 6px; margin-top: 18px;
36
+ }
37
+ input {
38
+ width: 100%; background: var(--bg); border: 1px solid var(--border);
39
+ border-radius: 6px; color: var(--text); padding: 10px 14px;
40
+ font-size: .95rem; outline: none; transition: border-color .2s;
41
+ }
42
+ input:focus { border-color: var(--accent); }
43
+ button {
44
+ margin-top: 22px; width: 100%; background: var(--accent);
45
+ color: #fff; border: none; border-radius: 6px; padding: 11px;
46
+ font-size: .98rem; font-weight: 600; cursor: pointer;
47
+ transition: background .2s, opacity .2s;
48
+ }
49
+ button:hover { background: var(--accent2); }
50
+ button:disabled { opacity: .45; cursor: not-allowed; }
51
+ .msg {
52
+ margin-top: 14px; padding: 11px 14px; border-radius: 6px;
53
+ font-size: .88rem; display: none;
54
+ }
55
+ .msg.ok { background:#1a3a2a; border:1px solid #2d6040; color:var(--success); display:block; }
56
+ .msg.err { background:#3b1a1a; border:1px solid #7a2828; color:var(--error); display:block; }
57
+ .session-out {
58
+ margin-top: 12px; background: var(--bg); border: 1px solid var(--border);
59
+ border-radius: 6px; padding: 10px 14px; font-size: .72rem;
60
+ font-family: monospace; color: var(--muted); word-break: break-all;
61
+ }
62
+ .hidden { display: none !important; }
63
+ footer { margin-top: 28px; font-size: .76rem; color: var(--muted); text-align: center; }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <div class="card">
68
+
69
+ <div class="logo">
70
+ <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
71
+ <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
72
+ </svg>
73
+ <h1>TG Store <span>· Add Session</span></h1>
74
+ </div>
75
+
76
+ <div class="steps">
77
+ <div class="step on" id="s1"></div>
78
+ <div class="step" id="s2"></div>
79
+ <div class="step" id="s3"></div>
80
+ </div>
81
+
82
+ <!-- Step 1 -->
83
+ <div id="p1">
84
+ <label>API ID</label>
85
+ <input id="apiId" type="number" placeholder="12345678" />
86
+ <label>API Hash</label>
87
+ <input id="apiHash" type="text" placeholder="0123456789abcdef..." />
88
+ <label>Phone Number (with country code)</label>
89
+ <input id="phone" type="tel" placeholder="+1 555 000 0000" />
90
+ <button id="btnCode">Send OTP →</button>
91
+ <div class="msg" id="m1"></div>
92
+ </div>
93
+
94
+ <!-- Step 2 -->
95
+ <div id="p2" class="hidden">
96
+ <p style="color:var(--muted);font-size:.88rem;line-height:1.5">
97
+ Check your Telegram app or SMS for the one-time code.
98
+ </p>
99
+ <label>OTP Code</label>
100
+ <input id="otp" type="text" placeholder="12345" maxlength="12" />
101
+ <div id="twoFAWrap" class="hidden">
102
+ <label>Two-Factor Password</label>
103
+ <input id="twoFA" type="password" placeholder="Cloud password" />
104
+ </div>
105
+ <button id="btnVerify">Verify &amp; Save →</button>
106
+ <div class="msg" id="m2"></div>
107
+ </div>
108
+
109
+ <!-- Step 3 -->
110
+ <div id="p3" class="hidden">
111
+ <p style="color:var(--success);font-weight:700;font-size:1.05rem">✓ Session saved!</p>
112
+ <p style="color:var(--muted);font-size:.88rem;margin-top:8px;line-height:1.5">
113
+ This account is now in the upload pool and will be used for file uploads.
114
+ </p>
115
+ <label style="margin-top:20px">String Session (store safely)</label>
116
+ <div class="session-out" id="sessionOut"></div>
117
+ <button onclick="location.reload()"
118
+ style="margin-top:18px;background:#21262d;border:1px solid var(--border)">
119
+ + Add Another Account
120
+ </button>
121
+ </div>
122
+
123
+ <footer>Powered by GramJS · Node.js · MongoDB</footer>
124
+ </div>
125
+
126
+ <script>
127
+ const $ = id => document.getElementById(id);
128
+
129
+ function setStep(n) {
130
+ ['p1','p2','p3'].forEach((id,i) => $(id).classList.toggle('hidden', i+1 !== n));
131
+ ['s1','s2','s3'].forEach((id,i) => $(id).classList.toggle('on', i < n));
132
+ }
133
+
134
+ function msg(id, text, type) {
135
+ const el = $(id);
136
+ el.textContent = text;
137
+ el.className = 'msg ' + (type === 'ok' ? 'ok' : 'err');
138
+ }
139
+
140
+ let pendingPhone = '';
141
+
142
+ $('btnCode').addEventListener('click', async () => {
143
+ const btn = $('btnCode');
144
+ btn.disabled = true; btn.textContent = 'Sending…';
145
+ msg('m1', '', '');
146
+
147
+ const body = {
148
+ apiId: $('apiId').value.trim(),
149
+ apiHash: $('apiHash').value.trim(),
150
+ phone: $('phone').value.trim(),
151
+ };
152
+ if (!body.apiId || !body.apiHash || !body.phone) {
153
+ msg('m1', 'All three fields are required.', 'err');
154
+ btn.disabled = false; btn.textContent = 'Send OTP →';
155
+ return;
156
+ }
157
+
158
+ try {
159
+ const r = await fetch('/strings/send-code', {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json' },
162
+ body: JSON.stringify(body),
163
+ });
164
+ const d = await r.json();
165
+ if (!r.ok) throw new Error(d.error);
166
+ pendingPhone = body.phone;
167
+ setStep(2);
168
+ } catch(e) {
169
+ msg('m1', e.message, 'err');
170
+ btn.disabled = false; btn.textContent = 'Send OTP →';
171
+ }
172
+ });
173
+
174
+ $('btnVerify').addEventListener('click', async () => {
175
+ const btn = $('btnVerify');
176
+ btn.disabled = true; btn.textContent = 'Verifying…';
177
+ msg('m2', '', '');
178
+
179
+ const body = { phone: pendingPhone, code: $('otp').value.trim() };
180
+ const pw = $('twoFA').value.trim();
181
+ if (pw) body.password = pw;
182
+
183
+ try {
184
+ const r = await fetch('/strings/verify', {
185
+ method: 'POST',
186
+ headers: { 'Content-Type': 'application/json' },
187
+ body: JSON.stringify(body),
188
+ });
189
+ const d = await r.json();
190
+ if (d.twoFA) {
191
+ $('twoFAWrap').classList.remove('hidden');
192
+ msg('m2', '2FA required — enter your cloud password above and try again.', 'err');
193
+ btn.disabled = false; btn.textContent = 'Verify & Save →';
194
+ return;
195
+ }
196
+ if (!r.ok) throw new Error(d.error);
197
+ $('sessionOut').textContent = d.session || '';
198
+ setStep(3);
199
+ } catch(e) {
200
+ msg('m2', e.message, 'err');
201
+ btn.disabled = false; btn.textContent = 'Verify & Save →';
202
+ }
203
+ });
204
+ </script>
205
+ </body>
206
+ </html>