dsh-session-tg-notify 0.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.
- package/README.md +151 -0
- package/cordis.patch.yml +6 -0
- package/package.json +52 -0
- package/src/client.js +777 -0
- package/src/config.js +207 -0
- package/src/events.js +308 -0
- package/src/index.js +392 -0
- package/src/presence.js +177 -0
- package/src/screenlock.js +72 -0
- package/src/telegram.js +162 -0
package/src/telegram.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-tg-notify — Telegram 通道(Bot API `sendMessage`)。
|
|
3
|
+
*
|
|
4
|
+
* 只依赖全局 fetch(Node >= 18),无第三方依赖。所有失败都被收敛成
|
|
5
|
+
* `{ ok: false, error }`,由调用方决定记日志还是回报给设置页 ——
|
|
6
|
+
* 通知失败绝不能影响 DSH 会话本身。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 把通知内容渲染成 Telegram 消息文本(HTML 模式,需转义)。 */
|
|
10
|
+
export function renderTelegramText(notification) {
|
|
11
|
+
const emoji = notification.emoji ?? '';
|
|
12
|
+
const title = notification.title ?? 'DSH 通知';
|
|
13
|
+
const body = notification.body ?? '';
|
|
14
|
+
const lines = [`${emoji} <b>${escapeHtml(title)}</b>`];
|
|
15
|
+
if (body) lines.push(escapeHtml(body));
|
|
16
|
+
if (notification.sessionLabel) lines.push(`<i>${escapeHtml(notification.sessionLabel)}</i>`);
|
|
17
|
+
return lines.join('\n');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function escapeHtml(text) {
|
|
21
|
+
return String(text).replace(/[&<>]/g, (ch) => ({ '&': '&', '<': '<', '>': '>' }[ch]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 发送一条 Telegram 消息。
|
|
26
|
+
* @param config - { botToken, chatId, apiBase }
|
|
27
|
+
* @param notification - { title, body, emoji?, sessionLabel? }
|
|
28
|
+
* @param fetchImpl - 可注入的 fetch(测试用)
|
|
29
|
+
* @returns { ok: true, messageId } 或 { ok: false, error }
|
|
30
|
+
*/
|
|
31
|
+
export async function sendTelegram(config, notification, fetchImpl = globalThis.fetch) {
|
|
32
|
+
const token = typeof config?.botToken === 'string' ? config.botToken.trim() : '';
|
|
33
|
+
const chatId = typeof config?.chatId === 'string' ? config.chatId.trim() : '';
|
|
34
|
+
if (token.length === 0) return { ok: false, error: '未配置 Telegram Bot Token' };
|
|
35
|
+
if (chatId.length === 0) return { ok: false, error: '未配置 Telegram Chat ID' };
|
|
36
|
+
if (typeof fetchImpl !== 'function') return { ok: false, error: '当前运行时没有 fetch' };
|
|
37
|
+
|
|
38
|
+
const base = (typeof config?.apiBase === 'string' && config.apiBase.trim().length > 0
|
|
39
|
+
? config.apiBase.trim()
|
|
40
|
+
: 'https://api.telegram.org').replace(/\/+$/, '');
|
|
41
|
+
const url = `${base}/bot${token}/sendMessage`;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetchImpl(url, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'content-type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
chat_id: chatId,
|
|
49
|
+
text: renderTelegramText(notification),
|
|
50
|
+
parse_mode: 'HTML',
|
|
51
|
+
disable_web_page_preview: true
|
|
52
|
+
}),
|
|
53
|
+
signal: AbortSignal.timeout(15000)
|
|
54
|
+
});
|
|
55
|
+
const payload = await response.json().catch(() => null);
|
|
56
|
+
if (!response.ok || payload?.ok !== true) {
|
|
57
|
+
const description = payload?.description ?? `HTTP ${response.status}`;
|
|
58
|
+
return { ok: false, error: description };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, messageId: payload?.result?.message_id ?? null };
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return { ok: false, error: error?.message ?? String(error) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 取 bot 身份,用于设置页的「测试连接」(同时校验 Token 是否有效)。 */
|
|
67
|
+
export async function getTelegramMe(config, fetchImpl = globalThis.fetch) {
|
|
68
|
+
const token = typeof config?.botToken === 'string' ? config.botToken.trim() : '';
|
|
69
|
+
if (token.length === 0) return { ok: false, error: '未配置 Telegram Bot Token' };
|
|
70
|
+
const base = (typeof config?.apiBase === 'string' && config.apiBase.trim().length > 0
|
|
71
|
+
? config.apiBase.trim()
|
|
72
|
+
: 'https://api.telegram.org').replace(/\/+$/, '');
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetchImpl(`${base}/bot${token}/getMe`, { signal: AbortSignal.timeout(15000) });
|
|
75
|
+
const payload = await response.json().catch(() => null);
|
|
76
|
+
if (!response.ok || payload?.ok !== true) {
|
|
77
|
+
return { ok: false, error: payload?.description ?? `HTTP ${response.status}` };
|
|
78
|
+
}
|
|
79
|
+
return { ok: true, username: payload.result?.username ?? null, name: payload.result?.first_name ?? null };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return { ok: false, error: error?.message ?? String(error) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 解析 Telegram 端点所需的 token 与 base(失败时给出可读错误)。 */
|
|
86
|
+
function resolveEndpoint(config) {
|
|
87
|
+
const token = typeof config?.botToken === 'string' ? config.botToken.trim() : '';
|
|
88
|
+
if (token.length === 0) return { error: '未配置 Telegram Bot Token' };
|
|
89
|
+
const base = (typeof config?.apiBase === 'string' && config.apiBase.trim().length > 0
|
|
90
|
+
? config.apiBase.trim()
|
|
91
|
+
: 'https://api.telegram.org').replace(/\/+$/, '');
|
|
92
|
+
return { token, base, url: `${base}/bot${token}` };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 把 Telegram 的 chat 对象压成设置页需要的形状。 */
|
|
96
|
+
function describeChat(chat) {
|
|
97
|
+
const name = chat.title
|
|
98
|
+
?? [chat.first_name, chat.last_name].filter(Boolean).join(' ')
|
|
99
|
+
?? (chat.username ? `@${chat.username}` : '');
|
|
100
|
+
return {
|
|
101
|
+
id: String(chat.id),
|
|
102
|
+
type: chat.type,
|
|
103
|
+
label: name.length > 0 ? name : String(chat.id),
|
|
104
|
+
username: chat.username ?? null
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 从 bot 的最近更新里发现可用的 chat —— 这就是获取 Chat ID 的正规途径。
|
|
110
|
+
*
|
|
111
|
+
* Telegram 的 bot **不能**主动发起会话:必须先由你给 bot 发过至少一条消息,
|
|
112
|
+
* `getUpdates` 才能看到那个 chat。本插件从不带 offset 调用,因此不会确认
|
|
113
|
+
* (消费)更新,历史消息始终可见。
|
|
114
|
+
*
|
|
115
|
+
* @returns { ok: true, chats: [{ id, type, label, username }] } 或 { ok: false, error }
|
|
116
|
+
*/
|
|
117
|
+
export async function getTelegramChats(config, fetchImpl = globalThis.fetch) {
|
|
118
|
+
const endpoint = resolveEndpoint(config);
|
|
119
|
+
if (endpoint.error) return { ok: false, error: endpoint.error };
|
|
120
|
+
if (typeof fetchImpl !== 'function') return { ok: false, error: '当前运行时没有 fetch' };
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetchImpl(`${endpoint.url}/getUpdates?limit=100&timeout=0`, {
|
|
123
|
+
signal: AbortSignal.timeout(15000)
|
|
124
|
+
});
|
|
125
|
+
const payload = await response.json().catch(() => null);
|
|
126
|
+
if (!response.ok || payload?.ok !== true) {
|
|
127
|
+
return { ok: false, error: payload?.description ?? `HTTP ${response.status}` };
|
|
128
|
+
}
|
|
129
|
+
const seen = new Map();
|
|
130
|
+
for (const update of payload.result ?? []) {
|
|
131
|
+
const chat = update?.message?.chat ?? update?.edited_message?.chat ?? update?.channel_post?.chat;
|
|
132
|
+
if (!chat || chat.id === undefined) continue;
|
|
133
|
+
seen.set(String(chat.id), describeChat(chat));
|
|
134
|
+
}
|
|
135
|
+
return { ok: true, chats: [...seen.values()] };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return { ok: false, error: error?.message ?? String(error) };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 校验 Chat ID 是否可达(`getChat`)。用于「测试连接」真正验证整条链路,
|
|
143
|
+
* 而不只是证明 token 有效。
|
|
144
|
+
*/
|
|
145
|
+
export async function getTelegramChat(config, chatId, fetchImpl = globalThis.fetch) {
|
|
146
|
+
const endpoint = resolveEndpoint(config);
|
|
147
|
+
if (endpoint.error) return { ok: false, error: endpoint.error };
|
|
148
|
+
const id = typeof chatId === 'string' ? chatId.trim() : String(chatId ?? '');
|
|
149
|
+
if (id.length === 0) return { ok: false, error: '未配置 Telegram Chat ID' };
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetchImpl(`${endpoint.url}/getChat?chat_id=${encodeURIComponent(id)}`, {
|
|
152
|
+
signal: AbortSignal.timeout(15000)
|
|
153
|
+
});
|
|
154
|
+
const payload = await response.json().catch(() => null);
|
|
155
|
+
if (!response.ok || payload?.ok !== true) {
|
|
156
|
+
return { ok: false, error: payload?.description ?? `HTTP ${response.status}` };
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, chat: describeChat(payload.result ?? {}) };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return { ok: false, error: error?.message ?? String(error) };
|
|
161
|
+
}
|
|
162
|
+
}
|