@xbibzlibrary/telebibz 0.4.5 → 3.1.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 +53 -138
- package/LICENSE +1 -1
- package/NOTICE.md +9 -4
- package/README.id.md +542 -206
- package/README.md +539 -213
- package/examples/01-quickstart.js +13 -0
- package/examples/02-menu-tombol.js +22 -0
- package/examples/03-wizard.js +58 -0
- package/examples/04-broadcast.js +25 -0
- package/examples/05-kirim-file.js +19 -0
- package/examples/06-menu.js +35 -0
- package/examples/07-inline-query.js +18 -0
- package/index.d.ts +106 -0
- package/index.js +44 -0
- package/lib/api.js +154 -0
- package/lib/broadcast.js +35 -0
- package/lib/composer.js +174 -0
- package/lib/context.js +190 -0
- package/lib/errors.js +35 -0
- package/lib/file.js +41 -0
- package/lib/inline-query.js +28 -0
- package/lib/keyboard.js +83 -0
- package/lib/logger.js +38 -0
- package/lib/menus.js +90 -0
- package/lib/net.js +115 -0
- package/lib/ratelimit.js +61 -0
- package/lib/runner.js +45 -0
- package/lib/session.js +36 -0
- package/lib/telebibz.js +193 -0
- package/lib/wizard.js +247 -0
- package/package.json +35 -97
- package/test/all.test.js +439 -0
- package/CODE_OF_CONDUCT.md +0 -37
- package/CONTRIBUTING.md +0 -59
- package/CONTRIBUTION_RULES.md +0 -41
- package/GOVERNANCE.md +0 -47
- package/README.zh-CN.md +0 -306
- package/RELEASE_AUTOMATION.md +0 -78
- package/RELEASE_POLICY.md +0 -32
- package/SECURITY.md +0 -47
- package/SHOWCASE.md +0 -29
- package/SUPPORT.md +0 -30
- package/assets/readme-preview.html +0 -75
- package/assets/telebibz-logo.png +0 -0
- package/assets/telebibz-readme-preview.png +0 -0
- package/bin/telebibz.mjs +0 -3
- package/dist/generated/api.d.ts +0 -13
- package/dist/generated/api.d.ts.map +0 -1
- package/dist/generated/api.js +0 -192
- package/dist/generated/api.js.map +0 -1
- package/dist/src/api/client.d.ts +0 -62
- package/dist/src/api/client.d.ts.map +0 -1
- package/dist/src/api/client.js +0 -104
- package/dist/src/api/client.js.map +0 -1
- package/dist/src/api/errors.d.ts +0 -45
- package/dist/src/api/errors.d.ts.map +0 -1
- package/dist/src/api/errors.js +0 -65
- package/dist/src/api/errors.js.map +0 -1
- package/dist/src/api/index.d.ts +0 -6
- package/dist/src/api/index.d.ts.map +0 -1
- package/dist/src/api/index.js +0 -6
- package/dist/src/api/index.js.map +0 -1
- package/dist/src/api/telegram-types/LICENSE +0 -21
- package/dist/src/api/telegram-types/api.d.ts +0 -22
- package/dist/src/api/telegram-types/checklist.d.ts +0 -72
- package/dist/src/api/telegram-types/inline.d.ts +0 -692
- package/dist/src/api/telegram-types/langs.d.ts +0 -193
- package/dist/src/api/telegram-types/manage.d.ts +0 -1144
- package/dist/src/api/telegram-types/markup.d.ts +0 -268
- package/dist/src/api/telegram-types/message.d.ts +0 -1537
- package/dist/src/api/telegram-types/methods.d.ts +0 -2870
- package/dist/src/api/telegram-types/mod.d.ts +0 -14
- package/dist/src/api/telegram-types/passport.d.ts +0 -163
- package/dist/src/api/telegram-types/payment.d.ts +0 -570
- package/dist/src/api/telegram-types/rich.d.ts +0 -1010
- package/dist/src/api/telegram-types/settings.d.ts +0 -120
- package/dist/src/api/telegram-types/story.d.ts +0 -89
- package/dist/src/api/telegram-types/update.d.ts +0 -84
- package/dist/src/api/telegram.d.ts +0 -7
- package/dist/src/api/telegram.d.ts.map +0 -1
- package/dist/src/api/telegram.js +0 -2
- package/dist/src/api/telegram.js.map +0 -1
- package/dist/src/api/transport.d.ts +0 -68
- package/dist/src/api/transport.d.ts.map +0 -1
- package/dist/src/api/transport.js +0 -264
- package/dist/src/api/transport.js.map +0 -1
- package/dist/src/api/types.d.ts +0 -466
- package/dist/src/api/types.d.ts.map +0 -1
- package/dist/src/api/types.js +0 -2
- package/dist/src/api/types.js.map +0 -1
- package/dist/src/branding/terminal.d.ts +0 -77
- package/dist/src/branding/terminal.d.ts.map +0 -1
- package/dist/src/branding/terminal.js +0 -328
- package/dist/src/branding/terminal.js.map +0 -1
- package/dist/src/broadcast/broadcast.d.ts +0 -50
- package/dist/src/broadcast/broadcast.d.ts.map +0 -1
- package/dist/src/broadcast/broadcast.js +0 -56
- package/dist/src/broadcast/broadcast.js.map +0 -1
- package/dist/src/cache/cache.d.ts +0 -34
- package/dist/src/cache/cache.d.ts.map +0 -1
- package/dist/src/cache/cache.js +0 -41
- package/dist/src/cache/cache.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -84
- package/dist/src/cli.js.map +0 -1
- package/dist/src/context/context.d.ts +0 -124
- package/dist/src/context/context.d.ts.map +0 -1
- package/dist/src/context/context.js +0 -302
- package/dist/src/context/context.js.map +0 -1
- package/dist/src/core/bot.d.ts +0 -204
- package/dist/src/core/bot.d.ts.map +0 -1
- package/dist/src/core/bot.js +0 -506
- package/dist/src/core/bot.js.map +0 -1
- package/dist/src/core/events.d.ts +0 -75
- package/dist/src/core/events.d.ts.map +0 -1
- package/dist/src/core/events.js +0 -35
- package/dist/src/core/events.js.map +0 -1
- package/dist/src/core/webhook-reply.d.ts +0 -34
- package/dist/src/core/webhook-reply.d.ts.map +0 -1
- package/dist/src/core/webhook-reply.js +0 -37
- package/dist/src/core/webhook-reply.js.map +0 -1
- package/dist/src/index.d.ts +0 -24
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -24
- package/dist/src/index.js.map +0 -1
- package/dist/src/keyboard/index.d.ts +0 -46
- package/dist/src/keyboard/index.d.ts.map +0 -1
- package/dist/src/keyboard/index.js +0 -55
- package/dist/src/keyboard/index.js.map +0 -1
- package/dist/src/middleware/compose.d.ts +0 -5
- package/dist/src/middleware/compose.d.ts.map +0 -1
- package/dist/src/middleware/compose.js +0 -17
- package/dist/src/middleware/compose.js.map +0 -1
- package/dist/src/observability/logger.d.ts +0 -78
- package/dist/src/observability/logger.d.ts.map +0 -1
- package/dist/src/observability/logger.js +0 -285
- package/dist/src/observability/logger.js.map +0 -1
- package/dist/src/plugins/plugin.d.ts +0 -38
- package/dist/src/plugins/plugin.d.ts.map +0 -1
- package/dist/src/plugins/plugin.js +0 -59
- package/dist/src/plugins/plugin.js.map +0 -1
- package/dist/src/queue/queue.d.ts +0 -77
- package/dist/src/queue/queue.d.ts.map +0 -1
- package/dist/src/queue/queue.js +0 -213
- package/dist/src/queue/queue.js.map +0 -1
- package/dist/src/router/router.d.ts +0 -61
- package/dist/src/router/router.d.ts.map +0 -1
- package/dist/src/router/router.js +0 -183
- package/dist/src/router/router.js.map +0 -1
- package/dist/src/state/conversation.d.ts +0 -56
- package/dist/src/state/conversation.d.ts.map +0 -1
- package/dist/src/state/conversation.js +0 -133
- package/dist/src/state/conversation.js.map +0 -1
- package/dist/src/state/forms.d.ts +0 -34
- package/dist/src/state/forms.d.ts.map +0 -1
- package/dist/src/state/forms.js +0 -44
- package/dist/src/state/forms.js.map +0 -1
- package/dist/src/state/menu.d.ts +0 -78
- package/dist/src/state/menu.d.ts.map +0 -1
- package/dist/src/state/menu.js +0 -127
- package/dist/src/state/menu.js.map +0 -1
- package/dist/src/storage/storage.d.ts +0 -146
- package/dist/src/storage/storage.d.ts.map +0 -1
- package/dist/src/storage/storage.js +0 -195
- package/dist/src/storage/storage.js.map +0 -1
- package/dist/src/telegram-features.d.ts +0 -33
- package/dist/src/telegram-features.d.ts.map +0 -1
- package/dist/src/telegram-features.js +0 -71
- package/dist/src/telegram-features.js.map +0 -1
- package/dist/src/testing.d.ts +0 -24
- package/dist/src/testing.d.ts.map +0 -1
- package/dist/src/testing.js +0 -38
- package/dist/src/testing.js.map +0 -1
- package/dist/src/utils/concurrency.d.ts +0 -25
- package/dist/src/utils/concurrency.d.ts.map +0 -1
- package/dist/src/utils/concurrency.js +0 -52
- package/dist/src/utils/concurrency.js.map +0 -1
- package/dist/src/utils/files.d.ts +0 -45
- package/dist/src/utils/files.d.ts.map +0 -1
- package/dist/src/utils/files.js +0 -53
- package/dist/src/utils/files.js.map +0 -1
- package/dist/src/utils/text.d.ts +0 -39
- package/dist/src/utils/text.d.ts.map +0 -1
- package/dist/src/utils/text.js +0 -56
- package/dist/src/utils/text.js.map +0 -1
- package/dist/src/webhook/handler.d.ts +0 -19
- package/dist/src/webhook/handler.d.ts.map +0 -1
- package/dist/src/webhook/handler.js +0 -141
- package/dist/src/webhook/handler.js.map +0 -1
- package/dist-cjs/generated/api.js +0 -194
- package/dist-cjs/package.json +0 -3
- package/dist-cjs/src/api/client.js +0 -107
- package/dist-cjs/src/api/errors.js +0 -74
- package/dist-cjs/src/api/index.js +0 -21
- package/dist-cjs/src/api/telegram-types/LICENSE +0 -21
- package/dist-cjs/src/api/telegram-types/api.d.ts +0 -22
- package/dist-cjs/src/api/telegram-types/checklist.d.ts +0 -72
- package/dist-cjs/src/api/telegram-types/inline.d.ts +0 -692
- package/dist-cjs/src/api/telegram-types/langs.d.ts +0 -193
- package/dist-cjs/src/api/telegram-types/manage.d.ts +0 -1144
- package/dist-cjs/src/api/telegram-types/markup.d.ts +0 -268
- package/dist-cjs/src/api/telegram-types/message.d.ts +0 -1537
- package/dist-cjs/src/api/telegram-types/methods.d.ts +0 -2870
- package/dist-cjs/src/api/telegram-types/mod.d.ts +0 -14
- package/dist-cjs/src/api/telegram-types/passport.d.ts +0 -163
- package/dist-cjs/src/api/telegram-types/payment.d.ts +0 -570
- package/dist-cjs/src/api/telegram-types/rich.d.ts +0 -1010
- package/dist-cjs/src/api/telegram-types/settings.d.ts +0 -120
- package/dist-cjs/src/api/telegram-types/story.d.ts +0 -89
- package/dist-cjs/src/api/telegram-types/update.d.ts +0 -84
- package/dist-cjs/src/api/telegram.js +0 -2
- package/dist-cjs/src/api/transport.js +0 -267
- package/dist-cjs/src/api/types.js +0 -2
- package/dist-cjs/src/branding/terminal.js +0 -338
- package/dist-cjs/src/broadcast/broadcast.js +0 -58
- package/dist-cjs/src/cache/cache.js +0 -45
- package/dist-cjs/src/cli.js +0 -86
- package/dist-cjs/src/context/context.js +0 -305
- package/dist-cjs/src/core/bot.js +0 -510
- package/dist-cjs/src/core/events.js +0 -38
- package/dist-cjs/src/core/webhook-reply.js +0 -42
- package/dist-cjs/src/index.js +0 -47
- package/dist-cjs/src/keyboard/index.js +0 -61
- package/dist-cjs/src/middleware/compose.js +0 -20
- package/dist-cjs/src/observability/logger.js +0 -293
- package/dist-cjs/src/plugins/plugin.js +0 -63
- package/dist-cjs/src/queue/queue.js +0 -219
- package/dist-cjs/src/router/router.js +0 -186
- package/dist-cjs/src/state/conversation.js +0 -139
- package/dist-cjs/src/state/forms.js +0 -47
- package/dist-cjs/src/state/menu.js +0 -133
- package/dist-cjs/src/storage/storage.js +0 -202
- package/dist-cjs/src/telegram-features.js +0 -76
- package/dist-cjs/src/testing.js +0 -45
- package/dist-cjs/src/utils/concurrency.js +0 -57
- package/dist-cjs/src/utils/files.js +0 -58
- package/dist-cjs/src/utils/text.js +0 -63
- package/dist-cjs/src/webhook/handler.js +0 -144
- package/docs/API.id.md +0 -1935
- package/docs/API.md +0 -1969
- package/docs/API.zh-CN.md +0 -1929
- package/docs/COOKBOOK.id.md +0 -321
- package/docs/COOKBOOK.md +0 -321
- package/docs/COOKBOOK.zh-CN.md +0 -321
- package/docs/ERRORS.id.md +0 -194
- package/docs/ERRORS.md +0 -194
- package/docs/ERRORS.zh-CN.md +0 -194
- package/docs/FILES.id.md +0 -243
- package/docs/FILES.md +0 -243
- package/docs/FILES.zh-CN.md +0 -243
- package/docs/GETTING_STARTED.id.md +0 -89
- package/docs/GETTING_STARTED.md +0 -89
- package/docs/GETTING_STARTED.zh-CN.md +0 -89
- package/docs/GITHUB_PACKAGES.id.md +0 -82
- package/docs/GITHUB_PACKAGES.md +0 -82
- package/docs/GITHUB_PACKAGES.zh-CN.md +0 -82
- package/docs/MIGRATION_TELEGRAF.id.md +0 -147
- package/docs/MIGRATION_TELEGRAF.md +0 -154
- package/docs/MIGRATION_TELEGRAF.zh-CN.md +0 -147
- package/docs/README.md +0 -67
- package/docs/STORAGE.id.md +0 -105
- package/docs/STORAGE.md +0 -105
- package/docs/STORAGE.zh-CN.md +0 -105
- package/docs/TESTING.id.md +0 -203
- package/docs/TESTING.md +0 -203
- package/docs/TESTING.zh-CN.md +0 -203
- package/docs/WEBHOOK.id.md +0 -212
- package/docs/WEBHOOK.md +0 -215
- package/docs/WEBHOOK.zh-CN.md +0 -212
- package/examples/README.md +0 -37
- package/examples/files.ts +0 -35
- package/examples/minimal.ts +0 -12
- package/examples/tsconfig.json +0 -9
- package/examples/webhook.ts +0 -42
- package/examples/wizard-registration.ts +0 -42
package/lib/menus.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// lib/menus.js — Menu inline ala @grammyjs/menu (mandiri, tanpa dep tambahan).
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { Composer } = require('./composer');
|
|
5
|
+
|
|
6
|
+
const re = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* const menu = new Menu('cfg');
|
|
10
|
+
* menu.text('🔔 Notif', async (ctx) => ctx.answerCallbackQuery('ditekan!'))
|
|
11
|
+
* .row()
|
|
12
|
+
* .url('Web', 'https://x')
|
|
13
|
+
* .submenu('Lanjut ▶', 'cfg-lanjut');
|
|
14
|
+
*
|
|
15
|
+
* bot.use(menu); // handler tombol terdaftar otomatis
|
|
16
|
+
* bot.cmd('cfg', async (ctx) =>
|
|
17
|
+
* ctx.reply('Menu:', { reply_markup: await menu.render(ctx) }));
|
|
18
|
+
*/
|
|
19
|
+
class Menu extends Composer {
|
|
20
|
+
constructor(id) {
|
|
21
|
+
super();
|
|
22
|
+
this.id = id;
|
|
23
|
+
this._ops = [];
|
|
24
|
+
this.callbackQuery(new RegExp(`^${re(id)}\\|`), async (ctx) => {
|
|
25
|
+
const i = Number(ctx.callback_query.data.slice(id.length + 1));
|
|
26
|
+
const op = this._ops[i];
|
|
27
|
+
if (!op) return ctx.answerCallbackQuery({ text: '⌛ Tombol usang — kirim ulang menunya', show_alert: true }).catch(() => {});
|
|
28
|
+
if (op.type === 'text' && op.handler) return op.handler(ctx);
|
|
29
|
+
if (op.type === 'submenu') {
|
|
30
|
+
const target = this._menus && this._menus.get(op.target);
|
|
31
|
+
if (target) {
|
|
32
|
+
try { await ctx.editMessageReplyMarkup({ reply_markup: await target.render(ctx) }); }
|
|
33
|
+
catch { /* pesan mungkin termodifikasi */ }
|
|
34
|
+
return ctx.answerCallbackQuery().catch(() => {});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return ctx.answerCallbackQuery().catch(() => {});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_add(o) { this._ops.push(o); return this; }
|
|
42
|
+
|
|
43
|
+
/** Tombol teks + handler saat ditekan. */
|
|
44
|
+
text(label, handler) { return this._add({ type: 'text', label, handler }); }
|
|
45
|
+
/** Tombol tautan. */
|
|
46
|
+
url(label, link, opts = {}) { return this._add({ type: 'url', label, link, opts }); }
|
|
47
|
+
/** Tombol WebApp. */
|
|
48
|
+
webApp(label, link) { return this._add({ type: 'webApp', label, link }); }
|
|
49
|
+
/** Tombol pindah ke menu lain dengan id target (Menu diregister lewat container). */
|
|
50
|
+
submenu(label, targetId) { return this._add({ type: 'submenu', label, target: targetId }); }
|
|
51
|
+
/** Baris baru. */
|
|
52
|
+
row() { return this._add({ type: 'row' }); }
|
|
53
|
+
/** Shortcut: kembali ke menu parent. */
|
|
54
|
+
back(label = '◀️ Kembali', parentId) { return this.submenu(label, parentId); }
|
|
55
|
+
|
|
56
|
+
/** Render keyboard (inline_keyboard). */
|
|
57
|
+
async render(ctx) {
|
|
58
|
+
const rows = [[]];
|
|
59
|
+
const push = (b) => rows[rows.length - 1].push(b);
|
|
60
|
+
for (let i = 0; i < this._ops.length; i++) {
|
|
61
|
+
const o = this._ops[i];
|
|
62
|
+
if (o.type === 'row') { rows.push([]); continue; }
|
|
63
|
+
if (o.type === 'url') { push({ text: o.label, url: o.link, ...(o.opts.style ? { style: o.opts.style } : {}), ...(o.opts.iconId ? { icon_custom_emoji_id: o.opts.iconId } : {}) }); continue; }
|
|
64
|
+
if (o.type === 'webApp') { push({ text: o.label, web_app: { url: o.link } }); continue; }
|
|
65
|
+
push({ text: o.label, callback_data: `${this.id}|${i}` });
|
|
66
|
+
}
|
|
67
|
+
return { inline_keyboard: rows.filter((r) => r.length) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Wadah beberapa menu terkait (dengan submenu silang):
|
|
73
|
+
* const mc = new MenuContainer();
|
|
74
|
+
* const utama = mc.create('utama'), lanjut = mc.create('lanjut');
|
|
75
|
+
* utama.submenu('Lanjut ▶', 'lanjut'); lanjut.back('◀️', 'utama');
|
|
76
|
+
* bot.use(mc);
|
|
77
|
+
*/
|
|
78
|
+
class MenuContainer extends Composer {
|
|
79
|
+
constructor() { super(); this._menus = new Map(); }
|
|
80
|
+
create(id) {
|
|
81
|
+
const m = new Menu(id);
|
|
82
|
+
m._menus = this._menus;
|
|
83
|
+
this._menus.set(id, m);
|
|
84
|
+
this.use(m);
|
|
85
|
+
return m;
|
|
86
|
+
}
|
|
87
|
+
get(id) { return this._menus.get(id); }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { Menu, MenuContainer };
|
package/lib/net.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// lib/net.js — transport Bot API production-grade (axios keep-alive, proxy, timeout,
|
|
2
|
+
// multipart attach://, 429 retry_after handler, debug logging).
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const dbg = require('debug')('telebibz:net');
|
|
9
|
+
let HttpsProxyAgent = null;
|
|
10
|
+
try { ({ HttpsProxyAgent } = require('https-proxy-agent')); } catch { /* proxy opsional */ }
|
|
11
|
+
|
|
12
|
+
const { File } = require('./file');
|
|
13
|
+
const mime = require('mime-types');
|
|
14
|
+
|
|
15
|
+
/** Error API Telegram dengan konteks debugging. */
|
|
16
|
+
class ApiError extends Error {
|
|
17
|
+
constructor(description, errorCode, method, payload, parameters) {
|
|
18
|
+
super(description);
|
|
19
|
+
this.name = 'ApiError';
|
|
20
|
+
this.description = description;
|
|
21
|
+
this.error_code = errorCode;
|
|
22
|
+
this.parameters = parameters || {};
|
|
23
|
+
this.method = method;
|
|
24
|
+
this.payload = payload;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const keepAliveHttp = new http.Agent({ keepAlive: true, maxSockets: 64 });
|
|
29
|
+
const keepAliveHttps = new https.Agent({ keepAlive: true, maxSockets: 64 });
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Bangun fungsi transport (method, payload) => result.
|
|
33
|
+
* opts: { apiRoot, proxy, timeoutMs, headers }
|
|
34
|
+
*/
|
|
35
|
+
function createTransport(token, opts = {}) {
|
|
36
|
+
const apiRoot = (opts.apiRoot || 'https://api.telegram.org').replace(/\/$/, '');
|
|
37
|
+
const agent = opts.proxy && HttpsProxyAgent ? new HttpsProxyAgent(opts.proxy) : keepAliveHttps;
|
|
38
|
+
const client = axios.create({
|
|
39
|
+
baseURL: `${apiRoot}/bot${token}/`,
|
|
40
|
+
httpAgent: keepAliveHttp,
|
|
41
|
+
httpsAgent: agent,
|
|
42
|
+
timeout: opts.timeoutMs || 35000, // sedikit > long-poll 25s
|
|
43
|
+
headers: opts.headers || {},
|
|
44
|
+
maxContentLength: Infinity,
|
|
45
|
+
maxBodyLength: Infinity,
|
|
46
|
+
proxy: false, // kendalikan lewat httpsAgent sendiri
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return async function transport(method, payload = {}) {
|
|
50
|
+
dbg('--> %s %s', method, JSON.stringify(payload, safeReplacer).slice(0, 400));
|
|
51
|
+
const files = [];
|
|
52
|
+
// Deep-walk: File di MANA PUN (media[i].media, photo top-level, thumbnail, dll)
|
|
53
|
+
const clean = collectAttach(payload, files);
|
|
54
|
+
|
|
55
|
+
let data, config = {};
|
|
56
|
+
if (files.length) {
|
|
57
|
+
const fd = new FormData();
|
|
58
|
+
for (const [k, v] of Object.entries(clean)) fd.append(k, typeof v === 'object' ? JSON.stringify(v) : String(v));
|
|
59
|
+
for (const [an, f] of files) {
|
|
60
|
+
const buf = await f.data();
|
|
61
|
+
fd.append(an, new Blob([buf], { type: f.type || mime.lookup(f.filename) || 'application/octet-stream' }), f.filename || an);
|
|
62
|
+
}
|
|
63
|
+
data = fd;
|
|
64
|
+
} else {
|
|
65
|
+
data = JSON.stringify(clean);
|
|
66
|
+
config.headers = { 'Content-Type': 'application/json' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let res;
|
|
70
|
+
try {
|
|
71
|
+
res = await client.post(method, data, config);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err.response && err.response.data) {
|
|
74
|
+
const d = err.response.data;
|
|
75
|
+
dbg('<-- ERR %s: %s', method, d.description);
|
|
76
|
+
throw new ApiError(d.description || `HTTP ${err.response.status}`, d.error_code || err.response.status, method, payload, d.parameters);
|
|
77
|
+
}
|
|
78
|
+
// error jaringan murni: tag agar Runner menganggapnya sementara
|
|
79
|
+
err.network = !err.response;
|
|
80
|
+
dbg('<-- NETWORK ERR %s: %s', method, err.message);
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
const env = res.data;
|
|
84
|
+
if (!env.ok) {
|
|
85
|
+
dbg('<-- ERR %s: %s', method, env.description);
|
|
86
|
+
throw new ApiError(env.description || 'unknown error', env.error_code, method, payload, env.parameters);
|
|
87
|
+
}
|
|
88
|
+
dbg('<-- OK %s', method);
|
|
89
|
+
return env.result;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Rekursif: salin payload sambil mengganti File jadi "attach://fileN". */
|
|
94
|
+
function collectAttach(value, files) {
|
|
95
|
+
if (value instanceof File) {
|
|
96
|
+
const an = `file${files.length}`;
|
|
97
|
+
files.push([an, value]);
|
|
98
|
+
return `attach://${an}`;
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) return value.map((v) => collectAttach(v, files));
|
|
101
|
+
if (value && typeof value === 'object') {
|
|
102
|
+
const o = {};
|
|
103
|
+
for (const [k, v] of Object.entries(value)) {
|
|
104
|
+
if (v === undefined || v === null) continue;
|
|
105
|
+
o[k] = collectAttach(v, files);
|
|
106
|
+
}
|
|
107
|
+
return o;
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** JSON.stringify yang aman terhadap File (untuk debug log). */
|
|
113
|
+
function safeReplacer(k, v) { return v instanceof File ? `<File:${v.filename}>` : v; }
|
|
114
|
+
|
|
115
|
+
module.exports = { createTransport, ApiError };
|
package/lib/ratelimit.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// lib/ratelimit.js — auto-retry 429, throttler antre, dan batas laju per-user.
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const dbg = require('debug')('telebibz:ratelimit');
|
|
5
|
+
|
|
6
|
+
/** Transformer 429: hormati retry_after Telegram (maks maxRetry kali).
|
|
7
|
+
* Pasang: bot.api.config.use(autoRetry()) */
|
|
8
|
+
function autoRetry({ maxRetry = 5, baseDelayMs = 500 } = {}) {
|
|
9
|
+
return async (prev, method, payload) => {
|
|
10
|
+
let attempt = 0;
|
|
11
|
+
for (;;) {
|
|
12
|
+
try { return await prev(method, payload); }
|
|
13
|
+
catch (err) {
|
|
14
|
+
const ra = err && err.parameters && err.parameters.retry_after;
|
|
15
|
+
if (ra !== undefined && attempt < maxRetry) {
|
|
16
|
+
attempt++;
|
|
17
|
+
const wait = ra * 1000 + baseDelayMs * attempt;
|
|
18
|
+
dbg('429 pada %s — tidur %dms (attempt %d)', method, wait, attempt);
|
|
19
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Transformer antrean: jaga maks `perSecond` panggilan API global.
|
|
29
|
+
* Pasang: bot.api.config.use(throttler()) */
|
|
30
|
+
function throttler({ perSecond = 28 } = {}) {
|
|
31
|
+
const interval = 1000 / perSecond;
|
|
32
|
+
let last = 0; let chain = Promise.resolve();
|
|
33
|
+
return (prev, method, payload) => {
|
|
34
|
+
chain = chain.then(async () => {
|
|
35
|
+
const wait = last + interval - Date.now();
|
|
36
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
37
|
+
last = Date.now();
|
|
38
|
+
return prev(method, payload);
|
|
39
|
+
});
|
|
40
|
+
return chain;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Middleware anti-spam: maks `limit` update per user per `windowMs`. */
|
|
45
|
+
function limiter({ windowMs = 2000, limit = 3, onExceeded } = {}) {
|
|
46
|
+
const bucket = new Map();
|
|
47
|
+
return async (ctx, next) => {
|
|
48
|
+
const key = ctx.from ? ctx.from.id : (ctx.chatId || '?');
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const arr = bucket.get(key) || [];
|
|
51
|
+
const alive = arr.filter((t) => now - t < windowMs);
|
|
52
|
+
if (alive.length >= limit) {
|
|
53
|
+
if (onExceeded) return onExceeded(ctx);
|
|
54
|
+
return; // diamkan spam
|
|
55
|
+
}
|
|
56
|
+
alive.push(now); bucket.set(key, alive);
|
|
57
|
+
return next();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { autoRetry, throttler, limiter };
|
package/lib/runner.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// lib/runner.js — long polling tahan banting (retry 409 & jaringan, backoff halus).
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Loop getUpdates; memanggil handleUpdate per update.
|
|
8
|
+
* stop() mengamaninya keluar loop dengan bersih.
|
|
9
|
+
* Perilaku:
|
|
10
|
+
* - error jaringan (fetch gagal) → retry 1 dtk
|
|
11
|
+
* - 409 (instance lain masih polling) → log callback onConflict + retry 5 dtk
|
|
12
|
+
* - error API lain → oper ke onFatal (default: berhenti bersih)
|
|
13
|
+
*/
|
|
14
|
+
async function pollLoop({ api, handleUpdate, allowedUpdates, dropPending, onConflict, onFatal, shouldStop, conflictDelay = 5000 }) {
|
|
15
|
+
let offset = 0;
|
|
16
|
+
if (dropPending) {
|
|
17
|
+
try {
|
|
18
|
+
const u = await api.callApi('getUpdates', { offset: -1, timeout: 0, allowed_updates: allowedUpdates });
|
|
19
|
+
if (u.length) offset = u[u.length - 1].update_id + 1;
|
|
20
|
+
} catch { /* abaikan — loop utama yang menangani */ }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
while (!shouldStop()) {
|
|
24
|
+
let updates;
|
|
25
|
+
try {
|
|
26
|
+
updates = await api.callApi('getUpdates', { offset, timeout: 25, allowed_updates: allowedUpdates });
|
|
27
|
+
if (!Array.isArray(updates)) updates = [];
|
|
28
|
+
} catch (err) {
|
|
29
|
+
const code = err && (err.error_code || err.code);
|
|
30
|
+
if (shouldStop()) break;
|
|
31
|
+
if (code === 409) { if (onConflict) onConflict(err); await sleep(conflictDelay); continue; }
|
|
32
|
+
const fatal = code && code !== 429;
|
|
33
|
+
if (fatal) { if (onFatal) onFatal(err); break; }
|
|
34
|
+
await sleep(1000); continue; // koneksi putus / 429 → napas sebentar
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const u of updates) {
|
|
38
|
+
if (shouldStop()) break;
|
|
39
|
+
offset = u.update_id + 1;
|
|
40
|
+
try { await handleUpdate(u); } catch { /* sudah ditangani error handler bot */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { pollLoop };
|
package/lib/session.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// lib/session.js — sesi per-user-per-chat, tanpa konfigurasi.
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* session({ initial, getKey, storage })
|
|
6
|
+
* - default getKey: from.id + chat.id (khas), fallback chat.id.
|
|
7
|
+
* - default storage: Map di memori. Boleh kirim storage custom { read(key), write(key,val), delete(key) }.
|
|
8
|
+
*/
|
|
9
|
+
function session(opts = {}) {
|
|
10
|
+
const initial = opts.initial || (() => ({}));
|
|
11
|
+
const store = opts.storage || new Map();
|
|
12
|
+
const getKey = opts.getKey || ((ctx) => {
|
|
13
|
+
const from = ctx.from && ctx.from.id;
|
|
14
|
+
const chat = ctx.chat && ctx.chat.id;
|
|
15
|
+
if (from !== undefined && chat !== undefined) return `${from}:${chat}`;
|
|
16
|
+
return chat !== undefined ? String(chat) : undefined;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const read = (k) => (store.read ? store.read(k) : store.get(k));
|
|
20
|
+
const write = (k, v) => (store.write ? store.write(k, v) : store.set(k, v));
|
|
21
|
+
const del = (k) => (store.delete ? store.delete(k) : store.delete(k));
|
|
22
|
+
|
|
23
|
+
return async (ctx, next) => {
|
|
24
|
+
const key = getKey(ctx);
|
|
25
|
+
if (key === undefined) { ctx.session = {}; return next(); }
|
|
26
|
+
let s = await read(key);
|
|
27
|
+
if (s === undefined || s === null) { s = initial(); await write(key, s); }
|
|
28
|
+
// Proxy: properti __deleted di akhir → hapus dari store
|
|
29
|
+
ctx.session = new Proxy(s, {
|
|
30
|
+
get: (t, p) => t[p], set: (t, p, v) => { t[p] = v; return true; },
|
|
31
|
+
});
|
|
32
|
+
return next();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { session };
|
package/lib/telebibz.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// lib/telebibz.js — kelas utama: 100% kode sendiri, 0 dependency.
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { makeApi } = require('./api');
|
|
5
|
+
const { Context } = require('./context');
|
|
6
|
+
const { Composer } = require('./composer');
|
|
7
|
+
const { session } = require('./session');
|
|
8
|
+
const { matchInlineQuery } = require('./inline-query');
|
|
9
|
+
const { Menu, MenuContainer } = require('./menus');
|
|
10
|
+
const { pollLoop } = require('./runner');
|
|
11
|
+
const wizard = require('./wizard');
|
|
12
|
+
const log = require('./logger');
|
|
13
|
+
const { humanize } = require('./errors');
|
|
14
|
+
const { broadcast } = require('./broadcast');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_UPDATES = [
|
|
17
|
+
'message', 'edited_message', 'callback_query', 'inline_query',
|
|
18
|
+
'my_chat_member', 'chat_member', 'message_reaction',
|
|
19
|
+
'business_connection', 'business_message', 'edited_business_message',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
class TeleBibz {
|
|
23
|
+
/**
|
|
24
|
+
* new TeleBibz('123:ABC', {
|
|
25
|
+
* allowedUpdates: [...], onError(err, ctx), silent, dropPending,
|
|
26
|
+
* transport: (method, payload) => Promise<result>, // ← suntik untuk test
|
|
27
|
+
* session: {...opsi session}, apiRoot: 'https://api.telegram.org'
|
|
28
|
+
* })
|
|
29
|
+
*/
|
|
30
|
+
constructor(token, opts = {}) {
|
|
31
|
+
if (!token || !/^\d+:[\w-]+$/.test(token)) {
|
|
32
|
+
throw new Error('TeleBibz: token tidak valid. Ambil dari @BotFather → /newbot.');
|
|
33
|
+
}
|
|
34
|
+
this.opts = opts;
|
|
35
|
+
this.token = token;
|
|
36
|
+
this.api = makeApi(token, opts.transport);
|
|
37
|
+
this.botInfo = undefined;
|
|
38
|
+
|
|
39
|
+
this._handlers = new Composer(); // composer publik (use/on/cmd/dll)
|
|
40
|
+
this._report = async (bare) => {
|
|
41
|
+
const err = bare && bare.error ? bare.error : bare;
|
|
42
|
+
const ctx = bare && bare.ctx;
|
|
43
|
+
if (typeof this.opts.onError === 'function') return this.opts.onError(err, ctx);
|
|
44
|
+
this.printError(err, ctx);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Pohon eksekusi: session → wizard → user handlers (dilindungi boundary)
|
|
48
|
+
this._root = new Composer()
|
|
49
|
+
.use(session(opts.session || {}))
|
|
50
|
+
.use(wizard.middleware());
|
|
51
|
+
this._safe = this._root.errorBoundary(this._report);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ---------- shortcut publik ---------- */
|
|
55
|
+
use(...mw) { this._handlers.use(...mw); return this; }
|
|
56
|
+
cmd(names, ...mw) { this._handlers.command(names, ...mw); return this; }
|
|
57
|
+
hears(match, ...mw) {
|
|
58
|
+
this._handlers.hears(match instanceof RegExp || Array.isArray(match) ? match : new RegExp(`^${escapeRe(match)}$`, 'i'), ...mw);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
action(trigs, ...mw) { this._handlers.callbackQuery(trigs, ...mw); return this; }
|
|
62
|
+
|
|
63
|
+
/** Mode inline: bot.inlineQuery(/kucing/, handler) atau bot.inlineQuery('*', h) */
|
|
64
|
+
branch(...a) { this._handlers.branch(...a); return this; }
|
|
65
|
+
filter(...a) { this._handlers.filter(...a); return this; }
|
|
66
|
+
drop(...a) { this._handlers.drop(...a); return this; }
|
|
67
|
+
route(...a) { this._handlers.route(...a); return this; }
|
|
68
|
+
lazy(...a) { this._handlers.lazy(...a); return this; }
|
|
69
|
+
fork(...a) { this._handlers.fork(...a); return this; }
|
|
70
|
+
|
|
71
|
+
inlineQuery(trigger, ...mw) { this._handlers.use(async (ctx, next) => (matchInlineQuery(trigger)(ctx) ? runChain(mw, ctx, next) : next())); return this; }
|
|
72
|
+
|
|
73
|
+
/** Shortcut shortcut: daftarkan daftar perintah ke tombol menu Telegram. */
|
|
74
|
+
setMyCommands(commands, extra = {}) { return this.api.setMyCommands(commands, extra); }
|
|
75
|
+
on(filter, ...mw) { this._handlers.on(filter, ...mw); return this; }
|
|
76
|
+
start(replies) {
|
|
77
|
+
const h = typeof replies === 'function' ? replies : async (ctx) => ctx.reply(replies);
|
|
78
|
+
return this.cmd('start', h);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ---------- wizard ---------- */
|
|
82
|
+
wizard(id, def, bindCommand = true) {
|
|
83
|
+
wizard.define(id, def);
|
|
84
|
+
if (bindCommand) this.cmd(id, (ctx) => wizard.start(ctx, id));
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
wizardStart(ctx, id) { return wizard.start(ctx, id); }
|
|
88
|
+
wizardActive(ctx) { return wizard.active(ctx); }
|
|
89
|
+
wizardCancel(ctx) { return wizard.cancel(ctx); }
|
|
90
|
+
wizardEdit(ctx, text, extra) { return wizard.editAsk(ctx, text, extra); }
|
|
91
|
+
wizardDelete(ctx) { return wizard.deleteAsk(ctx); }
|
|
92
|
+
|
|
93
|
+
/* ---------- broadcast ---------- */
|
|
94
|
+
broadcast(ids, pesan, opts) { return broadcast(this.api, ids, pesan, opts); }
|
|
95
|
+
|
|
96
|
+
/* ---------- siklus hidup ---------- */
|
|
97
|
+
|
|
98
|
+
/** Ambil identitas bot (dipanggil otomatis oleh launch; panggil manual untuk webhook). */
|
|
99
|
+
async init() {
|
|
100
|
+
const me = await this.api.getMe();
|
|
101
|
+
this.botInfo = me;
|
|
102
|
+
return me;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Tangani SATU update mentah (webhook/serverless/test). */
|
|
106
|
+
async handleUpdate(update) {
|
|
107
|
+
if (this._wired !== true) this._wire();
|
|
108
|
+
const ctx = new Context(update, this.api, this.botInfo);
|
|
109
|
+
try { await this._root.run(ctx); }
|
|
110
|
+
catch (err) { await this._report({ error: err, ctx }); }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_wire() {
|
|
114
|
+
this._wired = true;
|
|
115
|
+
// Pasang handler publik SETELAH boundary agar semuanya terlindungi
|
|
116
|
+
this._root.use(this._handlers.middleware());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Nyalakan long polling (dengan retry 409 & backoff jaringan). */
|
|
120
|
+
async launch(launchOpts = {}) {
|
|
121
|
+
await this.init();
|
|
122
|
+
const me = this.botInfo;
|
|
123
|
+
if (!this.opts.silent) {
|
|
124
|
+
log.banner('🤖 TeleBibz ON', [
|
|
125
|
+
`bot : @${me.username} (id ${me.id})`,
|
|
126
|
+
'mode : long-polling',
|
|
127
|
+
`library : telebibz ${require('../package.json').version} (0 dependency)`,
|
|
128
|
+
'brand : //—Xbibz Official—//',
|
|
129
|
+
]);
|
|
130
|
+
log.ok('menunggu update… (Ctrl+C untuk berhenti)');
|
|
131
|
+
}
|
|
132
|
+
this._stopping = false;
|
|
133
|
+
this._runPromise = pollLoop({
|
|
134
|
+
api: this.api,
|
|
135
|
+
handleUpdate: (u) => this.handleUpdate(u),
|
|
136
|
+
allowedUpdates: this.opts.allowedUpdates || DEFAULT_UPDATES,
|
|
137
|
+
dropPending: this.opts.dropPending || launchOpts.dropPending || false,
|
|
138
|
+
shouldStop: () => this._stopping,
|
|
139
|
+
conflictDelay: launchOpts.conflictDelay || 5000,
|
|
140
|
+
onConflict: () => { if (!this.opts.silent) log.warn('bot lain masih polling (409) — coba lagi 5 detik…'); },
|
|
141
|
+
onFatal: (err) => this._report({ error: err }),
|
|
142
|
+
});
|
|
143
|
+
// Ctrl+C → berhenti bersih
|
|
144
|
+
if (!this.opts.silent && !launchOpts.noSignalHandlers) {
|
|
145
|
+
const stop = () => { log.warn('mematikan…'); this.stop(); };
|
|
146
|
+
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
147
|
+
}
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Berhenti polling (resolve runPromise). */
|
|
152
|
+
stop() { this._stopping = true; }
|
|
153
|
+
/** Promise loop polling — ditunggu utk graceful shutdown/test. */
|
|
154
|
+
get runPromise() { return this._runPromise; }
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handler webhook gaya Node/Express:
|
|
158
|
+
* const server = bot.webhook();
|
|
159
|
+
* http.createServer((req,res) => req.url==='/tg' ? server(req,res) : ...)
|
|
160
|
+
* — framework mana pun: (req, res) pasangan Request/Response Node.
|
|
161
|
+
*/
|
|
162
|
+
webhook() {
|
|
163
|
+
return async (req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
const chunks = [];
|
|
166
|
+
for await (const c of req) chunks.push(Buffer.from(c));
|
|
167
|
+
const update = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
168
|
+
await this.handleUpdate(update);
|
|
169
|
+
res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end('{}');
|
|
170
|
+
} catch (e) { res.statusCode = 400; res.end('bad request'); }
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ---------- error cantik ---------- */
|
|
175
|
+
printError(err, ctx) {
|
|
176
|
+
const h = humanize(err);
|
|
177
|
+
log.error(`Telegram error${h.code ? ` (${h.code})` : ''}: ${h.pesan}`);
|
|
178
|
+
if (h.method) log.error(` saat memanggil : ${h.method}`);
|
|
179
|
+
const c = ctx && ctx.chat ? ctx.chat.id : '-';
|
|
180
|
+
if (c !== '-') log.error(` di chat : ${c}`);
|
|
181
|
+
if (h.saran) log.warn(` 💡 saran: ${h.saran}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function runChain(mws, ctx, next) {
|
|
186
|
+
let i = 0;
|
|
187
|
+
const step = () => (i < mws.length ? mws[i++](ctx, step) : next());
|
|
188
|
+
return step();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function escapeRe(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
192
|
+
|
|
193
|
+
module.exports = { TeleBibz };
|