@nightowne/tas-cli 2.0.0 → 2.1.0

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.
@@ -6,6 +6,7 @@
6
6
  import TelegramBot from 'node-telegram-bot-api';
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
+ import { pipeline } from 'stream/promises';
9
10
 
10
11
  export class TelegramClient {
11
12
  constructor(dataDir) {
@@ -18,8 +19,12 @@ export class TelegramClient {
18
19
  * Initialize with bot token
19
20
  * Get token from @BotFather on Telegram
20
21
  */
21
- async initialize(token) {
22
- this.bot = new TelegramBot(token, { polling: false });
22
+ async initialize(token, customApiUrl = null) {
23
+ const options = { polling: false };
24
+ if (customApiUrl) {
25
+ options.baseApiUrl = customApiUrl;
26
+ }
27
+ this.bot = new TelegramBot(token, options);
23
28
 
24
29
  // Verify the token works
25
30
  try {
@@ -75,30 +80,43 @@ export class TelegramClient {
75
80
  * Send a file to the storage chat
76
81
  * Telegram supports up to 2GB for documents!
77
82
  */
78
- async sendFile(filePath, caption = '') {
83
+ async sendFile(filePath, caption = '', options = {}) {
79
84
  if (!this.chatId) {
80
85
  throw new Error('Chat ID not set. Run init first.');
81
86
  }
82
87
 
83
- const fileStream = fs.createReadStream(filePath);
84
- const filename = path.basename(filePath);
88
+ if (!fs.existsSync(filePath)) {
89
+ throw new Error(`File not found: ${filePath}`);
90
+ }
85
91
 
86
- const message = await this.bot.sendDocument(this.chatId, fileStream, {
87
- caption: caption
88
- }, {
89
- filename: filename,
90
- contentType: 'application/octet-stream'
91
- });
92
+ try {
93
+ let fileStream = fs.createReadStream(filePath);
94
+ const filename = path.basename(filePath);
95
+
96
+ if (options.limitRate) {
97
+ const { Throttle } = await import('../utils/throttle.js');
98
+ fileStream = fileStream.pipe(new Throttle(options.limitRate));
99
+ }
100
+
101
+ const message = await this.bot.sendDocument(this.chatId, fileStream, {
102
+ caption: caption
103
+ }, {
104
+ filename: filename,
105
+ contentType: 'application/octet-stream'
106
+ });
92
107
 
93
- return {
94
- messageId: message.message_id,
95
- fileId: message.document.file_id,
96
- timestamp: message.date
97
- };
108
+ return {
109
+ messageId: message.message_id,
110
+ fileId: message.document.file_id,
111
+ timestamp: message.date
112
+ };
113
+ } catch (err) {
114
+ throw new Error(`Failed to upload to Telegram: ${err.message}`);
115
+ }
98
116
  }
99
117
 
100
118
  /**
101
- * Download a file from Telegram
119
+ * Download a file from Telegram (In-Memory buffer)
102
120
  */
103
121
  async downloadFile(fileId) {
104
122
  // Get file path from Telegram servers
@@ -116,6 +134,16 @@ export class TelegramClient {
116
134
  return Buffer.concat(chunks);
117
135
  }
118
136
 
137
+ /**
138
+ * Efficiently download a file from Telegram straight to disk
139
+ */
140
+ async downloadFileToPath(fileId, destPath) {
141
+ const fileStream = await this.bot.getFileStream(fileId);
142
+ const writeStream = fs.createWriteStream(destPath);
143
+ await pipeline(fileStream, writeStream);
144
+ return destPath;
145
+ }
146
+
119
147
  /**
120
148
  * Delete a message (optional cleanup)
121
149
  */
@@ -91,7 +91,17 @@ export function createHeader(filename, originalSize, chunkIndex, totalChunks, fl
91
91
  offset += 2;
92
92
 
93
93
  // Filename length
94
- const filenameBytes = Buffer.from(filename, 'utf-8').subarray(0, 42);
94
+ let filenameBytes = Buffer.from(filename, 'utf-8');
95
+ if (filenameBytes.length > 42) {
96
+ // We carefully truncate by chars instead of bytes to avoid splitting a UTF-8 character in half!
97
+ let truncated = filename;
98
+ while (Buffer.from(truncated, 'utf-8').length > 42) {
99
+ // Remove one character at a time from the end
100
+ truncated = truncated.slice(0, -1);
101
+ }
102
+ filenameBytes = Buffer.from(truncated, 'utf-8');
103
+ }
104
+
95
105
  header.writeUInt16LE(filenameBytes.length, offset);
96
106
  offset += 2;
97
107
 
@@ -8,6 +8,7 @@ import path from 'path';
8
8
 
9
9
  const gzip = promisify(zlib.gzip);
10
10
  const gunzip = promisify(zlib.gunzip);
11
+ import { PassThrough } from 'stream';
11
12
 
12
13
  // File extensions that are already compressed (skip compression for these)
13
14
  const SKIP_COMPRESSION = new Set([
@@ -71,6 +72,24 @@ export class Compressor {
71
72
  }
72
73
  }
73
74
 
75
+ /**
76
+ * Get a compression transform stream
77
+ * Returns: { stream: Transform, compressed: boolean }
78
+ */
79
+ getCompressStream(filename = '') {
80
+ if (this.shouldSkip(filename)) {
81
+ return {
82
+ stream: new PassThrough(),
83
+ compressed: false
84
+ };
85
+ }
86
+
87
+ return {
88
+ stream: zlib.createGzip({ level: 6 }),
89
+ compressed: true
90
+ };
91
+ }
92
+
74
93
  /**
75
94
  * Decompress gzip data
76
95
  */
@@ -81,4 +100,15 @@ export class Compressor {
81
100
 
82
101
  return await gunzip(data);
83
102
  }
103
+
104
+ /**
105
+ * Get a decompression transform stream
106
+ */
107
+ getDecompressStream(wasCompressed) {
108
+ if (!wasCompressed) {
109
+ return new PassThrough();
110
+ }
111
+
112
+ return zlib.createGunzip();
113
+ }
84
114
  }
@@ -0,0 +1,26 @@
1
+ import { Transform } from 'stream';
2
+
3
+ export class Throttle extends Transform {
4
+ constructor(bytesPerSecond) {
5
+ super();
6
+ this.bytesPerSecond = bytesPerSecond;
7
+ this.passed = 0;
8
+ this.startTime = Date.now();
9
+ }
10
+
11
+ _transform(chunk, encoding, callback) {
12
+ this.passed += chunk.length;
13
+ const elapsed = Date.now() - this.startTime;
14
+ const expectedTime = (this.passed / this.bytesPerSecond) * 1000;
15
+
16
+ if (expectedTime > elapsed) {
17
+ setTimeout(() => {
18
+ this.push(chunk);
19
+ callback();
20
+ }, expectedTime - elapsed);
21
+ } else {
22
+ this.push(chunk);
23
+ callback();
24
+ }
25
+ }
26
+ }