koztv-blog-tools 1.2.16 → 1.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tg-login.js +203 -0
- package/dist/index.js +47 -13
- package/dist/index.mjs +47 -13
- package/package.json +3 -2
package/bin/tg-login.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* QR code login for Telegram
|
|
4
|
+
* Run this first to create session, then use tg-export
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* tg-login
|
|
8
|
+
* tg-login --session-file ./my-session
|
|
9
|
+
*
|
|
10
|
+
* Environment variables:
|
|
11
|
+
* TELEGRAM_API_ID - API ID from https://my.telegram.org
|
|
12
|
+
* TELEGRAM_API_HASH - API Hash
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { TelegramClient, Api } = require('telegram');
|
|
16
|
+
const { StringSession } = require('telegram/sessions/index.js');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const readline = require('readline');
|
|
20
|
+
|
|
21
|
+
// Parse arguments
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const getArg = (name) => {
|
|
24
|
+
const idx = args.indexOf(`--${name}`);
|
|
25
|
+
return idx !== -1 ? args[idx + 1] : null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const API_ID = parseInt(process.env.TELEGRAM_API_ID || '0', 10);
|
|
29
|
+
const API_HASH = process.env.TELEGRAM_API_HASH || '';
|
|
30
|
+
const SESSION_FILE = getArg('session-file') || './.telegram-session';
|
|
31
|
+
|
|
32
|
+
if (!API_ID || !API_HASH) {
|
|
33
|
+
console.error('Error: TELEGRAM_API_ID and TELEGRAM_API_HASH required');
|
|
34
|
+
console.error('Get them from https://my.telegram.org');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Load existing session if any
|
|
39
|
+
let savedSession = '';
|
|
40
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
41
|
+
savedSession = fs.readFileSync(SESSION_FILE, 'utf-8').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function prompt(question) {
|
|
45
|
+
const rl = readline.createInterface({
|
|
46
|
+
input: process.stdin,
|
|
47
|
+
output: process.stdout,
|
|
48
|
+
});
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
rl.question(question, (answer) => {
|
|
51
|
+
rl.close();
|
|
52
|
+
resolve(answer);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main() {
|
|
58
|
+
console.log('Telegram QR Login');
|
|
59
|
+
console.log('==================\n');
|
|
60
|
+
|
|
61
|
+
const stringSession = new StringSession(savedSession);
|
|
62
|
+
const client = new TelegramClient(stringSession, API_ID, API_HASH, {
|
|
63
|
+
connectionRetries: 5,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await client.connect();
|
|
67
|
+
|
|
68
|
+
// Check if already authorized
|
|
69
|
+
if (await client.isUserAuthorized()) {
|
|
70
|
+
const me = await client.getMe();
|
|
71
|
+
console.log(`Already logged in as: ${me.firstName} (@${me.username})`);
|
|
72
|
+
|
|
73
|
+
const session = client.session.save();
|
|
74
|
+
fs.writeFileSync(SESSION_FILE, session);
|
|
75
|
+
console.log(`Session saved to: ${SESSION_FILE}`);
|
|
76
|
+
|
|
77
|
+
await client.disconnect();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('Generating QR code for login...');
|
|
82
|
+
console.log('Scan with Telegram: Settings -> Devices -> Link Desktop Device\n');
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = await client.invoke(
|
|
86
|
+
new Api.auth.ExportLoginToken({
|
|
87
|
+
apiId: API_ID,
|
|
88
|
+
apiHash: API_HASH,
|
|
89
|
+
exceptIds: [],
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (result instanceof Api.auth.LoginToken) {
|
|
94
|
+
const tokenBase64 = Buffer.from(result.token).toString('base64url');
|
|
95
|
+
const qrUrl = `tg://login?token=${tokenBase64}`;
|
|
96
|
+
|
|
97
|
+
// Try to show QR in terminal
|
|
98
|
+
try {
|
|
99
|
+
const QRCode = require('qrcode');
|
|
100
|
+
const qrString = await QRCode.toString(qrUrl, { type: 'terminal', small: true });
|
|
101
|
+
console.log(qrString);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.log('Install qrcode package for terminal QR: npm install qrcode');
|
|
104
|
+
console.log(`\nURL: ${qrUrl}\n`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`Token expires in ${result.expires - Math.floor(Date.now() / 1000)} seconds`);
|
|
108
|
+
console.log('\nWaiting for scan...');
|
|
109
|
+
|
|
110
|
+
const startTime = Date.now();
|
|
111
|
+
const timeout = 180000; // 3 minutes
|
|
112
|
+
|
|
113
|
+
while (Date.now() - startTime < timeout) {
|
|
114
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const checkResult = await client.invoke(
|
|
118
|
+
new Api.auth.ExportLoginToken({
|
|
119
|
+
apiId: API_ID,
|
|
120
|
+
apiHash: API_HASH,
|
|
121
|
+
exceptIds: [],
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (checkResult instanceof Api.auth.LoginTokenSuccess) {
|
|
126
|
+
console.log('\nQR scanned!');
|
|
127
|
+
const me = await client.getMe();
|
|
128
|
+
console.log(`Logged in as: ${me.firstName} (@${me.username})`);
|
|
129
|
+
|
|
130
|
+
const session = client.session.save();
|
|
131
|
+
fs.writeFileSync(SESSION_FILE, session);
|
|
132
|
+
console.log(`Session saved to: ${SESSION_FILE}`);
|
|
133
|
+
|
|
134
|
+
await client.disconnect();
|
|
135
|
+
return;
|
|
136
|
+
} else if (checkResult instanceof Api.auth.LoginTokenMigrateTo) {
|
|
137
|
+
await client._switchDC(checkResult.dcId);
|
|
138
|
+
const importResult = await client.invoke(
|
|
139
|
+
new Api.auth.ImportLoginToken({ token: checkResult.token })
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (importResult instanceof Api.auth.LoginTokenSuccess) {
|
|
143
|
+
const me = await client.getMe();
|
|
144
|
+
console.log(`\nLogged in as: ${me.firstName} (@${me.username})`);
|
|
145
|
+
|
|
146
|
+
const session = client.session.save();
|
|
147
|
+
fs.writeFileSync(SESSION_FILE, session);
|
|
148
|
+
console.log(`Session saved to: ${SESSION_FILE}`);
|
|
149
|
+
|
|
150
|
+
await client.disconnect();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
if (e.message?.includes('SESSION_PASSWORD_NEEDED')) {
|
|
156
|
+
const password = await prompt('\n2FA password: ');
|
|
157
|
+
await client.invoke(
|
|
158
|
+
new Api.auth.CheckPassword({
|
|
159
|
+
password: await client.computeCheck(
|
|
160
|
+
await client.invoke(new Api.account.GetPassword()),
|
|
161
|
+
password
|
|
162
|
+
),
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const me = await client.getMe();
|
|
167
|
+
console.log(`Logged in as: ${me.firstName} (@${me.username})`);
|
|
168
|
+
|
|
169
|
+
const session = client.session.save();
|
|
170
|
+
fs.writeFileSync(SESSION_FILE, session);
|
|
171
|
+
console.log(`Session saved to: ${SESSION_FILE}`);
|
|
172
|
+
|
|
173
|
+
await client.disconnect();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log('\nTimeout - run again.');
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.log('\nFalling back to phone login...');
|
|
183
|
+
const phone = await prompt('Phone number: ');
|
|
184
|
+
|
|
185
|
+
await client.start({
|
|
186
|
+
phoneNumber: async () => phone,
|
|
187
|
+
phoneCode: async () => await prompt('Code: '),
|
|
188
|
+
password: async () => await prompt('2FA password: '),
|
|
189
|
+
onError: (err) => console.error('Error:', err),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const me = await client.getMe();
|
|
193
|
+
console.log(`\nLogged in as: ${me.firstName} (@${me.username})`);
|
|
194
|
+
|
|
195
|
+
const session = client.session.save();
|
|
196
|
+
fs.writeFileSync(SESSION_FILE, session);
|
|
197
|
+
console.log(`Session saved to: ${SESSION_FILE}`);
|
|
198
|
+
} finally {
|
|
199
|
+
await client.disconnect();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
main().catch(console.error);
|
package/dist/index.js
CHANGED
|
@@ -934,6 +934,10 @@ async function processPost(post, options, exportDir) {
|
|
|
934
934
|
const sourceLang = translate.sourceLang || "ru";
|
|
935
935
|
for (const targetLang of translate.targetLangs) {
|
|
936
936
|
if (targetLang === sourceLang) continue;
|
|
937
|
+
if (translationExistsInDir(targetLang)) {
|
|
938
|
+
onProgress?.(` Skipping ${targetLang} (already translated)`);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
937
941
|
onProgress?.(` Translating to ${targetLang}...`);
|
|
938
942
|
const translateOpts = {
|
|
939
943
|
apiKey: translate.apiKey,
|
|
@@ -963,24 +967,32 @@ async function processPost(post, options, exportDir) {
|
|
|
963
967
|
await new Promise((r) => setTimeout(r, 5e3));
|
|
964
968
|
}
|
|
965
969
|
if (translate.keepOriginal) {
|
|
966
|
-
|
|
970
|
+
if (translationExistsInDir(sourceLang)) {
|
|
971
|
+
onProgress?.(` Skipping ${sourceLang} (already exists)`);
|
|
972
|
+
} else {
|
|
973
|
+
const russianSlug = generateSlug2(originalTitle);
|
|
974
|
+
languages.push({
|
|
975
|
+
lang: sourceLang,
|
|
976
|
+
title: originalTitle,
|
|
977
|
+
body: originalBody,
|
|
978
|
+
isOriginal: true,
|
|
979
|
+
slug: russianSlug
|
|
980
|
+
// Custom slug for Russian URL
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
} else {
|
|
985
|
+
const defaultLang = translate?.sourceLang || "ru";
|
|
986
|
+
if (translationExistsInDir(defaultLang)) {
|
|
987
|
+
onProgress?.(` Skipping ${defaultLang} (already exists)`);
|
|
988
|
+
} else {
|
|
967
989
|
languages.push({
|
|
968
|
-
lang:
|
|
990
|
+
lang: defaultLang,
|
|
969
991
|
title: originalTitle,
|
|
970
992
|
body: originalBody,
|
|
971
|
-
isOriginal: true
|
|
972
|
-
slug: russianSlug
|
|
973
|
-
// Custom slug for Russian URL
|
|
993
|
+
isOriginal: true
|
|
974
994
|
});
|
|
975
995
|
}
|
|
976
|
-
} else {
|
|
977
|
-
const defaultLang = translate?.sourceLang || "ru";
|
|
978
|
-
languages.push({
|
|
979
|
-
lang: defaultLang,
|
|
980
|
-
title: originalTitle,
|
|
981
|
-
body: originalBody,
|
|
982
|
-
isOriginal: true
|
|
983
|
-
});
|
|
984
996
|
}
|
|
985
997
|
for (const { lang, title, body, isOriginal, slug: customSlug } of languages) {
|
|
986
998
|
const langFile = path2.join(postDir, `${lang}.md`);
|
|
@@ -1175,6 +1187,28 @@ async function exportAndTranslate(options) {
|
|
|
1175
1187
|
skipped++;
|
|
1176
1188
|
continue;
|
|
1177
1189
|
}
|
|
1190
|
+
const postDir = path2.join(outputDir, String(post.msgId));
|
|
1191
|
+
const neededLangs = [];
|
|
1192
|
+
if (options.translate && options.translate.targetLangs.length > 0) {
|
|
1193
|
+
const sourceLang = options.translate.sourceLang || "ru";
|
|
1194
|
+
neededLangs.push(...options.translate.targetLangs.filter((l) => l !== sourceLang));
|
|
1195
|
+
if (options.translate.keepOriginal) {
|
|
1196
|
+
neededLangs.push(sourceLang);
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
neededLangs.push(options.translate?.sourceLang || "ru");
|
|
1200
|
+
}
|
|
1201
|
+
const allExist = neededLangs.every((lang) => {
|
|
1202
|
+
const langFile = path2.join(postDir, `${lang}.md`);
|
|
1203
|
+
if (!fs2.existsSync(langFile)) return false;
|
|
1204
|
+
const content = fs2.readFileSync(langFile, "utf-8");
|
|
1205
|
+
return content.includes(`original_link: "${post.link}"`);
|
|
1206
|
+
});
|
|
1207
|
+
if (allExist) {
|
|
1208
|
+
onProgress?.(`Skipping fully processed: ${postId}`);
|
|
1209
|
+
skipped++;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1178
1212
|
onProgress?.(`
|
|
1179
1213
|
Processing: ${postId}`);
|
|
1180
1214
|
try {
|
package/dist/index.mjs
CHANGED
|
@@ -876,6 +876,10 @@ async function processPost(post, options, exportDir) {
|
|
|
876
876
|
const sourceLang = translate.sourceLang || "ru";
|
|
877
877
|
for (const targetLang of translate.targetLangs) {
|
|
878
878
|
if (targetLang === sourceLang) continue;
|
|
879
|
+
if (translationExistsInDir(targetLang)) {
|
|
880
|
+
onProgress?.(` Skipping ${targetLang} (already translated)`);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
879
883
|
onProgress?.(` Translating to ${targetLang}...`);
|
|
880
884
|
const translateOpts = {
|
|
881
885
|
apiKey: translate.apiKey,
|
|
@@ -905,24 +909,32 @@ async function processPost(post, options, exportDir) {
|
|
|
905
909
|
await new Promise((r) => setTimeout(r, 5e3));
|
|
906
910
|
}
|
|
907
911
|
if (translate.keepOriginal) {
|
|
908
|
-
|
|
912
|
+
if (translationExistsInDir(sourceLang)) {
|
|
913
|
+
onProgress?.(` Skipping ${sourceLang} (already exists)`);
|
|
914
|
+
} else {
|
|
915
|
+
const russianSlug = generateSlug2(originalTitle);
|
|
916
|
+
languages.push({
|
|
917
|
+
lang: sourceLang,
|
|
918
|
+
title: originalTitle,
|
|
919
|
+
body: originalBody,
|
|
920
|
+
isOriginal: true,
|
|
921
|
+
slug: russianSlug
|
|
922
|
+
// Custom slug for Russian URL
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
} else {
|
|
927
|
+
const defaultLang = translate?.sourceLang || "ru";
|
|
928
|
+
if (translationExistsInDir(defaultLang)) {
|
|
929
|
+
onProgress?.(` Skipping ${defaultLang} (already exists)`);
|
|
930
|
+
} else {
|
|
909
931
|
languages.push({
|
|
910
|
-
lang:
|
|
932
|
+
lang: defaultLang,
|
|
911
933
|
title: originalTitle,
|
|
912
934
|
body: originalBody,
|
|
913
|
-
isOriginal: true
|
|
914
|
-
slug: russianSlug
|
|
915
|
-
// Custom slug for Russian URL
|
|
935
|
+
isOriginal: true
|
|
916
936
|
});
|
|
917
937
|
}
|
|
918
|
-
} else {
|
|
919
|
-
const defaultLang = translate?.sourceLang || "ru";
|
|
920
|
-
languages.push({
|
|
921
|
-
lang: defaultLang,
|
|
922
|
-
title: originalTitle,
|
|
923
|
-
body: originalBody,
|
|
924
|
-
isOriginal: true
|
|
925
|
-
});
|
|
926
938
|
}
|
|
927
939
|
for (const { lang, title, body, isOriginal, slug: customSlug } of languages) {
|
|
928
940
|
const langFile = path2.join(postDir, `${lang}.md`);
|
|
@@ -1117,6 +1129,28 @@ async function exportAndTranslate(options) {
|
|
|
1117
1129
|
skipped++;
|
|
1118
1130
|
continue;
|
|
1119
1131
|
}
|
|
1132
|
+
const postDir = path2.join(outputDir, String(post.msgId));
|
|
1133
|
+
const neededLangs = [];
|
|
1134
|
+
if (options.translate && options.translate.targetLangs.length > 0) {
|
|
1135
|
+
const sourceLang = options.translate.sourceLang || "ru";
|
|
1136
|
+
neededLangs.push(...options.translate.targetLangs.filter((l) => l !== sourceLang));
|
|
1137
|
+
if (options.translate.keepOriginal) {
|
|
1138
|
+
neededLangs.push(sourceLang);
|
|
1139
|
+
}
|
|
1140
|
+
} else {
|
|
1141
|
+
neededLangs.push(options.translate?.sourceLang || "ru");
|
|
1142
|
+
}
|
|
1143
|
+
const allExist = neededLangs.every((lang) => {
|
|
1144
|
+
const langFile = path2.join(postDir, `${lang}.md`);
|
|
1145
|
+
if (!fs2.existsSync(langFile)) return false;
|
|
1146
|
+
const content = fs2.readFileSync(langFile, "utf-8");
|
|
1147
|
+
return content.includes(`original_link: "${post.link}"`);
|
|
1148
|
+
});
|
|
1149
|
+
if (allExist) {
|
|
1150
|
+
onProgress?.(`Skipping fully processed: ${postId}`);
|
|
1151
|
+
skipped++;
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1120
1154
|
onProgress?.(`
|
|
1121
1155
|
Processing: ${postId}`);
|
|
1122
1156
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koztv-blog-tools",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.18",
|
|
4
4
|
"description": "Shared utilities for Telegram-based blog sites",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"tg-export": "./bin/export-telegram.js"
|
|
16
|
+
"tg-export": "./bin/export-telegram.js",
|
|
17
|
+
"tg-login": "./bin/tg-login.js"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"dist",
|