@es-labs/jslib 0.0.1
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/CHANGELOG.md +4 -0
- package/README.md +42 -0
- package/__test__/services.test.js +32 -0
- package/auth/index.js +226 -0
- package/auth/keyv.js +23 -0
- package/auth/knex.js +29 -0
- package/auth/redis.js +23 -0
- package/comms/email.js +123 -0
- package/comms/nexmo.js +44 -0
- package/comms/telegram.js +43 -0
- package/comms/telegram2/inbound.js +314 -0
- package/comms/telegram2/outbound.js +574 -0
- package/comms/webpush.js +60 -0
- package/config.js +37 -0
- package/express/controller/auth/oauth.js +39 -0
- package/express/controller/auth/oidc.js +87 -0
- package/express/controller/auth/own.js +100 -0
- package/express/controller/auth/saml.js +74 -0
- package/express/upload.js +48 -0
- package/index.js +1 -0
- package/iso/README.md +4 -0
- package/iso/__tests__/csv-utils.spec.js +128 -0
- package/iso/__tests__/datetime.spec.js +101 -0
- package/iso/__tests__/fetch.spec.js +270 -0
- package/iso/csv-utils.js +206 -0
- package/iso/datetime.js +103 -0
- package/iso/fetch.js +129 -0
- package/iso/fetch2.js +180 -0
- package/iso/log-filter.js +17 -0
- package/iso/sleep.js +6 -0
- package/iso/ws.js +63 -0
- package/node/oss-files/oss-uploader-client-fetch.js +258 -0
- package/node/oss-files/oss-uploader-client-fetch.md +31 -0
- package/node/oss-files/oss-uploader-client.js +219 -0
- package/node/oss-files/oss-uploader-server.js +199 -0
- package/node/oss-files/oss-uploader-usage.js +121 -0
- package/node/oss-files/oss-uploader-usage.md +34 -0
- package/node/oss-files/s3-uploader-client.js +217 -0
- package/node/oss-files/s3-uploader-server.js +123 -0
- package/node/oss-files/s3-uploader-usage.js +77 -0
- package/node/oss-files/s3-uploader-usage.md +34 -0
- package/package.json +53 -0
- package/packageInfo.js +9 -0
- package/services/ali.js +279 -0
- package/services/aws.js +194 -0
- package/services/db/__tests__/keyv.spec.js +31 -0
- package/services/db/keyv.js +14 -0
- package/services/db/knex.js +67 -0
- package/services/db/redis.js +51 -0
- package/services/index.js +57 -0
- package/services/mq/README.md +8 -0
- package/services/websocket.js +139 -0
- package/t4t/README.md +1 -0
- package/traps.js +20 -0
- package/utils/__tests__/aes.spec.js +52 -0
- package/utils/aes.js +23 -0
- package/web/UI.md +71 -0
- package/web/bwc-autocomplete.js +211 -0
- package/web/bwc-combobox.js +343 -0
- package/web/bwc-fileupload.js +87 -0
- package/web/bwc-loading-overlay.js +54 -0
- package/web/bwc-t4t-form.js +511 -0
- package/web/bwc-table.js +756 -0
- package/web/fetch.js +129 -0
- package/web/i18n.js +24 -0
- package/web/idle.js +49 -0
- package/web/parse-jwt.js +15 -0
- package/web/pwa.js +84 -0
- package/web/sign-pad.js +164 -0
- package/web/t4t-fe.js +164 -0
- package/web/util.js +126 -0
- package/web/web-cam.js +182 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
// telegram-sender.js
|
|
2
|
+
import FormData from "form-data";
|
|
3
|
+
import fetch from "node-fetch";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
const BASE_URL = (token) => `https://api.telegram.org/bot${token}`;
|
|
8
|
+
|
|
9
|
+
// ─── Core Request Helper ──────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
async function apiRequest(token, method, params = {}, formData = null) {
|
|
12
|
+
const url = `${BASE_URL(token)}/${method}`;
|
|
13
|
+
|
|
14
|
+
const options = formData
|
|
15
|
+
? { method: "POST", body: formData }
|
|
16
|
+
: {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
body: JSON.stringify(params),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const res = await fetch(url, options);
|
|
23
|
+
const data = await res.json();
|
|
24
|
+
|
|
25
|
+
if (!data.ok) {
|
|
26
|
+
throw new TelegramError(data.description, data.error_code, method);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return data.result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class TelegramError extends Error {
|
|
33
|
+
constructor(message, code, method) {
|
|
34
|
+
super(`[${method}] Telegram API error ${code}: ${message}`);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.method = method;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── File Helper ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolves a file input to the right Telegram format:
|
|
44
|
+
* - "file_id:..." → use existing Telegram file ID
|
|
45
|
+
* - "http(s)://..." → use URL
|
|
46
|
+
* - Everything else → treat as local path, attach as multipart
|
|
47
|
+
*/
|
|
48
|
+
function resolveFile(fd, fieldName, input) {
|
|
49
|
+
if (!input) return null;
|
|
50
|
+
if (input.startsWith("file_id:")) return input.replace("file_id:", "");
|
|
51
|
+
if (input.startsWith("http://") || input.startsWith("https://")) return input;
|
|
52
|
+
|
|
53
|
+
// Local file – attach to FormData
|
|
54
|
+
const stream = fs.createReadStream(input);
|
|
55
|
+
fd.append(fieldName, stream, path.basename(input));
|
|
56
|
+
return null; // signal: already added to form
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Keyboard Builders ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Inline keyboard attached to the message.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* inlineKeyboard([
|
|
66
|
+
* [{ text: "Visit", url: "https://example.com" }],
|
|
67
|
+
* [{ text: "Callback", callback_data: "btn_1" }, { text: "Pay", pay: true }]
|
|
68
|
+
* ])
|
|
69
|
+
*/
|
|
70
|
+
export function inlineKeyboard(rows) {
|
|
71
|
+
return JSON.stringify({ inline_keyboard: rows });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Custom reply keyboard shown to the user.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* replyKeyboard([["Yes", "No"], ["Cancel"]], { resize_keyboard: true })
|
|
79
|
+
*/
|
|
80
|
+
export function replyKeyboard(
|
|
81
|
+
rows,
|
|
82
|
+
{ resize_keyboard = true, one_time_keyboard = false, is_persistent = false, selective = false } = {}
|
|
83
|
+
) {
|
|
84
|
+
return JSON.stringify({
|
|
85
|
+
keyboard: rows.map((row) =>
|
|
86
|
+
row.map((btn) => (typeof btn === "string" ? { text: btn } : btn))
|
|
87
|
+
),
|
|
88
|
+
resize_keyboard,
|
|
89
|
+
one_time_keyboard,
|
|
90
|
+
is_persistent,
|
|
91
|
+
selective,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Removes any custom keyboard from the user's view. */
|
|
96
|
+
export function removeKeyboard(selective = false) {
|
|
97
|
+
return JSON.stringify({ remove_keyboard: true, selective });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Forces a reply prompt on the user's client. */
|
|
101
|
+
export function forceReply(input_field_placeholder = "", selective = false) {
|
|
102
|
+
return JSON.stringify({ force_reply: true, input_field_placeholder, selective });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Shared Message Options ───────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function commonOpts({
|
|
108
|
+
parse_mode, // "HTML" | "Markdown" | "MarkdownV2"
|
|
109
|
+
caption,
|
|
110
|
+
caption_parse_mode,
|
|
111
|
+
reply_to_message_id,
|
|
112
|
+
allow_sending_without_reply,
|
|
113
|
+
reply_markup, // use inlineKeyboard() / replyKeyboard() helpers
|
|
114
|
+
disable_notification,
|
|
115
|
+
protect_content,
|
|
116
|
+
message_thread_id, // forum thread id
|
|
117
|
+
business_connection_id,
|
|
118
|
+
} = {}) {
|
|
119
|
+
return Object.fromEntries(
|
|
120
|
+
Object.entries({
|
|
121
|
+
parse_mode, caption, caption_parse_mode,
|
|
122
|
+
reply_to_message_id, allow_sending_without_reply,
|
|
123
|
+
reply_markup, disable_notification, protect_content,
|
|
124
|
+
message_thread_id, business_connection_id,
|
|
125
|
+
}).filter(([, v]) => v !== undefined)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Text ─────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Send a plain or formatted text message.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} token
|
|
135
|
+
* @param {number|string} chatId
|
|
136
|
+
* @param {string} text
|
|
137
|
+
* @param {object} opts
|
|
138
|
+
* @param {string} [opts.parse_mode] – "HTML" | "Markdown" | "MarkdownV2"
|
|
139
|
+
* @param {Array} [opts.entities] – pre-built MessageEntity array
|
|
140
|
+
* @param {boolean} [opts.disable_web_page_preview]
|
|
141
|
+
* @param {string} [opts.reply_markup] – use inlineKeyboard() etc.
|
|
142
|
+
*/
|
|
143
|
+
export async function sendMessage(token, chatId, text, opts = {}) {
|
|
144
|
+
return apiRequest(token, "sendMessage", {
|
|
145
|
+
chat_id: chatId,
|
|
146
|
+
text,
|
|
147
|
+
disable_web_page_preview: opts.disable_web_page_preview,
|
|
148
|
+
entities: opts.entities,
|
|
149
|
+
...commonOpts(opts),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Photo ────────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} photo – local path, HTTPS URL, or "file_id:<id>"
|
|
157
|
+
* @param {boolean} [opts.has_spoiler]
|
|
158
|
+
*/
|
|
159
|
+
export async function sendPhoto(token, chatId, photo, opts = {}) {
|
|
160
|
+
const fd = new FormData();
|
|
161
|
+
fd.append("chat_id", String(chatId));
|
|
162
|
+
if (opts.caption) fd.append("caption", opts.caption);
|
|
163
|
+
if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode);
|
|
164
|
+
if (opts.has_spoiler) fd.append("has_spoiler", "true");
|
|
165
|
+
if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup);
|
|
166
|
+
if (opts.reply_to_message_id) fd.append("reply_to_message_id", String(opts.reply_to_message_id));
|
|
167
|
+
if (opts.disable_notification) fd.append("disable_notification", "true");
|
|
168
|
+
if (opts.protect_content) fd.append("protect_content", "true");
|
|
169
|
+
|
|
170
|
+
const resolved = resolveFile(fd, "photo", photo);
|
|
171
|
+
if (resolved) fd.append("photo", resolved);
|
|
172
|
+
|
|
173
|
+
return apiRequest(token, "sendPhoto", {}, fd);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Video ────────────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {string} video – local path, URL, or file_id
|
|
180
|
+
* @param {object} opts
|
|
181
|
+
* @param {number} [opts.duration]
|
|
182
|
+
* @param {number} [opts.width]
|
|
183
|
+
* @param {number} [opts.height]
|
|
184
|
+
* @param {string} [opts.thumbnail] – local path, URL, or file_id for cover image
|
|
185
|
+
* @param {boolean} [opts.supports_streaming]
|
|
186
|
+
* @param {boolean} [opts.has_spoiler]
|
|
187
|
+
*/
|
|
188
|
+
export async function sendVideo(token, chatId, video, opts = {}) {
|
|
189
|
+
const fd = new FormData();
|
|
190
|
+
fd.append("chat_id", String(chatId));
|
|
191
|
+
if (opts.duration) fd.append("duration", String(opts.duration));
|
|
192
|
+
if (opts.width) fd.append("width", String(opts.width));
|
|
193
|
+
if (opts.height) fd.append("height", String(opts.height));
|
|
194
|
+
if (opts.supports_streaming) fd.append("supports_streaming", "true");
|
|
195
|
+
if (opts.has_spoiler) fd.append("has_spoiler", "true");
|
|
196
|
+
if (opts.caption) fd.append("caption", opts.caption);
|
|
197
|
+
if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode);
|
|
198
|
+
if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup);
|
|
199
|
+
|
|
200
|
+
const resolvedVideo = resolveFile(fd, "video", video);
|
|
201
|
+
if (resolvedVideo) fd.append("video", resolvedVideo);
|
|
202
|
+
|
|
203
|
+
if (opts.thumbnail) {
|
|
204
|
+
const resolvedThumb = resolveFile(fd, "thumbnail", opts.thumbnail);
|
|
205
|
+
if (resolvedThumb) fd.append("thumbnail", resolvedThumb);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return apiRequest(token, "sendVideo", {}, fd);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Audio ────────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {string} audio – local path, URL, or file_id
|
|
215
|
+
* @param {object} opts
|
|
216
|
+
* @param {number} [opts.duration]
|
|
217
|
+
* @param {string} [opts.performer]
|
|
218
|
+
* @param {string} [opts.title]
|
|
219
|
+
* @param {string} [opts.thumbnail]
|
|
220
|
+
*/
|
|
221
|
+
export async function sendAudio(token, chatId, audio, opts = {}) {
|
|
222
|
+
const fd = new FormData();
|
|
223
|
+
fd.append("chat_id", String(chatId));
|
|
224
|
+
if (opts.duration) fd.append("duration", String(opts.duration));
|
|
225
|
+
if (opts.performer) fd.append("performer", opts.performer);
|
|
226
|
+
if (opts.title) fd.append("title", opts.title);
|
|
227
|
+
if (opts.caption) fd.append("caption", opts.caption);
|
|
228
|
+
if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode);
|
|
229
|
+
if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup);
|
|
230
|
+
|
|
231
|
+
const resolved = resolveFile(fd, "audio", audio);
|
|
232
|
+
if (resolved) fd.append("audio", resolved);
|
|
233
|
+
|
|
234
|
+
if (opts.thumbnail) {
|
|
235
|
+
const resolvedThumb = resolveFile(fd, "thumbnail", opts.thumbnail);
|
|
236
|
+
if (resolvedThumb) fd.append("thumbnail", resolvedThumb);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return apiRequest(token, "sendAudio", {}, fd);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── Document ─────────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {string} document – local path, URL, or file_id
|
|
246
|
+
* @param {boolean} [opts.disable_content_type_detection]
|
|
247
|
+
*/
|
|
248
|
+
export async function sendDocument(token, chatId, document, opts = {}) {
|
|
249
|
+
const fd = new FormData();
|
|
250
|
+
fd.append("chat_id", String(chatId));
|
|
251
|
+
if (opts.caption) fd.append("caption", opts.caption);
|
|
252
|
+
if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode);
|
|
253
|
+
if (opts.disable_content_type_detection)
|
|
254
|
+
fd.append("disable_content_type_detection", "true");
|
|
255
|
+
if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup);
|
|
256
|
+
|
|
257
|
+
const resolved = resolveFile(fd, "document", document);
|
|
258
|
+
if (resolved) fd.append("document", resolved);
|
|
259
|
+
|
|
260
|
+
if (opts.thumbnail) {
|
|
261
|
+
const resolvedThumb = resolveFile(fd, "thumbnail", opts.thumbnail);
|
|
262
|
+
if (resolvedThumb) fd.append("thumbnail", resolvedThumb);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return apiRequest(token, "sendDocument", {}, fd);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Voice ────────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
/** OGG/OPUS encoded voice message. */
|
|
271
|
+
export async function sendVoice(token, chatId, voice, opts = {}) {
|
|
272
|
+
const fd = new FormData();
|
|
273
|
+
fd.append("chat_id", String(chatId));
|
|
274
|
+
if (opts.duration) fd.append("duration", String(opts.duration));
|
|
275
|
+
if (opts.caption) fd.append("caption", opts.caption);
|
|
276
|
+
if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode);
|
|
277
|
+
|
|
278
|
+
const resolved = resolveFile(fd, "voice", voice);
|
|
279
|
+
if (resolved) fd.append("voice", resolved);
|
|
280
|
+
|
|
281
|
+
return apiRequest(token, "sendVoice", {}, fd);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Video Note ───────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/** Round video (1:1 aspect ratio). */
|
|
287
|
+
export async function sendVideoNote(token, chatId, videoNote, opts = {}) {
|
|
288
|
+
const fd = new FormData();
|
|
289
|
+
fd.append("chat_id", String(chatId));
|
|
290
|
+
if (opts.duration) fd.append("duration", String(opts.duration));
|
|
291
|
+
if (opts.length) fd.append("length", String(opts.length));
|
|
292
|
+
|
|
293
|
+
const resolved = resolveFile(fd, "video_note", videoNote);
|
|
294
|
+
if (resolved) fd.append("video_note", resolved);
|
|
295
|
+
|
|
296
|
+
return apiRequest(token, "sendVideoNote", {}, fd);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Sticker ──────────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {string} sticker – local .webp/.tgs/.webm, URL, or file_id
|
|
303
|
+
* @param {string} [opts.emoji] – emoji associated with the sticker
|
|
304
|
+
*/
|
|
305
|
+
export async function sendSticker(token, chatId, sticker, opts = {}) {
|
|
306
|
+
const fd = new FormData();
|
|
307
|
+
fd.append("chat_id", String(chatId));
|
|
308
|
+
if (opts.emoji) fd.append("emoji", opts.emoji);
|
|
309
|
+
if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup);
|
|
310
|
+
|
|
311
|
+
const resolved = resolveFile(fd, "sticker", sticker);
|
|
312
|
+
if (resolved) fd.append("sticker", resolved);
|
|
313
|
+
|
|
314
|
+
return apiRequest(token, "sendSticker", {}, fd);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── Animation (GIF) ──────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
export async function sendAnimation(token, chatId, animation, opts = {}) {
|
|
320
|
+
const fd = new FormData();
|
|
321
|
+
fd.append("chat_id", String(chatId));
|
|
322
|
+
if (opts.duration) fd.append("duration", String(opts.duration));
|
|
323
|
+
if (opts.width) fd.append("width", String(opts.width));
|
|
324
|
+
if (opts.height) fd.append("height", String(opts.height));
|
|
325
|
+
if (opts.caption) fd.append("caption", opts.caption);
|
|
326
|
+
if (opts.has_spoiler) fd.append("has_spoiler", "true");
|
|
327
|
+
|
|
328
|
+
const resolved = resolveFile(fd, "animation", animation);
|
|
329
|
+
if (resolved) fd.append("animation", resolved);
|
|
330
|
+
|
|
331
|
+
return apiRequest(token, "sendAnimation", {}, fd);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─── Media Group (Album) ──────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Send 2–10 photos/videos as an album.
|
|
338
|
+
*
|
|
339
|
+
* @param {Array} media Array of { type, file, caption?, parse_mode? }
|
|
340
|
+
* type: "photo" | "video" | "audio" | "document"
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* sendMediaGroup(token, chatId, [
|
|
344
|
+
* { type: "photo", file: "./img1.jpg", caption: "First" },
|
|
345
|
+
* { type: "photo", file: "./img2.jpg" },
|
|
346
|
+
* { type: "video", file: "./clip.mp4" },
|
|
347
|
+
* ]);
|
|
348
|
+
*/
|
|
349
|
+
export async function sendMediaGroup(token, chatId, media, opts = {}) {
|
|
350
|
+
const fd = new FormData();
|
|
351
|
+
fd.append("chat_id", String(chatId));
|
|
352
|
+
if (opts.reply_to_message_id)
|
|
353
|
+
fd.append("reply_to_message_id", String(opts.reply_to_message_id));
|
|
354
|
+
if (opts.disable_notification) fd.append("disable_notification", "true");
|
|
355
|
+
if (opts.protect_content) fd.append("protect_content", "true");
|
|
356
|
+
|
|
357
|
+
const mediaArray = media.map((item, i) => {
|
|
358
|
+
const fieldName = `file_${i}`;
|
|
359
|
+
const resolved = resolveFile(fd, fieldName, item.file);
|
|
360
|
+
return {
|
|
361
|
+
type: item.type,
|
|
362
|
+
media: resolved ?? `attach://${fieldName}`,
|
|
363
|
+
...(item.caption ? { caption: item.caption } : {}),
|
|
364
|
+
...(item.parse_mode ? { parse_mode: item.parse_mode } : {}),
|
|
365
|
+
...(item.has_spoiler ? { has_spoiler: true } : {}),
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
fd.append("media", JSON.stringify(mediaArray));
|
|
370
|
+
return apiRequest(token, "sendMediaGroup", {}, fd);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Location ─────────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @param {object} opts
|
|
377
|
+
* @param {number} [opts.horizontal_accuracy] – accuracy radius in metres (0–1500)
|
|
378
|
+
* @param {number} [opts.live_period] – seconds to broadcast live (60–86400)
|
|
379
|
+
* @param {number} [opts.heading] – 1–360 degrees
|
|
380
|
+
* @param {number} [opts.proximity_alert_radius]
|
|
381
|
+
*/
|
|
382
|
+
export async function sendLocation(token, chatId, latitude, longitude, opts = {}) {
|
|
383
|
+
return apiRequest(token, "sendLocation", {
|
|
384
|
+
chat_id: chatId,
|
|
385
|
+
latitude,
|
|
386
|
+
longitude,
|
|
387
|
+
horizontal_accuracy: opts.horizontal_accuracy,
|
|
388
|
+
live_period: opts.live_period,
|
|
389
|
+
heading: opts.heading,
|
|
390
|
+
proximity_alert_radius: opts.proximity_alert_radius,
|
|
391
|
+
...commonOpts(opts),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Edit a live location message while it's still broadcasting. */
|
|
396
|
+
export async function editLiveLocation(token, chatId, messageId, latitude, longitude, opts = {}) {
|
|
397
|
+
return apiRequest(token, "editMessageLiveLocation", {
|
|
398
|
+
chat_id: chatId,
|
|
399
|
+
message_id: messageId,
|
|
400
|
+
latitude,
|
|
401
|
+
longitude,
|
|
402
|
+
horizontal_accuracy: opts.horizontal_accuracy,
|
|
403
|
+
heading: opts.heading,
|
|
404
|
+
proximity_alert_radius: opts.proximity_alert_radius,
|
|
405
|
+
reply_markup: opts.reply_markup,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ─── Venue ────────────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
export async function sendVenue(token, chatId, { latitude, longitude, title, address, foursquare_id, foursquare_type, google_place_id, google_place_type }, opts = {}) {
|
|
412
|
+
return apiRequest(token, "sendVenue", {
|
|
413
|
+
chat_id: chatId,
|
|
414
|
+
latitude,
|
|
415
|
+
longitude,
|
|
416
|
+
title,
|
|
417
|
+
address,
|
|
418
|
+
foursquare_id,
|
|
419
|
+
foursquare_type,
|
|
420
|
+
google_place_id,
|
|
421
|
+
google_place_type,
|
|
422
|
+
...commonOpts(opts),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ─── Contact ──────────────────────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
export async function sendContact(token, chatId, { phone_number, first_name, last_name, vcard }, opts = {}) {
|
|
429
|
+
return apiRequest(token, "sendContact", {
|
|
430
|
+
chat_id: chatId,
|
|
431
|
+
phone_number,
|
|
432
|
+
first_name,
|
|
433
|
+
last_name,
|
|
434
|
+
vcard,
|
|
435
|
+
...commonOpts(opts),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ─── Poll ─────────────────────────────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* @param {string} question
|
|
443
|
+
* @param {string[]} options – 2–10 answer choices
|
|
444
|
+
* @param {object} opts
|
|
445
|
+
* @param {string} [opts.type] – "regular" | "quiz"
|
|
446
|
+
* @param {boolean} [opts.is_anonymous]
|
|
447
|
+
* @param {boolean} [opts.allows_multiple_answers]
|
|
448
|
+
* @param {number} [opts.correct_option_id] – required for quiz type
|
|
449
|
+
* @param {string} [opts.explanation]
|
|
450
|
+
* @param {number} [opts.open_period] – seconds (5–600)
|
|
451
|
+
* @param {number} [opts.close_date] – unix timestamp
|
|
452
|
+
* @param {boolean} [opts.is_closed]
|
|
453
|
+
*/
|
|
454
|
+
export async function sendPoll(token, chatId, question, options, opts = {}) {
|
|
455
|
+
return apiRequest(token, "sendPoll", {
|
|
456
|
+
chat_id: chatId,
|
|
457
|
+
question,
|
|
458
|
+
options,
|
|
459
|
+
type: opts.type ?? "regular",
|
|
460
|
+
is_anonymous: opts.is_anonymous ?? true,
|
|
461
|
+
allows_multiple_answers: opts.allows_multiple_answers,
|
|
462
|
+
correct_option_id: opts.correct_option_id,
|
|
463
|
+
explanation: opts.explanation,
|
|
464
|
+
explanation_parse_mode: opts.explanation_parse_mode,
|
|
465
|
+
open_period: opts.open_period,
|
|
466
|
+
close_date: opts.close_date,
|
|
467
|
+
is_closed: opts.is_closed,
|
|
468
|
+
...commonOpts(opts),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ─── Dice ─────────────────────────────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
/** @param {string} [emoji] – "🎲" | "🎯" | "🏀" | "⚽" | "🎳" | "🎰" (default 🎲) */
|
|
475
|
+
export async function sendDice(token, chatId, emoji = "🎲", opts = {}) {
|
|
476
|
+
return apiRequest(token, "sendDice", {
|
|
477
|
+
chat_id: chatId,
|
|
478
|
+
emoji,
|
|
479
|
+
...commonOpts(opts),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ─── Chat Action (typing indicator) ──────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* @param {string} action – "typing" | "upload_photo" | "record_video" |
|
|
487
|
+
* "upload_video" | "record_voice" | "upload_voice" |
|
|
488
|
+
* "upload_document" | "choose_sticker" |
|
|
489
|
+
* "find_location" | "record_video_note" |
|
|
490
|
+
* "upload_video_note"
|
|
491
|
+
*/
|
|
492
|
+
export async function sendChatAction(token, chatId, action, opts = {}) {
|
|
493
|
+
return apiRequest(token, "sendChatAction", {
|
|
494
|
+
chat_id: chatId,
|
|
495
|
+
action,
|
|
496
|
+
message_thread_id: opts.message_thread_id,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ─── Forward & Copy ───────────────────────────────────────────────────────────
|
|
501
|
+
|
|
502
|
+
export async function forwardMessage(token, chatId, fromChatId, messageId, opts = {}) {
|
|
503
|
+
return apiRequest(token, "forwardMessage", {
|
|
504
|
+
chat_id: chatId,
|
|
505
|
+
from_chat_id: fromChatId,
|
|
506
|
+
message_id: messageId,
|
|
507
|
+
disable_notification: opts.disable_notification,
|
|
508
|
+
protect_content: opts.protect_content,
|
|
509
|
+
message_thread_id: opts.message_thread_id,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Copy without the forward header. */
|
|
514
|
+
export async function copyMessage(token, chatId, fromChatId, messageId, opts = {}) {
|
|
515
|
+
return apiRequest(token, "copyMessage", {
|
|
516
|
+
chat_id: chatId,
|
|
517
|
+
from_chat_id: fromChatId,
|
|
518
|
+
message_id: messageId,
|
|
519
|
+
caption: opts.caption,
|
|
520
|
+
parse_mode: opts.parse_mode,
|
|
521
|
+
reply_markup: opts.reply_markup,
|
|
522
|
+
disable_notification: opts.disable_notification,
|
|
523
|
+
protect_content: opts.protect_content,
|
|
524
|
+
reply_to_message_id: opts.reply_to_message_id,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ─── Edit & Delete ────────────────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
export async function editMessageText(token, chatId, messageId, text, opts = {}) {
|
|
531
|
+
return apiRequest(token, "editMessageText", {
|
|
532
|
+
chat_id: chatId,
|
|
533
|
+
message_id: messageId,
|
|
534
|
+
text,
|
|
535
|
+
parse_mode: opts.parse_mode,
|
|
536
|
+
entities: opts.entities,
|
|
537
|
+
disable_web_page_preview: opts.disable_web_page_preview,
|
|
538
|
+
reply_markup: opts.reply_markup,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export async function editMessageCaption(token, chatId, messageId, caption, opts = {}) {
|
|
543
|
+
return apiRequest(token, "editMessageCaption", {
|
|
544
|
+
chat_id: chatId,
|
|
545
|
+
message_id: messageId,
|
|
546
|
+
caption,
|
|
547
|
+
parse_mode: opts.parse_mode,
|
|
548
|
+
reply_markup: opts.reply_markup,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export async function editMessageReplyMarkup(token, chatId, messageId, replyMarkup) {
|
|
553
|
+
return apiRequest(token, "editMessageReplyMarkup", {
|
|
554
|
+
chat_id: chatId,
|
|
555
|
+
message_id: messageId,
|
|
556
|
+
reply_markup: replyMarkup,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export async function deleteMessage(token, chatId, messageId) {
|
|
561
|
+
return apiRequest(token, "deleteMessage", { chat_id: chatId, message_id: messageId });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export async function pinMessage(token, chatId, messageId, opts = {}) {
|
|
565
|
+
return apiRequest(token, "pinChatMessage", {
|
|
566
|
+
chat_id: chatId,
|
|
567
|
+
message_id: messageId,
|
|
568
|
+
disable_notification: opts.disable_notification,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export async function unpinMessage(token, chatId, messageId) {
|
|
573
|
+
return apiRequest(token, "unpinChatMessage", { chat_id: chatId, message_id: messageId });
|
|
574
|
+
}
|
package/comms/webpush.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import webPush from 'web-push'
|
|
2
|
+
|
|
3
|
+
// npx web-push generate-vapid-keys
|
|
4
|
+
const vapidKeys = webPush.generateVAPIDKeys() // We use webpush to generate our public and private keys
|
|
5
|
+
const { publicKey, privateKey } = vapidKeys
|
|
6
|
+
const { WEBPUSH_VAPID_SUBJ } = process.env
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
if (WEBPUSH_VAPID_SUBJ) {
|
|
10
|
+
console.log('webpush setup')
|
|
11
|
+
webPush.setVapidDetails(WEBPUSH_VAPID_SUBJ, publicKey, privateKey) // We are giving webpush the required information to encrypt our data
|
|
12
|
+
console.log('webpush setup done')
|
|
13
|
+
} else {
|
|
14
|
+
console.log('no webpush setup')
|
|
15
|
+
}
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error('[webpush error]', e.toString())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// This function takes a subscription object and a payload as an argument. It will try to encrypt the payload
|
|
21
|
+
// then attempt to send a notification via the subscription's endpoint
|
|
22
|
+
// will throw exception if error
|
|
23
|
+
const send = async (subscription, payload, options = { TTL: 60 }) => {
|
|
24
|
+
// This means we won't resend a notification if the client is offline
|
|
25
|
+
// what if TTL = 0 ?
|
|
26
|
+
// web-push's sendNotification function does all the work for us
|
|
27
|
+
if (!subscription.keys) { payload = payload || null }
|
|
28
|
+
return await webPush.sendNotification(subscription, payload, options) // will throw if error
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const getPubKey = () => vapidKeys.publicKey
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
send,
|
|
35
|
+
getPubKey
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// // sw.js
|
|
39
|
+
// self.addEventListener('push', (event) => {
|
|
40
|
+
// const data = event.data.json();
|
|
41
|
+
// self.registration.showNotification(data.title, {
|
|
42
|
+
// body: data.body,
|
|
43
|
+
// icon: '/icon.png',
|
|
44
|
+
// });
|
|
45
|
+
// });
|
|
46
|
+
|
|
47
|
+
// // pn.js
|
|
48
|
+
// const reg = await navigator.serviceWorker.register('/sw.js');
|
|
49
|
+
|
|
50
|
+
// const subscription = await reg.pushManager.subscribe({
|
|
51
|
+
// userVisibleOnly: true,
|
|
52
|
+
// applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY), // your public VAPID key
|
|
53
|
+
// });
|
|
54
|
+
|
|
55
|
+
// // Save subscription to your server
|
|
56
|
+
// await fetch('/api/subscribe', {
|
|
57
|
+
// method: 'POST',
|
|
58
|
+
// body: JSON.stringify(subscription),
|
|
59
|
+
// headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
// });
|
package/config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
|
|
7
|
+
export default async function(app_path) {
|
|
8
|
+
process.env.NODE_ENV = process.env.NODE_ENV || '' // development, dev, prd... (development is on local machine)
|
|
9
|
+
const { NODE_ENV, VAULT } = process.env
|
|
10
|
+
if (!NODE_ENV) {
|
|
11
|
+
console.log('Exiting No Environment Specified')
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const packageJsonPath = path.join(app_path, 'package.json')
|
|
16
|
+
const packageJsonContent = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
17
|
+
const { version, name } = packageJsonContent
|
|
18
|
+
process.env.APP_VERSION = version
|
|
19
|
+
process.env.APP_NAME = name
|
|
20
|
+
|
|
21
|
+
if (NODE_ENV) {
|
|
22
|
+
if (VAULT && VAULT !== 'unused') {
|
|
23
|
+
try {
|
|
24
|
+
const vaultRes = await fetch(VAULT) // a GET with query parameters (protected)
|
|
25
|
+
const vaultConfig = await vaultRes.json()
|
|
26
|
+
process.env = { ...process.env, ...vaultConfig }
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.log('vault error', e.toString(), VAULT)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const sleep = (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds))
|
|
32
|
+
await sleep(2000)
|
|
33
|
+
console.log('CONFIG DONE!')
|
|
34
|
+
} else {
|
|
35
|
+
console.log('NODE_ENV and APP_PATH needs to be defined')
|
|
36
|
+
}
|
|
37
|
+
}
|