supertelegram 0.1.3 → 0.3.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.
package/README.md CHANGED
@@ -8,41 +8,63 @@ telegram cli for ai to read/write messages using gramjs.
8
8
  npm install -g supertelegram
9
9
  ```
10
10
 
11
- ## setup
11
+ ## quick start
12
12
 
13
- create `.env` file in your current directory:
14
- ```bash
15
- TELEGRAM_APP_ID="your_app_id"
16
- TELEGRAM_APP_HASH="your_app_hash"
17
- ```
18
-
19
- get credentials from https://my.telegram.org/apps
20
-
21
- ## login
13
+ just run login and follow the prompts:
22
14
 
23
15
  ```bash
24
16
  telegram login
25
17
  ```
26
18
 
27
- session is saved to `session.txt` in current directory.
19
+ if you don't have API credentials configured, it will:
20
+ 1. prompt you to get them from https://my.telegram.org/apps
21
+ 2. ask for your app_id and app_hash
22
+ 3. save them to `~/.supertelegram/config.json`
23
+ 4. continue with telegram phone/code login
24
+ 5. save session to `~/.supertelegram/session.txt`
25
+
26
+ that's it! now you're ready to use telegram from the cli.
28
27
 
29
28
  ## usage
30
29
 
30
+ ### messages
31
+
31
32
  ```bash
32
33
  # send a message
33
- telegram send <username> <message>
34
+ telegram send @username "hello there"
34
35
 
35
- # read messages
36
- telegram read <username> [limit]
36
+ # read messages (shows [photo], [video], [file] indicators)
37
+ telegram read @username 10
37
38
 
38
39
  # reply to latest message
39
- telegram reply <username> <message>
40
+ telegram reply "John" "hey back!"
40
41
 
41
- # get unread messages (json output)
42
- telegram unread [limit]
42
+ # get unread messages (json output with media info)
43
+ telegram unread 20
43
44
 
44
45
  # list dialogs
45
- telegram dialogs [limit]
46
+ telegram dialogs 10
47
+ ```
48
+
49
+ ### media (images/videos/files)
50
+
51
+ ```bash
52
+ # send a file with optional caption
53
+ telegram send-file @username photo.jpg
54
+ telegram send-file @username video.mp4 "check this out!"
55
+
56
+ # download media from a message
57
+ # (use 'read' command to get message IDs)
58
+ telegram download @username 12345 ./downloaded.jpg
59
+ telegram download @username 12346 # auto-named file
60
+ ```
61
+
62
+ ### config
63
+
64
+ manually set API credentials (optional):
65
+ ```bash
66
+ telegram config set appId "12345678"
67
+ telegram config set appHash "abc123..."
46
68
  ```
47
69
 
48
70
  ### flags
@@ -51,6 +73,27 @@ telegram dialogs [limit]
51
73
  - `--help` - show help
52
74
  - `--version` - show version
53
75
 
76
+ ### advanced
77
+
78
+ **multi-account / custom session location:**
79
+ ```bash
80
+ # use env var
81
+ TELEGRAM_SESSION=./custom.txt telegram send @friend "hey"
82
+
83
+ # or pass flag (todo)
84
+ telegram send @friend "hey" --session ./custom.txt
85
+ ```
86
+
87
+ **precedence for API credentials:**
88
+ 1. `TELEGRAM_APP_ID` and `TELEGRAM_APP_HASH` env vars
89
+ 2. `~/.supertelegram/config.json` (global)
90
+ 3. `.env` file in current directory (for dev)
91
+
92
+ **precedence for session file:**
93
+ 1. `TELEGRAM_SESSION` env var
94
+ 2. `~/.supertelegram/session.txt` (global default)
95
+ 3. `./session.txt` (backwards compat)
96
+
54
97
  ## development
55
98
 
56
99
  clone repo and install:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supertelegram",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "telegram cli for humans and bots",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -6,8 +6,12 @@ import {
6
6
  disconnect,
7
7
  getClient,
8
8
  login as telegramLogin,
9
+ sendFile as telegramSendFile,
10
+ downloadMedia as telegramDownloadMedia,
9
11
  } from "../client/telegram";
10
- import { askPhoneNumber, askPhoneCode, askPassword } from "./prompts";
12
+ import { askPhoneNumber, askPhoneCode, askPassword, askAppId, askAppHash } from "./prompts";
13
+ import { getApiCredentials, setConfig } from "../config/manager";
14
+ import { Api } from "telegram";
11
15
 
12
16
  export async function send(username: string, message: string) {
13
17
  if (!(await isLoggedIn())) {
@@ -20,6 +24,29 @@ export async function send(username: string, message: string) {
20
24
  await disconnect();
21
25
  }
22
26
 
27
+ function getMediaInfo(msg: Api.Message): string {
28
+ if (!msg.media) return "";
29
+
30
+ if (msg.media instanceof Api.MessageMediaPhoto) {
31
+ return " [photo]";
32
+ }
33
+ if (msg.media instanceof Api.MessageMediaDocument) {
34
+ const doc = msg.media.document;
35
+ if (doc instanceof Api.Document) {
36
+ const attrs = doc.attributes;
37
+ const filenameAttr = attrs.find((a) => "fileName" in a && a.fileName);
38
+ const filename = filenameAttr && "fileName" in filenameAttr ? filenameAttr.fileName : undefined;
39
+ const isVideo = attrs.some((a) => a instanceof Api.DocumentAttributeVideo);
40
+ const isAudio = attrs.some((a) => a instanceof Api.DocumentAttributeAudio);
41
+
42
+ if (isVideo) return ` [video${filename ? `: ${filename}` : ""}]`;
43
+ if (isAudio) return ` [audio${filename ? `: ${filename}` : ""}]`;
44
+ return ` [file${filename ? `: ${filename}` : ""}]`;
45
+ }
46
+ }
47
+ return " [media]";
48
+ }
49
+
23
50
  export async function read(username: string, limit = 10) {
24
51
  if (!(await isLoggedIn())) {
25
52
  console.error("not logged in. run: telegram login");
@@ -30,7 +57,8 @@ export async function read(username: string, limit = 10) {
30
57
  for (const msg of messages.reverse()) {
31
58
  const sender = msg.senderId?.toString() ?? "unknown";
32
59
  const date = msg.date ? new Date(msg.date * 1000).toISOString() : "";
33
- console.log(`[${date}] [${sender}]: ${msg.message}`);
60
+ const mediaInfo = getMediaInfo(msg);
61
+ console.log(`[${date}] [${sender}]: ${msg.message}${mediaInfo}`);
34
62
  }
35
63
  await disconnect();
36
64
  }
@@ -87,6 +115,7 @@ export async function unread(limit = 20) {
87
115
  messages: fromOthers.map((m) => ({
88
116
  id: m.id,
89
117
  text: m.message,
118
+ media: m.media ? getMediaInfo(m).trim() : null,
90
119
  date: m.date ? new Date(m.date * 1000).toISOString() : null,
91
120
  })),
92
121
  });
@@ -124,6 +153,18 @@ export async function reply(chatName: string, message: string) {
124
153
  }
125
154
 
126
155
  export async function login() {
156
+ // check if API credentials are configured
157
+ const creds = getApiCredentials();
158
+ if (!creds) {
159
+ console.log("no API credentials found. let's set them up first.");
160
+ const appId = await askAppId();
161
+ const appHash = await askAppHash();
162
+
163
+ setConfig("appId", appId);
164
+ setConfig("appHash", appHash);
165
+ console.log("credentials saved to ~/.supertelegram/config.json\n");
166
+ }
167
+
127
168
  const loggedIn = await isLoggedIn();
128
169
  if (loggedIn) {
129
170
  console.log("already logged in!");
@@ -141,3 +182,66 @@ export async function login() {
141
182
  console.log("login complete!");
142
183
  await disconnect();
143
184
  }
185
+
186
+ export async function config(action?: string, key?: string, value?: string) {
187
+ if (action === "set" && key && value) {
188
+ setConfig(key, value);
189
+ console.log(`set ${key} = ${value}`);
190
+ return;
191
+ }
192
+
193
+ if (action === "get" && key) {
194
+ const cfg = getApiCredentials();
195
+ if (key === "appId" && cfg) {
196
+ console.log(cfg.appId);
197
+ } else if (key === "appHash" && cfg) {
198
+ console.log(cfg.appHash);
199
+ } else {
200
+ console.log("not found");
201
+ }
202
+ return;
203
+ }
204
+
205
+ console.log("usage:");
206
+ console.log(" telegram config set appId <id>");
207
+ console.log(" telegram config set appHash <hash>");
208
+ console.log(" telegram config get appId");
209
+ }
210
+
211
+ export async function sendFile(username: string, filePath: string, caption?: string) {
212
+ if (!(await isLoggedIn())) {
213
+ console.error("not logged in. run: telegram login");
214
+ process.exit(1);
215
+ }
216
+
217
+ const result = await telegramSendFile(username, filePath, caption);
218
+ console.log(`sent file id: ${result.id}`);
219
+ await disconnect();
220
+ }
221
+
222
+ export async function downloadMedia(username: string, messageId: number, outputPath?: string) {
223
+ if (!(await isLoggedIn())) {
224
+ console.error("not logged in. run: telegram login");
225
+ process.exit(1);
226
+ }
227
+
228
+ const messages = await getMessages(username, 100);
229
+ const msg = messages.find((m) => m.id === messageId);
230
+
231
+ if (!msg) {
232
+ console.error(`message ${messageId} not found`);
233
+ await disconnect();
234
+ process.exit(1);
235
+ }
236
+
237
+ if (!msg.media) {
238
+ console.error(`message ${messageId} has no media`);
239
+ await disconnect();
240
+ process.exit(1);
241
+ }
242
+
243
+ console.log("downloading media...");
244
+ const path = await telegramDownloadMedia(msg, outputPath);
245
+ console.log(`saved to: ${path || outputPath || "unknown"}`);
246
+ await disconnect();
247
+ }
@@ -23,3 +23,12 @@ export async function askUsername(): Promise<string> {
23
23
  export async function askCommand(): Promise<string> {
24
24
  return input.text("command (send/read/dialogs/quit):");
25
25
  }
26
+
27
+ export async function askAppId(): Promise<string> {
28
+ console.log("\nget your API credentials from: https://my.telegram.org/apps\n");
29
+ return input.text("enter TELEGRAM_APP_ID:");
30
+ }
31
+
32
+ export async function askAppHash(): Promise<string> {
33
+ return input.text("enter TELEGRAM_APP_HASH:");
34
+ }
package/src/cli/run.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
- import { send, read, dialogs, unread, reply, login } from "./commands";
3
- import { setVerbose } from "../client/telegram";
2
+ import { send, read, dialogs, unread, reply, login, config, sendFile, downloadMedia } from "./commands";
3
+ import { setVerbose, setSessionPath } from "../client/telegram";
4
4
 
5
5
  const VERSION = "0.1.0";
6
6
  const NAME = "telegram";
@@ -12,12 +12,15 @@ usage:
12
12
  ${NAME} <command> [options]
13
13
 
14
14
  commands:
15
- send <chat> <message> send a message to a chat
16
- read <chat> [limit] read messages from a chat (default: 10)
17
- reply <chat> <message> reply to a chat by name (partial match)
18
- dialogs [limit] list recent dialogs (default: 10)
19
- unread [limit] show unread messages as json (default: 20)
20
- login authenticate with telegram (run separately)
15
+ send <chat> <message> send a message to a chat
16
+ send-file <chat> <path> send a file/image/video (optional: caption)
17
+ read <chat> [limit] read messages from a chat (default: 10)
18
+ download <chat> <id> [out] download media from message id
19
+ reply <chat> <message> reply to a chat by name (partial match)
20
+ dialogs [limit] list recent dialogs (default: 10)
21
+ unread [limit] show unread messages as json (default: 20)
22
+ login authenticate with telegram
23
+ config set <key> <val> set API credentials (appId, appHash)
21
24
 
22
25
  options:
23
26
  -v, --verbose show debug logs
@@ -26,7 +29,9 @@ options:
26
29
 
27
30
  examples:
28
31
  ${NAME} send @username "hello there"
32
+ ${NAME} send-file @username photo.jpg "check this out"
29
33
  ${NAME} read @username 5
34
+ ${NAME} download @username 12345 ./photo.jpg
30
35
  ${NAME} reply "John" "hey!"
31
36
  ${NAME} unread
32
37
  ${NAME} dialogs 20
@@ -56,7 +61,7 @@ if (verbose) {
56
61
  async function main() {
57
62
  switch (command) {
58
63
  case "send":
59
- if (rest.length < 2) {
64
+ if (rest.length < 2 || !rest[0]) {
60
65
  console.error("usage: telegram send <chat> <message>");
61
66
  process.exit(1);
62
67
  }
@@ -64,7 +69,7 @@ async function main() {
64
69
  break;
65
70
 
66
71
  case "read":
67
- if (rest.length < 1) {
72
+ if (rest.length < 1 || !rest[0]) {
68
73
  console.error("usage: telegram read <chat> [limit]");
69
74
  process.exit(1);
70
75
  }
@@ -80,17 +85,37 @@ async function main() {
80
85
  break;
81
86
 
82
87
  case "reply":
83
- if (rest.length < 2) {
88
+ if (rest.length < 2 || !rest[0]) {
84
89
  console.error("usage: telegram reply <chat> <message>");
85
90
  process.exit(1);
86
91
  }
87
92
  await reply(rest[0], rest.slice(1).join(" "));
88
93
  break;
89
94
 
95
+ case "send-file":
96
+ if (rest.length < 2 || !rest[0] || !rest[1]) {
97
+ console.error("usage: telegram send-file <chat> <path> [caption]");
98
+ process.exit(1);
99
+ }
100
+ await sendFile(rest[0], rest[1], rest.slice(2).join(" ") || undefined);
101
+ break;
102
+
103
+ case "download":
104
+ if (rest.length < 2 || !rest[0] || !rest[1]) {
105
+ console.error("usage: telegram download <chat> <message-id> [output-path]");
106
+ process.exit(1);
107
+ }
108
+ await downloadMedia(rest[0], Number.parseInt(rest[1]), rest[2]);
109
+ break;
110
+
90
111
  case "login":
91
112
  await login();
92
113
  break;
93
114
 
115
+ case "config":
116
+ await config(rest[0], rest[1], rest[2]);
117
+ break;
118
+
94
119
  case "help":
95
120
  console.log(HELP);
96
121
  break;
@@ -3,6 +3,7 @@ import { StringSession } from "telegram/sessions";
3
3
  import { Logger } from "telegram/extensions/Logger";
4
4
  import type { LogLevel } from "telegram/extensions/Logger";
5
5
  import { loadSession, saveSession } from "../session/storage";
6
+ import { getApiCredentials } from "../config/manager";
6
7
 
7
8
  let verbose = false;
8
9
 
@@ -11,25 +12,38 @@ export function setVerbose(v: boolean) {
11
12
  }
12
13
 
13
14
  class SilentLogger extends Logger {
14
- log(_level: LogLevel, _message: string, _color: string): void {
15
+ override log(_level: LogLevel, _message: string, _color: string): void {
15
16
  if (verbose) {
16
17
  super.log(_level, _message, _color);
17
18
  }
18
19
  }
19
20
  }
20
21
 
21
- const API_ID = Number(process.env.TELEGRAM_APP_ID);
22
- const API_HASH = process.env.TELEGRAM_APP_HASH ?? "";
23
-
24
22
  let client: TelegramClient | null = null;
23
+ let customSessionPath: string | undefined;
24
+
25
+ export function setSessionPath(path: string) {
26
+ customSessionPath = path;
27
+ }
25
28
 
26
29
  export async function getClient(): Promise<TelegramClient> {
27
30
  if (client) return client;
28
31
 
29
- const sessionStr = loadSession();
32
+ const creds = getApiCredentials();
33
+ if (!creds) {
34
+ throw new Error(
35
+ "telegram API credentials not found.\n" +
36
+ "get them from: https://my.telegram.org/apps\n" +
37
+ "then run: telegram config set appId <id>\n" +
38
+ " telegram config set appHash <hash>\n" +
39
+ "or set env vars: TELEGRAM_APP_ID, TELEGRAM_APP_HASH"
40
+ );
41
+ }
42
+
43
+ const sessionStr = loadSession(customSessionPath);
30
44
  const session = new StringSession(sessionStr);
31
45
 
32
- client = new TelegramClient(session, API_ID, API_HASH, {
46
+ client = new TelegramClient(session, creds.appId, creds.appHash, {
33
47
  connectionRetries: 5,
34
48
  baseLogger: new SilentLogger(),
35
49
  });
@@ -58,7 +72,7 @@ export async function login(callbacks: {
58
72
  });
59
73
 
60
74
  const sessionStr = c.session.save() as unknown as string;
61
- saveSession(sessionStr);
75
+ saveSession(sessionStr, customSessionPath);
62
76
  console.log("session saved!");
63
77
  }
64
78
 
@@ -70,6 +84,34 @@ export async function sendMessage(
70
84
  return c.sendMessage(username, { message });
71
85
  }
72
86
 
87
+ export async function sendFile(
88
+ username: string,
89
+ filePath: string,
90
+ caption?: string
91
+ ): Promise<Api.Message> {
92
+ const c = await getClient();
93
+ return c.sendFile(username, {
94
+ file: filePath,
95
+ caption: caption,
96
+ });
97
+ }
98
+
99
+ export async function downloadMedia(
100
+ message: Api.Message,
101
+ outputPath?: string
102
+ ): Promise<string | undefined> {
103
+ const c = await getClient();
104
+ if (!message.media) {
105
+ return undefined;
106
+ }
107
+
108
+ const buffer = await c.downloadMedia(message, {
109
+ outputFile: outputPath,
110
+ });
111
+
112
+ return buffer as string | undefined;
113
+ }
114
+
73
115
  export async function getMessages(
74
116
  username: string,
75
117
  limit = 10
@@ -0,0 +1,95 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ const CONFIG_DIR = join(homedir(), ".supertelegram");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+ const SESSION_FILE = join(CONFIG_DIR, "session.txt");
8
+
9
+ export interface Config {
10
+ appId?: string;
11
+ appHash?: string;
12
+ }
13
+
14
+ function ensureConfigDir() {
15
+ if (!existsSync(CONFIG_DIR)) {
16
+ mkdirSync(CONFIG_DIR, { recursive: true });
17
+ }
18
+ }
19
+
20
+ export function getConfig(): Config {
21
+ ensureConfigDir();
22
+
23
+ if (!existsSync(CONFIG_FILE)) {
24
+ return {};
25
+ }
26
+
27
+ try {
28
+ const content = readFileSync(CONFIG_FILE, "utf-8");
29
+ return JSON.parse(content);
30
+ } catch (err) {
31
+ console.error("failed to read config:", err);
32
+ return {};
33
+ }
34
+ }
35
+
36
+ export function setConfig(key: string, value: string) {
37
+ ensureConfigDir();
38
+
39
+ const config = getConfig();
40
+ config[key as keyof Config] = value;
41
+
42
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
43
+ }
44
+
45
+ export function getSessionPath(customPath?: string): string {
46
+ // precedence:
47
+ // 1. custom path from flag
48
+ // 2. TELEGRAM_SESSION env var
49
+ // 3. global ~/.supertelegram/session.txt
50
+ // 4. local ./session.txt (backwards compat)
51
+
52
+ if (customPath) {
53
+ return customPath;
54
+ }
55
+
56
+ if (process.env.TELEGRAM_SESSION) {
57
+ return process.env.TELEGRAM_SESSION;
58
+ }
59
+
60
+ // check if local session.txt exists (backwards compat)
61
+ if (existsSync("./session.txt")) {
62
+ return "./session.txt";
63
+ }
64
+
65
+ return SESSION_FILE;
66
+ }
67
+
68
+ export function getApiCredentials(): { appId: number; appHash: string } | null {
69
+ // precedence:
70
+ // 1. env vars
71
+ // 2. global config
72
+ // 3. local .env (handled by process.env already)
73
+
74
+ const envAppId = process.env.TELEGRAM_APP_ID;
75
+ const envAppHash = process.env.TELEGRAM_APP_HASH;
76
+
77
+ if (envAppId && envAppHash) {
78
+ return {
79
+ appId: Number.parseInt(envAppId),
80
+ appHash: envAppHash,
81
+ };
82
+ }
83
+
84
+ const config = getConfig();
85
+ if (config.appId && config.appHash) {
86
+ return {
87
+ appId: Number.parseInt(config.appId),
88
+ appHash: config.appHash,
89
+ };
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ export { CONFIG_DIR, CONFIG_FILE, SESSION_FILE };
@@ -1,14 +1,24 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { getSessionPath } from "../config/manager";
2
4
 
3
- const SESSION_FILE = "./session.txt";
4
-
5
- export function loadSession(): string {
6
- if (existsSync(SESSION_FILE)) {
7
- return readFileSync(SESSION_FILE, "utf-8").trim();
5
+ export function loadSession(customPath?: string): string {
6
+ const sessionFile = getSessionPath(customPath);
7
+
8
+ if (existsSync(sessionFile)) {
9
+ return readFileSync(sessionFile, "utf-8").trim();
8
10
  }
9
11
  return "";
10
12
  }
11
13
 
12
- export function saveSession(session: string): void {
13
- writeFileSync(SESSION_FILE, session, "utf-8");
14
+ export function saveSession(session: string, customPath?: string): void {
15
+ const sessionFile = getSessionPath(customPath);
16
+
17
+ // ensure directory exists
18
+ const dir = dirname(sessionFile);
19
+ if (!existsSync(dir)) {
20
+ mkdirSync(dir, { recursive: true });
21
+ }
22
+
23
+ writeFileSync(sessionFile, session, "utf-8");
14
24
  }