@ouija-dev/plugin-notify-telegram 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/dist/config.d.ts +50 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -0
- package/dist/formatter.d.ts +52 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +93 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/keyboard.d.ts +11 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +17 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/telegram-client.d.ts +67 -0
- package/dist/telegram-client.d.ts.map +1 -0
- package/dist/telegram-client.js +113 -0
- package/dist/telegram-client.js.map +1 -0
- package/package.json +23 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface TelegramConfig {
|
|
2
|
+
/** Telegram Bot API token (from BotFather) */
|
|
3
|
+
botToken: string;
|
|
4
|
+
/** Telegram chat ID to send notifications to (MK's personal chat) */
|
|
5
|
+
chatId: string;
|
|
6
|
+
/** HTML or MarkdownV2. Defaults to HTML — simpler escaping rules. */
|
|
7
|
+
parseMode: 'HTML' | 'MarkdownV2';
|
|
8
|
+
/** When true, messages are delivered silently (no phone notification) */
|
|
9
|
+
disableNotification: boolean;
|
|
10
|
+
/** Base URL for deep links back to Ouija dashboard */
|
|
11
|
+
dashboardBaseUrl: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* JSON Schema for Ajv validation at plugin load time.
|
|
15
|
+
* Matches TelegramConfig shape exactly.
|
|
16
|
+
*/
|
|
17
|
+
export declare const telegramConfigSchema: {
|
|
18
|
+
readonly type: "object";
|
|
19
|
+
readonly required: readonly ["botToken", "chatId"];
|
|
20
|
+
readonly properties: {
|
|
21
|
+
readonly botToken: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
readonly minLength: 1;
|
|
24
|
+
readonly description: "Telegram Bot API token (from BotFather)";
|
|
25
|
+
};
|
|
26
|
+
readonly chatId: {
|
|
27
|
+
readonly type: "string";
|
|
28
|
+
readonly minLength: 1;
|
|
29
|
+
readonly description: "Target chat ID for notifications";
|
|
30
|
+
};
|
|
31
|
+
readonly parseMode: {
|
|
32
|
+
readonly type: "string";
|
|
33
|
+
readonly enum: readonly ["HTML", "MarkdownV2"];
|
|
34
|
+
readonly default: "HTML";
|
|
35
|
+
readonly description: "Telegram message parse mode";
|
|
36
|
+
};
|
|
37
|
+
readonly disableNotification: {
|
|
38
|
+
readonly type: "boolean";
|
|
39
|
+
readonly default: false;
|
|
40
|
+
readonly description: "Send messages silently without phone notification";
|
|
41
|
+
};
|
|
42
|
+
readonly dashboardBaseUrl: {
|
|
43
|
+
readonly type: "string";
|
|
44
|
+
readonly default: "http://localhost:4000";
|
|
45
|
+
readonly description: "Base URL for dashboard deep links";
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
readonly additionalProperties: false;
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,SAAS,EAAE,MAAM,GAAG,YAAY,CAAC;IACjC,yEAAyE;IACzE,mBAAmB,EAAE,OAAO,CAAC;IAC7B,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCvB,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ---- Telegram plugin configuration ----
|
|
2
|
+
/**
|
|
3
|
+
* JSON Schema for Ajv validation at plugin load time.
|
|
4
|
+
* Matches TelegramConfig shape exactly.
|
|
5
|
+
*/
|
|
6
|
+
export const telegramConfigSchema = {
|
|
7
|
+
type: 'object',
|
|
8
|
+
required: ['botToken', 'chatId'],
|
|
9
|
+
properties: {
|
|
10
|
+
botToken: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
minLength: 1,
|
|
13
|
+
description: 'Telegram Bot API token (from BotFather)',
|
|
14
|
+
},
|
|
15
|
+
chatId: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
minLength: 1,
|
|
18
|
+
description: 'Target chat ID for notifications',
|
|
19
|
+
},
|
|
20
|
+
parseMode: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: ['HTML', 'MarkdownV2'],
|
|
23
|
+
default: 'HTML',
|
|
24
|
+
description: 'Telegram message parse mode',
|
|
25
|
+
},
|
|
26
|
+
disableNotification: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
default: false,
|
|
29
|
+
description: 'Send messages silently without phone notification',
|
|
30
|
+
},
|
|
31
|
+
dashboardBaseUrl: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
default: 'http://localhost:4000',
|
|
34
|
+
description: 'Base URL for dashboard deep links',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAe1C;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;IAChC,UAAU,EAAE;QACV,QAAQ,EAAE;YACR,IAAI,EAAE,QAAQ;YACd,SAAS,EAAE,CAAC;YACZ,WAAW,EAAE,yCAAyC;SACvD;QACD,MAAM,EAAE;YACN,IAAI,EAAE,QAAQ;YACd,SAAS,EAAE,CAAC;YACZ,WAAW,EAAE,kCAAkC;SAChD;QACD,SAAS,EAAE;YACT,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,CAAC,MAAM,EAAE,YAAY,CAAC;YAC5B,OAAO,EAAE,MAAM;YACf,WAAW,EAAE,6BAA6B;SAC3C;QACD,mBAAmB,EAAE;YACnB,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,KAAK;YACd,WAAW,EAAE,mDAAmD;SACjE;QACD,gBAAgB,EAAE;YAChB,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,uBAAuB;YAChC,WAAW,EAAE,mCAAmC;SACjD;KACF;IACD,oBAAoB,EAAE,KAAK;CACnB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Notification } from '@ouija-dev/types';
|
|
2
|
+
export interface DispatchStartedData {
|
|
3
|
+
agentName: string;
|
|
4
|
+
cardTitle: string;
|
|
5
|
+
cardId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PrReadyData {
|
|
8
|
+
prUrl: string;
|
|
9
|
+
prTitle: string;
|
|
10
|
+
repoName: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AgentFailedData {
|
|
13
|
+
agentName: string;
|
|
14
|
+
errorMessage: string;
|
|
15
|
+
pipelineId?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface StallDetectedData {
|
|
18
|
+
agentName: string;
|
|
19
|
+
stalledMinutes: number;
|
|
20
|
+
pipelineId?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Escape HTML special characters for Telegram HTML parse mode.
|
|
24
|
+
* Telegram HTML requires escaping: & < >
|
|
25
|
+
*/
|
|
26
|
+
export declare function escapeHtml(text: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Format any Notification into Telegram HTML text.
|
|
29
|
+
* Renders: icon + bold title + blank line + body.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatNotification(notification: Notification): string;
|
|
32
|
+
/**
|
|
33
|
+
* Agent started working on a card.
|
|
34
|
+
* "Agent rex-coder started working on Card: Fix login bug"
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatDispatchStarted(data: DispatchStartedData): string;
|
|
37
|
+
/**
|
|
38
|
+
* Agent opened a PR.
|
|
39
|
+
* "PR ready for review: [link]" with [View PR] button.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatPrReady(data: PrReadyData): string;
|
|
42
|
+
/**
|
|
43
|
+
* Agent failed with an error.
|
|
44
|
+
* "Agent failed: error message" with [Retry] [View] buttons.
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatAgentFailed(data: AgentFailedData): string;
|
|
47
|
+
/**
|
|
48
|
+
* Agent stalled — no heartbeat for N minutes.
|
|
49
|
+
* "Agent stalled — no heartbeat for 5m" with [Retry] [Cancel] buttons.
|
|
50
|
+
*/
|
|
51
|
+
export declare function formatStallDetected(data: StallDetectedData): string;
|
|
52
|
+
//# sourceMappingURL=formatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.d.ts","sourceRoot":"","sources":["../src/formatter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAqB,MAAM,kBAAkB,CAAC;AAIxE,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/C;AAaD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,YAAY,GAAG,MAAM,CAUrE;AAID;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAWvE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAUvD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAU/D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CASnE"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ---- Notification -> Telegram HTML message formatter ----
|
|
2
|
+
// Uses HTML parse mode throughout — simpler escaping than MarkdownV2.
|
|
3
|
+
// ---- HTML escaping ----
|
|
4
|
+
/**
|
|
5
|
+
* Escape HTML special characters for Telegram HTML parse mode.
|
|
6
|
+
* Telegram HTML requires escaping: & < >
|
|
7
|
+
*/
|
|
8
|
+
export function escapeHtml(text) {
|
|
9
|
+
return text
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>');
|
|
13
|
+
}
|
|
14
|
+
// ---- Level icons ----
|
|
15
|
+
const LEVEL_ICON = {
|
|
16
|
+
info: '\u2139\uFE0F', // information
|
|
17
|
+
warning: '\u26A0\uFE0F', // warning
|
|
18
|
+
error: '\u274C', // red X
|
|
19
|
+
success: '\u2705', // green check
|
|
20
|
+
};
|
|
21
|
+
// ---- Generic notification formatter ----
|
|
22
|
+
/**
|
|
23
|
+
* Format any Notification into Telegram HTML text.
|
|
24
|
+
* Renders: icon + bold title + blank line + body.
|
|
25
|
+
*/
|
|
26
|
+
export function formatNotification(notification) {
|
|
27
|
+
const icon = LEVEL_ICON[notification.level];
|
|
28
|
+
const title = escapeHtml(notification.title);
|
|
29
|
+
const body = escapeHtml(notification.body);
|
|
30
|
+
return [
|
|
31
|
+
`${icon} <b>${title}</b>`,
|
|
32
|
+
'',
|
|
33
|
+
body,
|
|
34
|
+
].join('\n');
|
|
35
|
+
}
|
|
36
|
+
// ---- Typed pipeline event formatters ----
|
|
37
|
+
/**
|
|
38
|
+
* Agent started working on a card.
|
|
39
|
+
* "Agent rex-coder started working on Card: Fix login bug"
|
|
40
|
+
*/
|
|
41
|
+
export function formatDispatchStarted(data) {
|
|
42
|
+
const agentName = escapeHtml(data.agentName);
|
|
43
|
+
const cardTitle = escapeHtml(data.cardTitle);
|
|
44
|
+
const cardId = escapeHtml(data.cardId);
|
|
45
|
+
return [
|
|
46
|
+
`\u2139\uFE0F <b>Agent dispatched</b>`,
|
|
47
|
+
'',
|
|
48
|
+
`<b>${agentName}</b> started working on Card: <code>${cardId}</code>`,
|
|
49
|
+
`<i>${cardTitle}</i>`,
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Agent opened a PR.
|
|
54
|
+
* "PR ready for review: [link]" with [View PR] button.
|
|
55
|
+
*/
|
|
56
|
+
export function formatPrReady(data) {
|
|
57
|
+
const prTitle = escapeHtml(data.prTitle);
|
|
58
|
+
const repoName = escapeHtml(data.repoName);
|
|
59
|
+
const prUrl = escapeHtml(data.prUrl);
|
|
60
|
+
return [
|
|
61
|
+
`\u2705 <b>PR ready for review</b>`,
|
|
62
|
+
'',
|
|
63
|
+
`<b>${repoName}</b>: <a href="${prUrl}">${prTitle}</a>`,
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Agent failed with an error.
|
|
68
|
+
* "Agent failed: error message" with [Retry] [View] buttons.
|
|
69
|
+
*/
|
|
70
|
+
export function formatAgentFailed(data) {
|
|
71
|
+
const agentName = escapeHtml(data.agentName);
|
|
72
|
+
const errorMessage = escapeHtml(data.errorMessage);
|
|
73
|
+
return [
|
|
74
|
+
`\u274C <b>Agent failed</b>`,
|
|
75
|
+
'',
|
|
76
|
+
`<b>${agentName}</b> encountered an error:`,
|
|
77
|
+
`<code>${errorMessage}</code>`,
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Agent stalled — no heartbeat for N minutes.
|
|
82
|
+
* "Agent stalled — no heartbeat for 5m" with [Retry] [Cancel] buttons.
|
|
83
|
+
*/
|
|
84
|
+
export function formatStallDetected(data) {
|
|
85
|
+
const agentName = escapeHtml(data.agentName);
|
|
86
|
+
const minutes = data.stalledMinutes;
|
|
87
|
+
return [
|
|
88
|
+
`\u26A0\uFE0F <b>Agent stalled</b>`,
|
|
89
|
+
'',
|
|
90
|
+
`<b>${agentName}</b> — no heartbeat for <b>${minutes}m</b>`,
|
|
91
|
+
].join('\n');
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=formatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../src/formatter.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,sEAAsE;AA8BtE,0BAA0B;AAE1B;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI;SACR,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,wBAAwB;AAExB,MAAM,UAAU,GAAsC;IACpD,IAAI,EAAE,cAAc,EAAK,cAAc;IACvC,OAAO,EAAE,cAAc,EAAE,UAAU;IACnC,KAAK,EAAE,QAAQ,EAAW,QAAQ;IAClC,OAAO,EAAE,QAAQ,EAAS,cAAc;CACzC,CAAC;AAEF,2CAA2C;AAE3C;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,YAA0B;IAC3D,MAAM,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAE3C,OAAO;QACL,GAAG,IAAI,OAAO,KAAK,MAAM;QACzB,EAAE;QACF,IAAI;KACL,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,4CAA4C;AAE5C;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAyB;IAC7D,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEvC,OAAO;QACL,sCAAsC;QACtC,EAAE;QACF,MAAM,SAAS,uCAAuC,MAAM,SAAS;QACrE,MAAM,SAAS,MAAM;KACtB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAiB;IAC7C,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAErC,OAAO;QACL,mCAAmC;QACnC,EAAE;QACF,MAAM,QAAQ,kBAAkB,KAAK,KAAK,OAAO,MAAM;KACxD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAqB;IACrD,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,YAAY,GAAG,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAEnD,OAAO;QACL,4BAA4B;QAC5B,EAAE;QACF,MAAM,SAAS,4BAA4B;QAC3C,SAAS,YAAY,SAAS;KAC/B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAuB;IACzD,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC;IAEpC,OAAO;QACL,mCAAmC;QACnC,EAAE;QACF,MAAM,SAAS,8BAA8B,OAAO,OAAO;KAC5D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { NotificationPlugin, Notification, PluginManifest, PluginContext, PluginHealth } from '@ouija-dev/types';
|
|
2
|
+
import type { TelegramConfig } from './config.js';
|
|
3
|
+
import { TelegramClient } from './telegram-client.js';
|
|
4
|
+
export declare class TelegramNotifyPlugin implements NotificationPlugin<TelegramConfig> {
|
|
5
|
+
readonly manifest: PluginManifest;
|
|
6
|
+
private config;
|
|
7
|
+
private logger;
|
|
8
|
+
/** Exposed for testing — allows injecting a mock fetch into the underlying client. */
|
|
9
|
+
client: TelegramClient;
|
|
10
|
+
/**
|
|
11
|
+
* LRU-ish idempotency cache: idempotencyKey -> true.
|
|
12
|
+
* Prevents duplicate sends on retry within a single process lifetime.
|
|
13
|
+
*/
|
|
14
|
+
private readonly sentCache;
|
|
15
|
+
init(context: PluginContext<TelegramConfig>): Promise<void>;
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
healthCheck(): Promise<PluginHealth>;
|
|
19
|
+
/**
|
|
20
|
+
* Send a notification to the configured Telegram chat.
|
|
21
|
+
* Idempotent on notification.idempotencyKey — duplicate calls are no-ops.
|
|
22
|
+
*/
|
|
23
|
+
send(notification: Notification): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Verify the bot token and channel reachability without sending a message.
|
|
26
|
+
* Calls getMe() which has no side effects.
|
|
27
|
+
*/
|
|
28
|
+
testConnection(): Promise<{
|
|
29
|
+
ok: boolean;
|
|
30
|
+
message?: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export declare const PluginFactory: {
|
|
34
|
+
manifest: PluginManifest;
|
|
35
|
+
create: () => TelegramNotifyPlugin;
|
|
36
|
+
};
|
|
37
|
+
export default PluginFactory;
|
|
38
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,YAAY,EACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,cAAc,EAAoB,MAAM,sBAAsB,CAAC;AAUxE,qBAAa,oBAAqB,YAAW,kBAAkB,CAAC,cAAc,CAAC;IAC7E,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAgB/B;IAEF,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,MAAM,CAA2B;IACzC,sFAAsF;IACtF,MAAM,EAAG,cAAc,CAAC;IAExB;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2B;IAE/C,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB3D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAatB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrB,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC;IAkB1C;;;OAGG;IACG,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCrD;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAenE;AAID,eAAO,MAAM,aAAa;;kBAEZ,oBAAoB;CACjC,CAAC;AAEF,eAAe,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// ---- Telegram Notification Plugin ----
|
|
2
|
+
// Implements NotificationPlugin<TelegramConfig>.
|
|
3
|
+
// Zero external SDK dependencies — raw fetch via TelegramClient.
|
|
4
|
+
import { telegramConfigSchema } from './config.js';
|
|
5
|
+
import { TelegramClient, TelegramApiError } from './telegram-client.js';
|
|
6
|
+
import { formatNotification } from './formatter.js';
|
|
7
|
+
import { buildInlineKeyboard } from './keyboard.js';
|
|
8
|
+
// ---- Idempotency cache ----
|
|
9
|
+
const SENT_CACHE_MAX = 1_000;
|
|
10
|
+
// ---- Plugin implementation ----
|
|
11
|
+
export class TelegramNotifyPlugin {
|
|
12
|
+
manifest = {
|
|
13
|
+
name: '@ouija-dev/plugin-notify-telegram',
|
|
14
|
+
version: '0.1.0',
|
|
15
|
+
type: 'notification',
|
|
16
|
+
coreApiVersion: '>=1.0.0 <2.0.0',
|
|
17
|
+
configSchema: telegramConfigSchema,
|
|
18
|
+
events: {
|
|
19
|
+
produces: [],
|
|
20
|
+
consumes: [
|
|
21
|
+
'notification.send',
|
|
22
|
+
'agent.work.completed',
|
|
23
|
+
'agent.work.failed',
|
|
24
|
+
'agent.work.pr_ready',
|
|
25
|
+
'agent.work.stalled',
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
config;
|
|
30
|
+
logger;
|
|
31
|
+
/** Exposed for testing — allows injecting a mock fetch into the underlying client. */
|
|
32
|
+
client;
|
|
33
|
+
/**
|
|
34
|
+
* LRU-ish idempotency cache: idempotencyKey -> true.
|
|
35
|
+
* Prevents duplicate sends on retry within a single process lifetime.
|
|
36
|
+
*/
|
|
37
|
+
sentCache = new Map();
|
|
38
|
+
async init(context) {
|
|
39
|
+
// Apply defaults for optional fields that may be absent in the raw config.
|
|
40
|
+
// Spread defaults first so caller-provided values always win.
|
|
41
|
+
const cfg = context.config;
|
|
42
|
+
this.config = {
|
|
43
|
+
parseMode: cfg.parseMode ?? 'HTML',
|
|
44
|
+
disableNotification: cfg.disableNotification ?? false,
|
|
45
|
+
dashboardBaseUrl: cfg.dashboardBaseUrl ?? 'http://localhost:4000',
|
|
46
|
+
botToken: cfg.botToken,
|
|
47
|
+
chatId: cfg.chatId,
|
|
48
|
+
};
|
|
49
|
+
this.logger = context.logger;
|
|
50
|
+
this.client = new TelegramClient(this.config.botToken);
|
|
51
|
+
this.logger.info('Telegram notification plugin initialised', {
|
|
52
|
+
chatId: this.config.chatId,
|
|
53
|
+
parseMode: this.config.parseMode,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async start() {
|
|
57
|
+
// Eagerly verify the bot token on startup so misconfiguration surfaces
|
|
58
|
+
// immediately rather than on the first notification attempt.
|
|
59
|
+
const result = await this.testConnection();
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
this.logger.warn('Telegram bot token verification failed at startup', {
|
|
62
|
+
message: result.message,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.logger.info('Telegram notification plugin started', { bot: result.message });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async stop() {
|
|
70
|
+
this.sentCache.clear();
|
|
71
|
+
this.logger.info('Telegram notification plugin stopped');
|
|
72
|
+
}
|
|
73
|
+
async healthCheck() {
|
|
74
|
+
try {
|
|
75
|
+
const result = await this.testConnection();
|
|
76
|
+
const health = { healthy: result.ok };
|
|
77
|
+
if (result.message !== undefined) {
|
|
78
|
+
health.message = result.message;
|
|
79
|
+
}
|
|
80
|
+
return health;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return {
|
|
84
|
+
healthy: false,
|
|
85
|
+
message: `Health check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ---- NotificationPlugin ----
|
|
90
|
+
/**
|
|
91
|
+
* Send a notification to the configured Telegram chat.
|
|
92
|
+
* Idempotent on notification.idempotencyKey — duplicate calls are no-ops.
|
|
93
|
+
*/
|
|
94
|
+
async send(notification) {
|
|
95
|
+
if (this.sentCache.has(notification.idempotencyKey)) {
|
|
96
|
+
this.logger.info('Duplicate notification skipped', {
|
|
97
|
+
idempotencyKey: notification.idempotencyKey,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const text = formatNotification(notification);
|
|
102
|
+
const keyboard = buildInlineKeyboard(notification.actions);
|
|
103
|
+
const sendOpts = {
|
|
104
|
+
parseMode: this.config.parseMode,
|
|
105
|
+
disableNotification: this.config.disableNotification,
|
|
106
|
+
};
|
|
107
|
+
if (keyboard !== undefined) {
|
|
108
|
+
sendOpts.replyMarkup = keyboard;
|
|
109
|
+
}
|
|
110
|
+
await this.client.sendMessage(this.config.chatId, text, sendOpts);
|
|
111
|
+
// Track sent — evict oldest entry when cache is full to bound memory usage.
|
|
112
|
+
if (this.sentCache.size >= SENT_CACHE_MAX) {
|
|
113
|
+
const firstKey = this.sentCache.keys().next().value;
|
|
114
|
+
if (firstKey !== undefined)
|
|
115
|
+
this.sentCache.delete(firstKey);
|
|
116
|
+
}
|
|
117
|
+
this.sentCache.set(notification.idempotencyKey, true);
|
|
118
|
+
this.logger.info('Notification sent', {
|
|
119
|
+
title: notification.title,
|
|
120
|
+
level: notification.level,
|
|
121
|
+
idempotencyKey: notification.idempotencyKey,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Verify the bot token and channel reachability without sending a message.
|
|
126
|
+
* Calls getMe() which has no side effects.
|
|
127
|
+
*/
|
|
128
|
+
async testConnection() {
|
|
129
|
+
try {
|
|
130
|
+
const user = await this.client.getMe();
|
|
131
|
+
const botName = user.username !== undefined ? `@${user.username}` : user.first_name;
|
|
132
|
+
return { ok: true, message: `Connected as ${botName}` };
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err instanceof TelegramApiError) {
|
|
136
|
+
return { ok: false, message: err.description ?? err.message };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
message: `Connection failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ---- Plugin factory (consumed by PluginLoader) ----
|
|
146
|
+
export const PluginFactory = {
|
|
147
|
+
manifest: new TelegramNotifyPlugin().manifest,
|
|
148
|
+
create: () => new TelegramNotifyPlugin(),
|
|
149
|
+
};
|
|
150
|
+
export default PluginFactory;
|
|
151
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NotificationAction } from '@ouija-dev/types';
|
|
2
|
+
import type { TelegramInlineKeyboard, TelegramInlineButton } from './telegram-client.js';
|
|
3
|
+
export type { TelegramInlineKeyboard, TelegramInlineButton };
|
|
4
|
+
/**
|
|
5
|
+
* Build a Telegram inline keyboard from Notification actions.
|
|
6
|
+
* Each action becomes a single-button row (max 3 rows for mobile readability).
|
|
7
|
+
*
|
|
8
|
+
* Returns undefined when there are no actions — caller omits reply_markup.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildInlineKeyboard(actions: NotificationAction[] | undefined): TelegramInlineKeyboard | undefined;
|
|
11
|
+
//# sourceMappingURL=keyboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../src/keyboard.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEzF,YAAY,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,CAAC;AAE7D;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,kBAAkB,EAAE,GAAG,SAAS,GACxC,sBAAsB,GAAG,SAAS,CASpC"}
|
package/dist/keyboard.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ---- Inline keyboard builder for Telegram notifications ----
|
|
2
|
+
/**
|
|
3
|
+
* Build a Telegram inline keyboard from Notification actions.
|
|
4
|
+
* Each action becomes a single-button row (max 3 rows for mobile readability).
|
|
5
|
+
*
|
|
6
|
+
* Returns undefined when there are no actions — caller omits reply_markup.
|
|
7
|
+
*/
|
|
8
|
+
export function buildInlineKeyboard(actions) {
|
|
9
|
+
if (actions === undefined || actions.length === 0)
|
|
10
|
+
return undefined;
|
|
11
|
+
// Cap at 3 rows to keep the message mobile-friendly
|
|
12
|
+
const rows = actions.slice(0, 3).map((action) => [
|
|
13
|
+
{ text: action.label, url: action.url },
|
|
14
|
+
]);
|
|
15
|
+
return { inline_keyboard: rows };
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=keyboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard.js","sourceRoot":"","sources":["../src/keyboard.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAO/D;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAyC;IAEzC,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAEpE,oDAAoD;IACpD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAA0B,EAAE,CAAC;QACvE,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE;KACxC,CAAC,CAAC;IAEH,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface TelegramResponse<T = unknown> {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
result?: T;
|
|
4
|
+
description?: string;
|
|
5
|
+
error_code?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface TelegramUser {
|
|
8
|
+
id: number;
|
|
9
|
+
is_bot: boolean;
|
|
10
|
+
first_name: string;
|
|
11
|
+
username?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TelegramMessage {
|
|
14
|
+
message_id: number;
|
|
15
|
+
date: number;
|
|
16
|
+
text?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface TelegramInlineButton {
|
|
19
|
+
text: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}
|
|
22
|
+
export interface TelegramInlineKeyboard {
|
|
23
|
+
inline_keyboard: TelegramInlineButton[][];
|
|
24
|
+
}
|
|
25
|
+
export interface SendMessageOptions {
|
|
26
|
+
parseMode?: 'HTML' | 'MarkdownV2';
|
|
27
|
+
disableNotification?: boolean;
|
|
28
|
+
replyMarkup?: TelegramInlineKeyboard;
|
|
29
|
+
}
|
|
30
|
+
export declare class TelegramApiError extends Error {
|
|
31
|
+
readonly errorCode?: number | undefined;
|
|
32
|
+
readonly description?: string | undefined;
|
|
33
|
+
constructor(message: string, errorCode?: number | undefined, description?: string | undefined);
|
|
34
|
+
}
|
|
35
|
+
export declare class TelegramRateLimitError extends TelegramApiError {
|
|
36
|
+
readonly retryAfterMs: number;
|
|
37
|
+
constructor(retryAfterMs: number);
|
|
38
|
+
}
|
|
39
|
+
export declare class TelegramClient {
|
|
40
|
+
private readonly botToken;
|
|
41
|
+
private readonly baseUrl;
|
|
42
|
+
private lastSendTimes;
|
|
43
|
+
/** Override for testing — allows injecting a mock fetch. */
|
|
44
|
+
_fetchFn: typeof fetch;
|
|
45
|
+
constructor(botToken: string);
|
|
46
|
+
/**
|
|
47
|
+
* Call a Telegram Bot API method.
|
|
48
|
+
* Handles 429 rate limit errors and surfaces typed errors.
|
|
49
|
+
*/
|
|
50
|
+
private call;
|
|
51
|
+
/**
|
|
52
|
+
* Send a text message to a chat.
|
|
53
|
+
* Enforces per-chat rate limiting (30 msg/sec).
|
|
54
|
+
*/
|
|
55
|
+
sendMessage(chatId: string, text: string, options?: SendMessageOptions): Promise<TelegramMessage>;
|
|
56
|
+
/**
|
|
57
|
+
* Call getMe to verify the bot token and retrieve bot info.
|
|
58
|
+
* Used as a health check — no side effects.
|
|
59
|
+
*/
|
|
60
|
+
getMe(): Promise<TelegramUser>;
|
|
61
|
+
/**
|
|
62
|
+
* Enforce per-chat rate limit: >= MIN_INTERVAL_MS between sends.
|
|
63
|
+
* Sleeps only the remaining time if a send happened recently.
|
|
64
|
+
*/
|
|
65
|
+
private enforceRateLimit;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=telegram-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram-client.d.ts","sourceRoot":"","sources":["../src/telegram-client.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,OAAO;IAC3C,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,CAAC,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,sBAAsB;IACrC,eAAe,EAAE,oBAAoB,EAAE,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAClC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,WAAW,CAAC,EAAE,sBAAsB,CAAC;CACtC;AAWD,qBAAa,gBAAiB,SAAQ,KAAK;aAGvB,SAAS,CAAC,EAAE,MAAM;aAClB,WAAW,CAAC,EAAE,MAAM;gBAFpC,OAAO,EAAE,MAAM,EACC,SAAS,CAAC,EAAE,MAAM,YAAA,EAClB,WAAW,CAAC,EAAE,MAAM,YAAA;CAKvC;AAED,qBAAa,sBAAuB,SAAQ,gBAAgB;aAExC,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAKvC;AAID,qBAAa,cAAc;IAOb,OAAO,CAAC,QAAQ,CAAC,QAAQ;IANrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,aAAa,CAA6B;IAElD,4DAA4D;IAC5D,QAAQ,EAAE,OAAO,KAAK,CAAqC;gBAE9B,QAAQ,EAAE,MAAM;IAI7C;;;OAGG;YACW,IAAI;IAoClB;;;OAGG;IACG,WAAW,CACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,eAAe,CAAC;IAyB3B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC;IAIpC;;;OAGG;YACW,gBAAgB;CAS/B"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ---- Raw Telegram Bot API client ----
|
|
2
|
+
// No SDK dependency. Uses global fetch (Node 18+).
|
|
3
|
+
// ---- Rate limiting ----
|
|
4
|
+
// Telegram allows 30 messages/second to the same chat.
|
|
5
|
+
// We enforce a minimum 34ms gap between sends to the same chat to stay safely
|
|
6
|
+
// within that limit, and add a short sleep when we receive a 429.
|
|
7
|
+
const MIN_INTERVAL_MS = 34; // ~29 msg/sec, comfortably under the 30/sec limit
|
|
8
|
+
// ---- Error classes ----
|
|
9
|
+
export class TelegramApiError extends Error {
|
|
10
|
+
errorCode;
|
|
11
|
+
description;
|
|
12
|
+
constructor(message, errorCode, description) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.errorCode = errorCode;
|
|
15
|
+
this.description = description;
|
|
16
|
+
this.name = 'TelegramApiError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class TelegramRateLimitError extends TelegramApiError {
|
|
20
|
+
retryAfterMs;
|
|
21
|
+
constructor(retryAfterMs) {
|
|
22
|
+
super(`Rate limited — retry after ${retryAfterMs}ms`, 429, 'Too Many Requests');
|
|
23
|
+
this.retryAfterMs = retryAfterMs;
|
|
24
|
+
this.name = 'TelegramRateLimitError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// ---- Client ----
|
|
28
|
+
export class TelegramClient {
|
|
29
|
+
botToken;
|
|
30
|
+
baseUrl;
|
|
31
|
+
lastSendTimes = new Map();
|
|
32
|
+
/** Override for testing — allows injecting a mock fetch. */
|
|
33
|
+
_fetchFn = globalThis.fetch.bind(globalThis);
|
|
34
|
+
constructor(botToken) {
|
|
35
|
+
this.botToken = botToken;
|
|
36
|
+
this.baseUrl = `https://api.telegram.org/bot${botToken}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Call a Telegram Bot API method.
|
|
40
|
+
* Handles 429 rate limit errors and surfaces typed errors.
|
|
41
|
+
*/
|
|
42
|
+
async call(method, body) {
|
|
43
|
+
const url = `${this.baseUrl}/${method}`;
|
|
44
|
+
// Build RequestInit carefully to satisfy exactOptionalPropertyTypes — no
|
|
45
|
+
// undefined values in the object literal where the type expects absence.
|
|
46
|
+
const init = body !== undefined
|
|
47
|
+
? {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
}
|
|
52
|
+
: { method: 'GET' };
|
|
53
|
+
const response = await this._fetchFn(url, init);
|
|
54
|
+
// Handle Telegram's 429 before parsing JSON — it always includes Retry-After
|
|
55
|
+
if (response.status === 429) {
|
|
56
|
+
const retryAfterSec = parseInt(response.headers.get('Retry-After') ?? '1', 10);
|
|
57
|
+
const retryAfterMs = retryAfterSec * 1000;
|
|
58
|
+
throw new TelegramRateLimitError(retryAfterMs);
|
|
59
|
+
}
|
|
60
|
+
const result = (await response.json());
|
|
61
|
+
if (!result.ok) {
|
|
62
|
+
throw new TelegramApiError(`Telegram API error [${method}]: ${result.description ?? 'Unknown error'}`, result.error_code, result.description);
|
|
63
|
+
}
|
|
64
|
+
return result.result;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Send a text message to a chat.
|
|
68
|
+
* Enforces per-chat rate limiting (30 msg/sec).
|
|
69
|
+
*/
|
|
70
|
+
async sendMessage(chatId, text, options) {
|
|
71
|
+
await this.enforceRateLimit(chatId);
|
|
72
|
+
const body = {
|
|
73
|
+
chat_id: chatId,
|
|
74
|
+
text,
|
|
75
|
+
};
|
|
76
|
+
if (options?.parseMode !== undefined) {
|
|
77
|
+
body['parse_mode'] = options.parseMode;
|
|
78
|
+
}
|
|
79
|
+
if (options?.disableNotification === true) {
|
|
80
|
+
body['disable_notification'] = true;
|
|
81
|
+
}
|
|
82
|
+
if (options?.replyMarkup !== undefined) {
|
|
83
|
+
body['reply_markup'] = JSON.stringify(options.replyMarkup);
|
|
84
|
+
}
|
|
85
|
+
const message = await this.call('sendMessage', body);
|
|
86
|
+
this.lastSendTimes.set(chatId, Date.now());
|
|
87
|
+
return message;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Call getMe to verify the bot token and retrieve bot info.
|
|
91
|
+
* Used as a health check — no side effects.
|
|
92
|
+
*/
|
|
93
|
+
async getMe() {
|
|
94
|
+
return this.call('getMe');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Enforce per-chat rate limit: >= MIN_INTERVAL_MS between sends.
|
|
98
|
+
* Sleeps only the remaining time if a send happened recently.
|
|
99
|
+
*/
|
|
100
|
+
async enforceRateLimit(chatId) {
|
|
101
|
+
const last = this.lastSendTimes.get(chatId);
|
|
102
|
+
if (last !== undefined) {
|
|
103
|
+
const elapsed = Date.now() - last;
|
|
104
|
+
if (elapsed < MIN_INTERVAL_MS) {
|
|
105
|
+
await sleep(MIN_INTERVAL_MS - elapsed);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function sleep(ms) {
|
|
111
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=telegram-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram-client.js","sourceRoot":"","sources":["../src/telegram-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ouija-dev/plugin-notify-telegram",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist/", "README.md"],
|
|
8
|
+
"publishConfig": { "access": "public" },
|
|
9
|
+
"repository": { "type": "git", "url": "https://github.com/muhammadkh4n/ouija.git" },
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@ouija-dev/types": "*",
|
|
17
|
+
"@ouija-dev/plugin-sdk": "*"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.5.0",
|
|
21
|
+
"vitest": "^3.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|