@amirhosseinnouri/send 1.0.0-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # @amirhosseinnouri/send
2
+
3
+ Send a **structured message** to a chat provider — from code or the command line.
4
+
5
+ Supported providers: **Slack**, **Microsoft Teams**, **Telegram**, **Element/Matrix**, **Mattermost**.
6
+
7
+ A message is provider-agnostic:
8
+
9
+ ```ts
10
+ interface Message {
11
+ title?: string;
12
+ body: string;
13
+ markdown?: boolean; // render the body as markdown in the provider's native format
14
+ }
15
+ ```
16
+
17
+ Each provider renders it into its own native format (Slack blocks, Teams MessageCard,
18
+ Telegram/Element markdown, Mattermost attachment). With `markdown: true` the body is
19
+ rendered as rich markdown; otherwise it is sent as plain text.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ npm i @amirhosseinnouri/send
25
+ # or run directly, no install:
26
+ npx @amirhosseinnouri/send --help
27
+ ```
28
+
29
+ ## Programmatic API
30
+
31
+ ```ts
32
+ import { send, createSender } from "@amirhosseinnouri/send";
33
+
34
+ // one-shot — flat config + message
35
+ await send({
36
+ provider: "slack",
37
+ webhookUrl: "https://hooks.slack.com/services/...",
38
+ title: "Release 1.2.0",
39
+ body: "## Changes\n- Fixed a bug",
40
+ markdown: true,
41
+ });
42
+
43
+ // reusable sender bound to one provider
44
+ const sender = createSender({ provider: "telegram", botToken: "T", chatId: "42" });
45
+ await sender.send({ body: "hello" });
46
+ ```
47
+
48
+ Exports also include the zod schemas (`senderConfigSchema`, `messageSchema`,
49
+ `providerConfigSchemas`, ...) and the `PROVIDERS` list for consumers that want to
50
+ validate input or reuse the per-provider config shapes.
51
+
52
+ ### Provider config
53
+
54
+ | provider | config keys |
55
+ | ------------ | ---------------------------------------------- |
56
+ | `slack` | `webhookUrl` |
57
+ | `teams` | `webhookUrl` |
58
+ | `mattermost` | `webhookUrl` |
59
+ | `telegram` | `botToken`, `chatId` |
60
+ | `element` | `homeserverUrl`, `accessToken`, `roomId` |
61
+
62
+ ## CLI
63
+
64
+ ```sh
65
+ send --provider slack --webhook-url <url> \
66
+ --title "Release 1.2" --body "Bug **fixes**" --markdown
67
+ ```
68
+
69
+ - Provider config flags: `--webhook-url` (slack/teams/mattermost);
70
+ `--bot-token --chat-id` (telegram);
71
+ `--homeserver-url --access-token --room-id` (element).
72
+ - Body: `--body <text>` (required).
73
+ - `--title` optional. `--markdown` renders the body as markdown (off by default).
74
+
75
+ ```sh
76
+ send --provider telegram --bot-token T --chat-id 42 --title Hi --body "the body"
77
+ ```
78
+
79
+ ## Examples by provider
80
+
81
+ Each block shows the CLI invocation and the equivalent programmatic call.
82
+
83
+ ### Slack
84
+
85
+ ```sh
86
+ send --provider slack \
87
+ --webhook-url https://hooks.slack.com/services/T000/B000/XXXX \
88
+ --title "Release 1.2.0" --body "Bug **fixes** and improvements" --markdown
89
+ ```
90
+
91
+ ```ts
92
+ await send({
93
+ provider: "slack",
94
+ webhookUrl: "https://hooks.slack.com/services/T000/B000/XXXX",
95
+ title: "Release 1.2.0",
96
+ body: "Bug **fixes** and improvements",
97
+ markdown: true,
98
+ });
99
+ ```
100
+
101
+ ### Microsoft Teams
102
+
103
+ ```sh
104
+ send --provider teams \
105
+ --webhook-url https://outlook.office.com/webhook/XXXX/IncomingWebhook/YYYY \
106
+ --title "Deploy complete" --body "Shipped to **production**" --markdown
107
+ ```
108
+
109
+ ```ts
110
+ await send({
111
+ provider: "teams",
112
+ webhookUrl: "https://outlook.office.com/webhook/XXXX/IncomingWebhook/YYYY",
113
+ title: "Deploy complete",
114
+ body: "Shipped to **production**",
115
+ markdown: true,
116
+ });
117
+ ```
118
+
119
+ ### Telegram
120
+
121
+ ```sh
122
+ send --provider telegram \
123
+ --bot-token 123456:ABC-DEF1234ghIkl \
124
+ --chat-id -1001234567890 \
125
+ --title "Alert" --body "Disk usage at *90%*" --markdown
126
+ ```
127
+
128
+ ```ts
129
+ await send({
130
+ provider: "telegram",
131
+ botToken: "123456:ABC-DEF1234ghIkl",
132
+ chatId: "-1001234567890",
133
+ title: "Alert",
134
+ body: "Disk usage at *90%*",
135
+ markdown: true,
136
+ });
137
+ ```
138
+
139
+ ### Element / Matrix
140
+
141
+ ```sh
142
+ send --provider element \
143
+ --homeserver-url https://matrix.org \
144
+ --access-token syt_XXXX \
145
+ --room-id '!abc123:matrix.org' \
146
+ --title "CI" --body "Build **passed**" --markdown
147
+ ```
148
+
149
+ ```ts
150
+ await send({
151
+ provider: "element",
152
+ homeserverUrl: "https://matrix.org",
153
+ accessToken: "syt_XXXX",
154
+ roomId: "!abc123:matrix.org",
155
+ title: "CI",
156
+ body: "Build **passed**",
157
+ markdown: true,
158
+ });
159
+ ```
160
+
161
+ ### Mattermost
162
+
163
+ ```sh
164
+ send --provider mattermost \
165
+ --webhook-url https://chat.example.com/hooks/xxxxxxxxxxxxxxxxx \
166
+ --title "Backup" --body "Nightly backup `OK`" --markdown
167
+ ```
168
+
169
+ ```ts
170
+ await send({
171
+ provider: "mattermost",
172
+ webhookUrl: "https://chat.example.com/hooks/xxxxxxxxxxxxxxxxx",
173
+ title: "Backup",
174
+ body: "Nightly backup `OK`",
175
+ markdown: true,
176
+ });
177
+ ```
178
+
179
+ ## License
180
+
181
+ MIT
package/dist/cli.cjs ADDED
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_command_line_args = __toESM(require("command-line-args"), 1);
28
+ var import_zod7 = require("zod");
29
+
30
+ // src/schema/sender.ts
31
+ var import_zod6 = require("zod");
32
+
33
+ // src/schema/element.ts
34
+ var import_zod = require("zod");
35
+ var elementConfigSchema = import_zod.z.object({
36
+ provider: import_zod.z.literal("element"),
37
+ homeserverUrl: import_zod.z.url(),
38
+ accessToken: import_zod.z.string(),
39
+ roomId: import_zod.z.string()
40
+ });
41
+
42
+ // src/schema/mattermost.ts
43
+ var import_zod2 = require("zod");
44
+ var mattermostConfigSchema = import_zod2.z.object({
45
+ provider: import_zod2.z.literal("mattermost"),
46
+ webhookUrl: import_zod2.z.url()
47
+ });
48
+
49
+ // src/schema/slack.ts
50
+ var import_zod3 = require("zod");
51
+ var slackConfigSchema = import_zod3.z.object({
52
+ provider: import_zod3.z.literal("slack"),
53
+ webhookUrl: import_zod3.z.url()
54
+ });
55
+
56
+ // src/schema/teams.ts
57
+ var import_zod4 = require("zod");
58
+ var teamsConfigSchema = import_zod4.z.object({
59
+ provider: import_zod4.z.literal("teams"),
60
+ webhookUrl: import_zod4.z.url()
61
+ });
62
+
63
+ // src/schema/telegram.ts
64
+ var import_zod5 = require("zod");
65
+ var telegramConfigSchema = import_zod5.z.object({
66
+ provider: import_zod5.z.literal("telegram"),
67
+ botToken: import_zod5.z.string(),
68
+ chatId: import_zod5.z.string()
69
+ });
70
+
71
+ // src/schema/sender.ts
72
+ var messageSchema = import_zod6.z.object({
73
+ title: import_zod6.z.string().optional(),
74
+ body: import_zod6.z.string(),
75
+ /** Render the body as markdown in the provider's native format. */
76
+ markdown: import_zod6.z.boolean().optional()
77
+ });
78
+ var senderConfigSchema = import_zod6.z.discriminatedUnion("provider", [
79
+ slackConfigSchema,
80
+ teamsConfigSchema,
81
+ mattermostConfigSchema,
82
+ telegramConfigSchema,
83
+ elementConfigSchema
84
+ ]);
85
+ var sendInputSchema = import_zod6.z.intersection(
86
+ senderConfigSchema,
87
+ messageSchema
88
+ );
89
+
90
+ // src/lib/flags.ts
91
+ var toKebab = (key) => key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
92
+ var flagFor = (key) => `--${toKebab(key)}`;
93
+ var configKeys = [
94
+ ...new Set(
95
+ senderConfigSchema.options.flatMap((option) => Object.keys(option.shape))
96
+ )
97
+ ];
98
+ var configOptionDefinitions = configKeys.map((key) => ({
99
+ name: toKebab(key),
100
+ type: String
101
+ }));
102
+ var optionDefinitions = [
103
+ ...configOptionDefinitions,
104
+ { name: "title", type: String },
105
+ { name: "body", type: String },
106
+ { name: "markdown", type: Boolean },
107
+ { name: "help", alias: "h", type: Boolean }
108
+ ];
109
+
110
+ // src/lib/config.ts
111
+ function formatConfigError(error) {
112
+ const lines = error.issues.map((issue) => {
113
+ const key = issue.path[0];
114
+ const where = typeof key === "string" ? flagFor(key) : "config";
115
+ return ` \u2192 ${where}: ${issue.message}`;
116
+ });
117
+ return `Invalid provider config:
118
+ ${lines.join("\n")}`;
119
+ }
120
+ function parseConfig(values) {
121
+ const result = senderConfigSchema.safeParse(values);
122
+ if (!result.success) {
123
+ throw new Error(formatConfigError(result.error));
124
+ }
125
+ return result.data;
126
+ }
127
+
128
+ // src/providers/element.ts
129
+ var import_marked = require("marked");
130
+ var ElementSender = class {
131
+ name = "Element";
132
+ homeserverUrl;
133
+ accessToken;
134
+ roomId;
135
+ constructor(config) {
136
+ this.homeserverUrl = config.homeserverUrl;
137
+ this.accessToken = config.accessToken;
138
+ this.roomId = config.roomId;
139
+ }
140
+ async send(message) {
141
+ const lines = [];
142
+ if (message.title) {
143
+ lines.push(message.markdown ? `**${message.title}**` : message.title, "");
144
+ }
145
+ lines.push(message.body);
146
+ const body = lines.join("\n");
147
+ const formatted = message.markdown ? {
148
+ format: "org.matrix.custom.html",
149
+ formatted_body: await import_marked.marked.parse(body, { async: true })
150
+ } : {};
151
+ const txnId = `send-${Date.now()}`;
152
+ const encodedRoomId = encodeURIComponent(this.roomId);
153
+ const url = `${this.homeserverUrl}/_matrix/client/v3/rooms/${encodedRoomId}/send/m.room.message/${txnId}`;
154
+ let response;
155
+ try {
156
+ response = await fetch(url, {
157
+ method: "PUT",
158
+ headers: {
159
+ "Content-Type": "application/json",
160
+ Authorization: `Bearer ${this.accessToken}`
161
+ },
162
+ body: JSON.stringify({
163
+ msgtype: "m.text",
164
+ body,
165
+ ...formatted
166
+ })
167
+ });
168
+ } catch (error) {
169
+ const cause = error instanceof Error ? error.cause ?? error.message : error;
170
+ throw new Error(`Element fetch failed: ${JSON.stringify(cause)}`);
171
+ }
172
+ if (!response.ok) {
173
+ const responseBody = await response.text();
174
+ throw new Error(
175
+ `Element API returned status ${response.status}: ${responseBody}`
176
+ );
177
+ }
178
+ }
179
+ };
180
+
181
+ // src/providers/mattermost.ts
182
+ var MattermostSender = class {
183
+ name = "Mattermost";
184
+ webhookUrl;
185
+ constructor(config) {
186
+ this.webhookUrl = config.webhookUrl;
187
+ }
188
+ /** Mattermost always renders markdown; escape special chars for plain bodies. */
189
+ escapeMarkdown(text) {
190
+ return text.replace(/([\\`*_{}[\]()#+\-.!|>~])/g, "\\$1");
191
+ }
192
+ async send(message) {
193
+ const summary = message.title ?? message.body;
194
+ const body = message.markdown ? message.body : this.escapeMarkdown(message.body);
195
+ const payload = {
196
+ text: message.title ? `#### ${message.title}` : "",
197
+ attachments: [
198
+ {
199
+ fallback: summary,
200
+ color: "#2eb886",
201
+ text: body
202
+ }
203
+ ]
204
+ };
205
+ const response = await fetch(this.webhookUrl, {
206
+ method: "POST",
207
+ headers: { "Content-Type": "application/json" },
208
+ body: JSON.stringify(payload)
209
+ });
210
+ if (!response.ok) {
211
+ const body2 = await response.text();
212
+ throw new Error(
213
+ `Mattermost webhook returned status ${response.status}: ${body2}`
214
+ );
215
+ }
216
+ }
217
+ };
218
+
219
+ // src/providers/slack.ts
220
+ var SlackSender = class {
221
+ name = "Slack";
222
+ webhookUrl;
223
+ constructor(config) {
224
+ this.webhookUrl = config.webhookUrl;
225
+ }
226
+ async send(message) {
227
+ const summary = message.title ?? message.body;
228
+ const blocks = [];
229
+ if (message.title) {
230
+ blocks.push({
231
+ type: "header",
232
+ text: { type: "plain_text", text: message.title }
233
+ });
234
+ }
235
+ blocks.push({ type: "divider" });
236
+ blocks.push({
237
+ type: "section",
238
+ text: {
239
+ type: message.markdown ? "mrkdwn" : "plain_text",
240
+ text: message.body
241
+ }
242
+ });
243
+ const payload = { text: summary, blocks };
244
+ const response = await fetch(this.webhookUrl, {
245
+ method: "POST",
246
+ headers: { "Content-Type": "application/json" },
247
+ body: JSON.stringify(payload)
248
+ });
249
+ if (!response.ok) {
250
+ const body = await response.text();
251
+ throw new Error(
252
+ `Slack webhook returned status ${response.status}: ${body}`
253
+ );
254
+ }
255
+ }
256
+ };
257
+
258
+ // src/providers/teams.ts
259
+ var TeamsSender = class {
260
+ name = "Microsoft Teams";
261
+ webhookUrl;
262
+ constructor(config) {
263
+ this.webhookUrl = config.webhookUrl;
264
+ }
265
+ async send(message) {
266
+ const summary = message.title ?? message.body;
267
+ const messageCard = {
268
+ "@type": "MessageCard",
269
+ "@context": "https://schema.org/extensions",
270
+ themeColor: "0078D7",
271
+ summary,
272
+ sections: [
273
+ {
274
+ activityTitle: message.title,
275
+ text: message.body,
276
+ markdown: message.markdown ?? false
277
+ }
278
+ ]
279
+ };
280
+ const response = await fetch(this.webhookUrl, {
281
+ method: "POST",
282
+ headers: { "Content-Type": "application/json" },
283
+ body: JSON.stringify(messageCard)
284
+ });
285
+ if (!response.ok) {
286
+ throw new Error(
287
+ `Teams webhook returned status ${response.status}: ${response.statusText}`
288
+ );
289
+ }
290
+ }
291
+ };
292
+
293
+ // src/providers/telegram.ts
294
+ var TelegramSender = class {
295
+ name = "Telegram";
296
+ botToken;
297
+ chatId;
298
+ constructor(config) {
299
+ this.botToken = config.botToken;
300
+ this.chatId = config.chatId;
301
+ }
302
+ escapeMarkdown(text) {
303
+ return text.replace(/([*_`[])/g, "\\$1");
304
+ }
305
+ async send(message) {
306
+ const lines = [];
307
+ if (message.markdown) {
308
+ if (message.title) {
309
+ lines.push(`*${this.escapeMarkdown(message.title)}*`, "");
310
+ }
311
+ lines.push(message.body);
312
+ } else {
313
+ if (message.title) lines.push(message.title, "");
314
+ lines.push(message.body);
315
+ }
316
+ const text = lines.join("\n");
317
+ const url = `https://api.telegram.org/bot${this.botToken}/sendMessage`;
318
+ const response = await fetch(url, {
319
+ method: "POST",
320
+ headers: { "Content-Type": "application/json" },
321
+ body: JSON.stringify({
322
+ chat_id: this.chatId,
323
+ text,
324
+ ...message.markdown ? { parse_mode: "Markdown" } : {}
325
+ })
326
+ });
327
+ if (!response.ok) {
328
+ const body = await response.text();
329
+ throw new Error(
330
+ `Telegram API returned status ${response.status}: ${body}`
331
+ );
332
+ }
333
+ }
334
+ };
335
+
336
+ // src/providers/index.ts
337
+ function createSender(config) {
338
+ switch (config.provider) {
339
+ case "slack":
340
+ return new SlackSender(config);
341
+ case "teams":
342
+ return new TeamsSender(config);
343
+ case "mattermost":
344
+ return new MattermostSender(config);
345
+ case "telegram":
346
+ return new TelegramSender(config);
347
+ case "element":
348
+ return new ElementSender(config);
349
+ }
350
+ }
351
+
352
+ // src/cli.ts
353
+ var USAGE = `send \u2014 post a structured message to a chat provider
354
+
355
+ Usage:
356
+ send --provider <slack|teams|telegram|element|mattermost> [config] --body <text> [options]
357
+
358
+ Provider config:
359
+ slack | teams | mattermost --webhook-url <url>
360
+ telegram --bot-token <token> --chat-id <id>
361
+ element --homeserver-url <url> --access-token <token> --room-id <id>
362
+
363
+ Message:
364
+ --title <text> Optional heading
365
+ --body <text> Message body
366
+ --markdown Render the body as markdown
367
+
368
+ Examples:
369
+ send --provider slack --webhook-url https://hooks.slack.com/... \\
370
+ --title "Release 1.2" --body "Bug **fixes**" --markdown
371
+
372
+ send --provider telegram --bot-token T --chat-id 42 --title Hi --body "the body"
373
+ `;
374
+ function fail(message) {
375
+ process.stderr.write(`${message}
376
+ `);
377
+ process.exit(1);
378
+ }
379
+ async function main() {
380
+ const values = (0, import_command_line_args.default)(optionDefinitions, {
381
+ camelCase: true
382
+ });
383
+ if (values.help || !values.provider) {
384
+ process.stdout.write(USAGE);
385
+ process.exit(values.help ? 0 : 1);
386
+ }
387
+ const config = parseConfig(values);
388
+ const messageResult = messageSchema.safeParse({
389
+ title: values.title,
390
+ body: values.body,
391
+ markdown: values.markdown
392
+ });
393
+ if (!messageResult.success) {
394
+ fail(
395
+ `Invalid message (a body is required):
396
+ ${import_zod7.z.prettifyError(messageResult.error)}`
397
+ );
398
+ }
399
+ const sender = createSender(config);
400
+ try {
401
+ await sender.send(messageResult.data);
402
+ process.stdout.write(`Sent to ${sender.name}.
403
+ `);
404
+ } catch (error) {
405
+ fail(`Failed to send to ${sender.name}: ${error.message}`);
406
+ }
407
+ }
408
+ main().catch((error) => fail(error.message));