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.
@@ -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
- const russianSlug = generateSlug2(originalTitle);
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: sourceLang,
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
- const russianSlug = generateSlug2(originalTitle);
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: sourceLang,
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.16",
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",