supertelegram 0.3.2 → 0.5.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/.github/workflows/publish.yml +52 -0
- package/PUBLISHING.md +18 -1
- package/README.md +42 -11
- package/package.json +9 -1
- package/src/cli/commands.ts +112 -14
- package/src/cli/run.ts +48 -5
- package/src/client/telegram.ts +53 -3
- package/src/config/accounts.ts +113 -0
- package/src/config/manager.ts +13 -6
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# publishes to npm when you push a version tag (e.g. v0.5.0) or cut a GitHub release.
|
|
4
|
+
# tag the current version with: git tag v$(node -p "require('./package.json').version") && git push --tags
|
|
5
|
+
# auth is via npm OIDC trusted publishing (no NPM_TOKEN) — configure a trusted
|
|
6
|
+
# publisher for this package on npmjs.com pointing at this repo + workflow.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags:
|
|
11
|
+
- "v*"
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
publish:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
id-token: write # enables npm provenance
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: oven-sh/setup-bun@v2
|
|
26
|
+
with:
|
|
27
|
+
bun-version: latest
|
|
28
|
+
|
|
29
|
+
- name: install deps
|
|
30
|
+
run: bun install --frozen-lockfile
|
|
31
|
+
|
|
32
|
+
- uses: actions/setup-node@v4
|
|
33
|
+
with:
|
|
34
|
+
node-version: 24
|
|
35
|
+
registry-url: "https://registry.npmjs.org"
|
|
36
|
+
|
|
37
|
+
# OIDC trusted publishing needs a recent npm (node 24 ships one, but pin latest)
|
|
38
|
+
- name: upgrade npm
|
|
39
|
+
run: npm install -g npm@latest
|
|
40
|
+
|
|
41
|
+
- name: guard — tag matches package.json version
|
|
42
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
43
|
+
run: |
|
|
44
|
+
PKG_VERSION="v$(node -p "require('./package.json').version")"
|
|
45
|
+
if [ "$PKG_VERSION" != "${GITHUB_REF_NAME}" ]; then
|
|
46
|
+
echo "::error::tag ${GITHUB_REF_NAME} does not match package.json ($PKG_VERSION)"
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# no NODE_AUTH_TOKEN: npm authenticates via OIDC (id-token: write above)
|
|
51
|
+
- name: publish
|
|
52
|
+
run: npm publish --access public --provenance
|
package/PUBLISHING.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# publishing guide
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## automated (github actions) — preferred
|
|
4
|
+
|
|
5
|
+
CI publishes to npm on every version tag. auth is via **OIDC trusted
|
|
6
|
+
publishing** — no `NPM_TOKEN` secret. one-time setup: on npmjs.com, open the
|
|
7
|
+
package → Settings → Trusted Publishing, and add a GitHub Actions publisher
|
|
8
|
+
pointing at `caffeinum/supertelegram`, workflow `publish.yml`.
|
|
9
|
+
|
|
10
|
+
then to release:
|
|
11
|
+
```bash
|
|
12
|
+
# bump version in package.json first, commit, then:
|
|
13
|
+
git tag "v$(node -p "require('./package.json').version")"
|
|
14
|
+
git push --tags
|
|
15
|
+
```
|
|
16
|
+
the `.github/workflows/publish.yml` workflow checks the tag matches
|
|
17
|
+
package.json, installs, and runs `npm publish --access public --provenance`.
|
|
18
|
+
you can also trigger it manually from the Actions tab (workflow_dispatch).
|
|
19
|
+
|
|
20
|
+
## manual setup
|
|
4
21
|
|
|
5
22
|
1. login to npm:
|
|
6
23
|
```bash
|
package/README.md
CHANGED
|
@@ -32,9 +32,11 @@ that's it! now you're ready to use telegram from the cli.
|
|
|
32
32
|
### messages
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
# send a message
|
|
35
|
+
# send a message — chat can be @username, a numeric id, or a name
|
|
36
36
|
telegram send @username "hello there"
|
|
37
|
-
telegram send
|
|
37
|
+
telegram send 7069934904 "hey" # by id (for chats with no username)
|
|
38
|
+
telegram send "Ben YC" "hello" # by name (partial, case-insensitive)
|
|
39
|
+
telegram send "me" "note to self" # use "me" for saved messages
|
|
38
40
|
|
|
39
41
|
# read messages (shows [photo], [video], [file] indicators)
|
|
40
42
|
telegram read @username 10
|
|
@@ -45,7 +47,7 @@ telegram reply "John" "hey back!"
|
|
|
45
47
|
# get unread messages (json output with media info)
|
|
46
48
|
telegram unread 20
|
|
47
49
|
|
|
48
|
-
# list dialogs
|
|
50
|
+
# list dialogs (shows @username and [id: ...] for each chat)
|
|
49
51
|
telegram dialogs 10
|
|
50
52
|
```
|
|
51
53
|
|
|
@@ -76,15 +78,42 @@ telegram config set appHash "abc123..."
|
|
|
76
78
|
- `--help` - show help
|
|
77
79
|
- `--version` - show version
|
|
78
80
|
|
|
81
|
+
### multiple accounts
|
|
82
|
+
|
|
83
|
+
log in to as many accounts as you want, each stored under a name, and switch
|
|
84
|
+
between them:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# log into named accounts (prompts phone/code the first time)
|
|
88
|
+
telegram login personal
|
|
89
|
+
telegram login work
|
|
90
|
+
|
|
91
|
+
# see them (* marks the active one)
|
|
92
|
+
telegram accounts
|
|
93
|
+
# * work — @yourworkhandle
|
|
94
|
+
# personal — @yourhandle
|
|
95
|
+
|
|
96
|
+
# switch the active account (all later commands use it)
|
|
97
|
+
telegram switch personal
|
|
98
|
+
telegram whoami # personal — @yourhandle
|
|
99
|
+
|
|
100
|
+
# or run a single command as another account without switching
|
|
101
|
+
telegram -a work send @boss "on it"
|
|
102
|
+
|
|
103
|
+
# remove an account
|
|
104
|
+
telegram logout work
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
sessions live in `~/.supertelegram/accounts/<name>.txt`; the active account is
|
|
108
|
+
tracked in `~/.supertelegram/accounts.json`. API credentials (appId/appHash)
|
|
109
|
+
are shared across accounts. upgrading from an older version? your existing
|
|
110
|
+
login is migrated automatically into an account named `default`.
|
|
111
|
+
|
|
79
112
|
### advanced
|
|
80
113
|
|
|
81
|
-
**
|
|
114
|
+
**custom session location (one-off / scripting):**
|
|
82
115
|
```bash
|
|
83
|
-
# use env var
|
|
84
116
|
TELEGRAM_SESSION=./custom.txt telegram send @friend "hey"
|
|
85
|
-
|
|
86
|
-
# or pass flag (todo)
|
|
87
|
-
telegram send @friend "hey" --session ./custom.txt
|
|
88
117
|
```
|
|
89
118
|
|
|
90
119
|
**precedence for API credentials:**
|
|
@@ -93,9 +122,11 @@ telegram send @friend "hey" --session ./custom.txt
|
|
|
93
122
|
3. `.env` file in current directory (for dev)
|
|
94
123
|
|
|
95
124
|
**precedence for session file:**
|
|
96
|
-
1.
|
|
97
|
-
2.
|
|
98
|
-
3.
|
|
125
|
+
1. `--account <name>` flag
|
|
126
|
+
2. `TELEGRAM_SESSION` env var
|
|
127
|
+
3. active account (`~/.supertelegram/accounts/<name>.txt`)
|
|
128
|
+
4. `./session.txt` (backwards compat)
|
|
129
|
+
5. `~/.supertelegram/session.txt` (legacy default)
|
|
99
130
|
|
|
100
131
|
## development
|
|
101
132
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supertelegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "telegram cli for humans and bots",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/caffeinum/supertelegram.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/caffeinum/supertelegram#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/caffeinum/supertelegram/issues"
|
|
14
|
+
},
|
|
7
15
|
"bin": {
|
|
8
16
|
"telegram": "src/cli/run.ts"
|
|
9
17
|
},
|
package/src/cli/commands.ts
CHANGED
|
@@ -11,6 +11,15 @@ import {
|
|
|
11
11
|
} from "../client/telegram";
|
|
12
12
|
import { askPhoneNumber, askPhoneCode, askPassword, askAppId, askAppHash } from "./prompts";
|
|
13
13
|
import { getApiCredentials, setConfig } from "../config/manager";
|
|
14
|
+
import {
|
|
15
|
+
listAccounts,
|
|
16
|
+
setCurrentAccount,
|
|
17
|
+
registerAccount,
|
|
18
|
+
removeAccount,
|
|
19
|
+
getCurrentAccount,
|
|
20
|
+
accountSessionPath,
|
|
21
|
+
} from "../config/accounts";
|
|
22
|
+
import { setSessionPath } from "../client/telegram";
|
|
14
23
|
import { Api } from "telegram";
|
|
15
24
|
|
|
16
25
|
export async function send(username: string, message: string) {
|
|
@@ -71,8 +80,16 @@ export async function dialogs(limit = 10) {
|
|
|
71
80
|
|
|
72
81
|
const dialogList = await getDialogs(limit);
|
|
73
82
|
for (const dialog of dialogList) {
|
|
74
|
-
|
|
83
|
+
const entity = dialog.entity;
|
|
84
|
+
const username =
|
|
85
|
+
entity && "username" in entity && entity.username
|
|
86
|
+
? ` @${entity.username}`
|
|
87
|
+
: "";
|
|
88
|
+
const id = dialog.id?.toString() ?? entity?.id?.toString() ?? "?";
|
|
89
|
+
const unread = dialog.unreadCount > 0 ? ` (${dialog.unreadCount} unread)` : "";
|
|
90
|
+
console.log(`- ${dialog.title}${username} [id: ${id}]${unread}`);
|
|
75
91
|
}
|
|
92
|
+
console.log("\nsend with: telegram send <@username|id> \"your message\"");
|
|
76
93
|
await disconnect();
|
|
77
94
|
}
|
|
78
95
|
|
|
@@ -152,37 +169,118 @@ export async function reply(chatName: string, message: string) {
|
|
|
152
169
|
await disconnect();
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
export async function login() {
|
|
156
|
-
|
|
172
|
+
export async function login(accountName?: string) {
|
|
173
|
+
const name = accountName || getCurrentAccount() || "default";
|
|
174
|
+
// isolate this login to the named account's own session file
|
|
175
|
+
setSessionPath(accountSessionPath(name));
|
|
176
|
+
|
|
177
|
+
// check if API credentials are configured (shared across accounts)
|
|
157
178
|
const creds = getApiCredentials();
|
|
158
179
|
if (!creds) {
|
|
159
180
|
console.log("no API credentials found. let's set them up first.");
|
|
160
181
|
const appId = await askAppId();
|
|
161
182
|
const appHash = await askAppHash();
|
|
162
|
-
|
|
183
|
+
|
|
163
184
|
setConfig("appId", appId);
|
|
164
185
|
setConfig("appHash", appHash);
|
|
165
186
|
console.log("credentials saved to ~/.supertelegram/config.json\n");
|
|
166
187
|
}
|
|
167
188
|
|
|
168
189
|
const loggedIn = await isLoggedIn();
|
|
169
|
-
if (loggedIn) {
|
|
170
|
-
console.log(
|
|
171
|
-
await
|
|
172
|
-
|
|
190
|
+
if (!loggedIn) {
|
|
191
|
+
console.log(`starting login for account "${name}"...`);
|
|
192
|
+
await telegramLogin({
|
|
193
|
+
phoneNumber: askPhoneNumber,
|
|
194
|
+
phoneCode: askPhoneCode,
|
|
195
|
+
password: askPassword,
|
|
196
|
+
});
|
|
197
|
+
} else {
|
|
198
|
+
console.log(`account "${name}" already has a session, refreshing details...`);
|
|
173
199
|
}
|
|
174
200
|
|
|
175
|
-
|
|
176
|
-
await
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
// capture identity + register (and make current)
|
|
202
|
+
const client = await getClient();
|
|
203
|
+
const me = await client.getMe();
|
|
204
|
+
const fullName = [me.firstName, me.lastName].filter(Boolean).join(" ");
|
|
205
|
+
registerAccount(name, {
|
|
206
|
+
username: me.username ?? undefined,
|
|
207
|
+
userId: me.id?.toString(),
|
|
208
|
+
name: fullName || undefined,
|
|
180
209
|
});
|
|
181
210
|
|
|
182
|
-
|
|
211
|
+
const label = me.username ? `@${me.username}` : fullName || name;
|
|
212
|
+
console.log(`logged in as ${label} — account "${name}" is now active`);
|
|
183
213
|
await disconnect();
|
|
184
214
|
}
|
|
185
215
|
|
|
216
|
+
export async function accounts() {
|
|
217
|
+
const list = listAccounts();
|
|
218
|
+
if (list.length === 0) {
|
|
219
|
+
console.log("no accounts yet. run: telegram login <name>");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const { name, meta, current } of list) {
|
|
223
|
+
const marker = current ? "*" : " ";
|
|
224
|
+
const who = meta.username
|
|
225
|
+
? `@${meta.username}`
|
|
226
|
+
: meta.name || (meta.userId ? `id ${meta.userId}` : "");
|
|
227
|
+
console.log(`${marker} ${name}${who ? ` — ${who}` : ""}`);
|
|
228
|
+
}
|
|
229
|
+
console.log("\nswitch with: telegram switch <name>");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function switchAccount(name?: string) {
|
|
233
|
+
if (!name) {
|
|
234
|
+
console.error("usage: telegram switch <name>");
|
|
235
|
+
console.error("see accounts with: telegram accounts");
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
setCurrentAccount(name);
|
|
239
|
+
console.log(`switched to account "${name}"`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function logout(name?: string) {
|
|
243
|
+
const target = name || getCurrentAccount();
|
|
244
|
+
if (!target) {
|
|
245
|
+
console.error("no account to log out. see: telegram accounts");
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
removeAccount(target);
|
|
249
|
+
const now = getCurrentAccount();
|
|
250
|
+
console.log(
|
|
251
|
+
`logged out of "${target}"` + (now ? `. active account is now "${now}"` : ". no accounts left")
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function whoami() {
|
|
256
|
+
const current = getCurrentAccount();
|
|
257
|
+
if (!current) {
|
|
258
|
+
console.log("not logged in. run: telegram login");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let meta = listAccounts().find((a) => a.name === current)?.meta;
|
|
263
|
+
|
|
264
|
+
// backfill identity for accounts migrated without metadata
|
|
265
|
+
if (meta && !meta.username && !meta.name && (await isLoggedIn())) {
|
|
266
|
+
const client = await getClient();
|
|
267
|
+
const me = await client.getMe();
|
|
268
|
+
const fullName = [me.firstName, me.lastName].filter(Boolean).join(" ");
|
|
269
|
+
meta = {
|
|
270
|
+
username: me.username ?? undefined,
|
|
271
|
+
userId: me.id?.toString(),
|
|
272
|
+
name: fullName || undefined,
|
|
273
|
+
};
|
|
274
|
+
registerAccount(current, meta, false);
|
|
275
|
+
await disconnect();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const who = meta?.username
|
|
279
|
+
? `@${meta.username}`
|
|
280
|
+
: meta?.name || (meta?.userId ? `id ${meta.userId}` : "unknown");
|
|
281
|
+
console.log(`${current} — ${who}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
186
284
|
export async function config(action?: string, key?: string, value?: string) {
|
|
187
285
|
if (action === "set" && key && value) {
|
|
188
286
|
setConfig(key, value);
|
package/src/cli/run.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { send, read, dialogs, unread, reply, login, config, sendFile, downloadMedia } from "./commands";
|
|
2
|
+
import { send, read, dialogs, unread, reply, login, config, sendFile, downloadMedia, accounts, switchAccount, logout, whoami } from "./commands";
|
|
3
3
|
import { setVerbose, setSessionPath } from "../client/telegram";
|
|
4
|
+
import { migrateLegacyIfNeeded, accountSessionPath } from "../config/accounts";
|
|
5
|
+
import pkg from "../../package.json";
|
|
4
6
|
|
|
5
|
-
const VERSION =
|
|
7
|
+
const VERSION = pkg.version;
|
|
6
8
|
const NAME = "telegram";
|
|
7
9
|
|
|
8
10
|
const HELP = `
|
|
@@ -12,17 +14,22 @@ usage:
|
|
|
12
14
|
${NAME} <command> [options]
|
|
13
15
|
|
|
14
16
|
commands:
|
|
15
|
-
send <chat> <message> send a message
|
|
17
|
+
send <chat> <message> send a message (chat = @username, id, or name)
|
|
16
18
|
send-file <chat> <path> send a file/image/video (optional: caption)
|
|
17
19
|
read <chat> [limit] read messages from a chat (default: 10)
|
|
18
20
|
download <chat> <id> [out] download media from message id
|
|
19
21
|
reply <chat> <message> reply to a chat by name (partial match)
|
|
20
22
|
dialogs [limit] list recent dialogs (default: 10)
|
|
21
23
|
unread [limit] show unread messages as json (default: 20)
|
|
22
|
-
login
|
|
24
|
+
login [name] authenticate with telegram (into a named account)
|
|
25
|
+
accounts list logged-in accounts (* = current)
|
|
26
|
+
switch <name> switch the active account
|
|
27
|
+
whoami show the active account
|
|
28
|
+
logout [name] remove an account (default: current)
|
|
23
29
|
config set <key> <val> set API credentials (appId, appHash)
|
|
24
30
|
|
|
25
31
|
options:
|
|
32
|
+
-a, --account <name> run this command as a specific account
|
|
26
33
|
-v, --verbose show debug logs
|
|
27
34
|
-h, --help show this help
|
|
28
35
|
--version show version
|
|
@@ -35,6 +42,9 @@ examples:
|
|
|
35
42
|
${NAME} reply "John" "hey!"
|
|
36
43
|
${NAME} unread
|
|
37
44
|
${NAME} dialogs 20
|
|
45
|
+
${NAME} login work # log into a second account named "work"
|
|
46
|
+
${NAME} switch work # make it active
|
|
47
|
+
${NAME} -a personal unread # run one command as another account
|
|
38
48
|
`.trim();
|
|
39
49
|
|
|
40
50
|
const rawArgs = process.argv.slice(2);
|
|
@@ -51,6 +61,15 @@ if (rawArgs.includes("--version")) {
|
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
const verbose = rawArgs.includes("--verbose") || rawArgs.includes("-v");
|
|
64
|
+
|
|
65
|
+
// pull out --account/-a <name> before positional parsing
|
|
66
|
+
let accountFlag: string | undefined;
|
|
67
|
+
const accIdx = rawArgs.findIndex((a) => a === "--account" || a === "-a");
|
|
68
|
+
if (accIdx !== -1) {
|
|
69
|
+
accountFlag = rawArgs[accIdx + 1];
|
|
70
|
+
rawArgs.splice(accIdx, accountFlag ? 2 : 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
const args = rawArgs.filter((a) => !a.startsWith("-"));
|
|
55
74
|
const [command, ...rest] = args;
|
|
56
75
|
|
|
@@ -58,6 +77,14 @@ if (verbose) {
|
|
|
58
77
|
setVerbose(true);
|
|
59
78
|
}
|
|
60
79
|
|
|
80
|
+
// fold a pre-multi-account session.txt into account "default" (one-time, no-op after)
|
|
81
|
+
migrateLegacyIfNeeded();
|
|
82
|
+
|
|
83
|
+
// an explicit --account pins this invocation to that account's session
|
|
84
|
+
if (accountFlag) {
|
|
85
|
+
setSessionPath(accountSessionPath(accountFlag));
|
|
86
|
+
}
|
|
87
|
+
|
|
61
88
|
async function main() {
|
|
62
89
|
switch (command) {
|
|
63
90
|
case "send":
|
|
@@ -109,7 +136,23 @@ async function main() {
|
|
|
109
136
|
break;
|
|
110
137
|
|
|
111
138
|
case "login":
|
|
112
|
-
await login();
|
|
139
|
+
await login(rest[0]);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case "accounts":
|
|
143
|
+
await accounts();
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "switch":
|
|
147
|
+
await switchAccount(rest[0]);
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case "whoami":
|
|
151
|
+
await whoami();
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case "logout":
|
|
155
|
+
await logout(rest[0]);
|
|
113
156
|
break;
|
|
114
157
|
|
|
115
158
|
case "config":
|
package/src/client/telegram.ts
CHANGED
|
@@ -76,12 +76,60 @@ export async function login(callbacks: {
|
|
|
76
76
|
console.log("session saved!");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// a chat can be addressed by @username, "me", phone (+123), t.me link,
|
|
80
|
+
// or a numeric id copied from `telegram dialogs`. usernames/links resolve
|
|
81
|
+
// natively; numeric ids and bare names need the dialog entity cache, so we
|
|
82
|
+
// look them up here.
|
|
83
|
+
export async function resolveEntity(
|
|
84
|
+
identifier: string
|
|
85
|
+
): Promise<string | Api.TypeEntityLike> {
|
|
86
|
+
const c = await getClient();
|
|
87
|
+
const id = identifier.trim();
|
|
88
|
+
|
|
89
|
+
// things gramjs resolves on its own
|
|
90
|
+
if (
|
|
91
|
+
id === "me" ||
|
|
92
|
+
id.startsWith("@") ||
|
|
93
|
+
id.startsWith("+") ||
|
|
94
|
+
id.startsWith("http")
|
|
95
|
+
) {
|
|
96
|
+
return id;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// numeric id (users positive, chats/channels may be negative)
|
|
100
|
+
if (/^-?\d+$/.test(id)) {
|
|
101
|
+
try {
|
|
102
|
+
// fast path: entity already in the session cache
|
|
103
|
+
return await c.getInputEntity(id);
|
|
104
|
+
} catch {
|
|
105
|
+
// slow path: fetch dialogs to populate + match the entity
|
|
106
|
+
const match = (await c.getDialogs({ limit: 200 })).find(
|
|
107
|
+
(d) => d.id?.toString() === id || d.entity?.id?.toString() === id
|
|
108
|
+
);
|
|
109
|
+
if (match?.entity) return match.entity;
|
|
110
|
+
throw new Error(
|
|
111
|
+
`chat with id ${id} not found in your dialogs. run: telegram dialogs 200`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// bare name: partial, case-insensitive match against dialog titles
|
|
117
|
+
const match = (await c.getDialogs({ limit: 200 })).find((d) =>
|
|
118
|
+
d.title?.toLowerCase().includes(id.toLowerCase())
|
|
119
|
+
);
|
|
120
|
+
if (match?.entity) return match.entity;
|
|
121
|
+
|
|
122
|
+
// let gramjs take a final shot (e.g. a username without the @)
|
|
123
|
+
return id;
|
|
124
|
+
}
|
|
125
|
+
|
|
79
126
|
export async function sendMessage(
|
|
80
127
|
username: string,
|
|
81
128
|
message: string
|
|
82
129
|
): Promise<Api.Message> {
|
|
83
130
|
const c = await getClient();
|
|
84
|
-
|
|
131
|
+
const entity = await resolveEntity(username);
|
|
132
|
+
return c.sendMessage(entity, { message });
|
|
85
133
|
}
|
|
86
134
|
|
|
87
135
|
export async function sendFile(
|
|
@@ -90,7 +138,8 @@ export async function sendFile(
|
|
|
90
138
|
caption?: string
|
|
91
139
|
): Promise<Api.Message> {
|
|
92
140
|
const c = await getClient();
|
|
93
|
-
|
|
141
|
+
const entity = await resolveEntity(username);
|
|
142
|
+
return c.sendFile(entity, {
|
|
94
143
|
file: filePath,
|
|
95
144
|
caption: caption,
|
|
96
145
|
});
|
|
@@ -117,7 +166,8 @@ export async function getMessages(
|
|
|
117
166
|
limit = 10
|
|
118
167
|
): Promise<Api.Message[]> {
|
|
119
168
|
const c = await getClient();
|
|
120
|
-
const
|
|
169
|
+
const entity = await resolveEntity(username);
|
|
170
|
+
const messages = await c.getMessages(entity, { limit });
|
|
121
171
|
return messages as Api.Message[];
|
|
122
172
|
}
|
|
123
173
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
// kept in sync with manager.ts; recomputed here to avoid a circular import
|
|
13
|
+
const CONFIG_DIR = join(homedir(), ".supertelegram");
|
|
14
|
+
const ACCOUNTS_DIR = join(CONFIG_DIR, "accounts");
|
|
15
|
+
const ACCOUNTS_FILE = join(CONFIG_DIR, "accounts.json");
|
|
16
|
+
const LEGACY_SESSION_FILE = join(CONFIG_DIR, "session.txt");
|
|
17
|
+
|
|
18
|
+
export interface AccountMeta {
|
|
19
|
+
username?: string;
|
|
20
|
+
userId?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AccountsRegistry {
|
|
25
|
+
current?: string;
|
|
26
|
+
accounts: Record<string, AccountMeta>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readRegistry(): AccountsRegistry {
|
|
30
|
+
if (!existsSync(ACCOUNTS_FILE)) return { accounts: {} };
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(ACCOUNTS_FILE, "utf-8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return { accounts: {} };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeRegistry(reg: AccountsRegistry): void {
|
|
39
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
40
|
+
writeFileSync(ACCOUNTS_FILE, JSON.stringify(reg, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function accountSessionPath(name: string): string {
|
|
44
|
+
return join(ACCOUNTS_DIR, `${name}.txt`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getCurrentAccount(): string | undefined {
|
|
48
|
+
return readRegistry().current;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function listAccounts(): {
|
|
52
|
+
name: string;
|
|
53
|
+
meta: AccountMeta;
|
|
54
|
+
current: boolean;
|
|
55
|
+
}[] {
|
|
56
|
+
const reg = readRegistry();
|
|
57
|
+
return Object.entries(reg.accounts).map(([name, meta]) => ({
|
|
58
|
+
name,
|
|
59
|
+
meta,
|
|
60
|
+
current: name === reg.current,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function setCurrentAccount(name: string): void {
|
|
65
|
+
const reg = readRegistry();
|
|
66
|
+
if (!reg.accounts[name]) {
|
|
67
|
+
const known = Object.keys(reg.accounts);
|
|
68
|
+
throw new Error(
|
|
69
|
+
`account "${name}" not found.` +
|
|
70
|
+
(known.length ? ` known accounts: ${known.join(", ")}` : " run: telegram login <name>")
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
reg.current = name;
|
|
74
|
+
writeRegistry(reg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerAccount(
|
|
78
|
+
name: string,
|
|
79
|
+
meta: AccountMeta,
|
|
80
|
+
makeCurrent = true
|
|
81
|
+
): void {
|
|
82
|
+
if (!existsSync(ACCOUNTS_DIR)) mkdirSync(ACCOUNTS_DIR, { recursive: true });
|
|
83
|
+
const reg = readRegistry();
|
|
84
|
+
reg.accounts[name] = meta;
|
|
85
|
+
if (makeCurrent) reg.current = name;
|
|
86
|
+
writeRegistry(reg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function removeAccount(name: string): void {
|
|
90
|
+
const reg = readRegistry();
|
|
91
|
+
if (!reg.accounts[name]) {
|
|
92
|
+
throw new Error(`account "${name}" not found`);
|
|
93
|
+
}
|
|
94
|
+
delete reg.accounts[name];
|
|
95
|
+
if (reg.current === name) reg.current = Object.keys(reg.accounts)[0];
|
|
96
|
+
writeRegistry(reg);
|
|
97
|
+
|
|
98
|
+
const sessionFile = accountSessionPath(name);
|
|
99
|
+
if (existsSync(sessionFile)) rmSync(sessionFile);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// one-time migration: fold a pre-multi-account session.txt into account "default"
|
|
103
|
+
// so existing installs keep their login after upgrading.
|
|
104
|
+
export function migrateLegacyIfNeeded(): void {
|
|
105
|
+
const reg = readRegistry();
|
|
106
|
+
if (Object.keys(reg.accounts).length > 0) return; // already on the accounts system
|
|
107
|
+
|
|
108
|
+
if (existsSync(LEGACY_SESSION_FILE) && readFileSync(LEGACY_SESSION_FILE, "utf-8").trim()) {
|
|
109
|
+
if (!existsSync(ACCOUNTS_DIR)) mkdirSync(ACCOUNTS_DIR, { recursive: true });
|
|
110
|
+
copyFileSync(LEGACY_SESSION_FILE, accountSessionPath("default"));
|
|
111
|
+
registerAccount("default", {}, true);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/config/manager.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { getCurrentAccount, accountSessionPath } from "./accounts";
|
|
4
5
|
|
|
5
6
|
const CONFIG_DIR = join(homedir(), ".supertelegram");
|
|
6
7
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
@@ -44,24 +45,30 @@ export function setConfig(key: string, value: string) {
|
|
|
44
45
|
|
|
45
46
|
export function getSessionPath(customPath?: string): string {
|
|
46
47
|
// precedence:
|
|
47
|
-
// 1. custom path from flag
|
|
48
|
+
// 1. custom path from flag (--account resolves to one of these)
|
|
48
49
|
// 2. TELEGRAM_SESSION env var
|
|
49
|
-
// 3.
|
|
50
|
+
// 3. current account's session (~/.supertelegram/accounts/<name>.txt)
|
|
50
51
|
// 4. local ./session.txt (backwards compat)
|
|
51
|
-
|
|
52
|
+
// 5. global ~/.supertelegram/session.txt (legacy default)
|
|
53
|
+
|
|
52
54
|
if (customPath) {
|
|
53
55
|
return customPath;
|
|
54
56
|
}
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
if (process.env.TELEGRAM_SESSION) {
|
|
57
59
|
return process.env.TELEGRAM_SESSION;
|
|
58
60
|
}
|
|
59
|
-
|
|
61
|
+
|
|
62
|
+
const current = getCurrentAccount();
|
|
63
|
+
if (current) {
|
|
64
|
+
return accountSessionPath(current);
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
// check if local session.txt exists (backwards compat)
|
|
61
68
|
if (existsSync("./session.txt")) {
|
|
62
69
|
return "./session.txt";
|
|
63
70
|
}
|
|
64
|
-
|
|
71
|
+
|
|
65
72
|
return SESSION_FILE;
|
|
66
73
|
}
|
|
67
74
|
|