polar-alerts 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -0
- package/dist/alert-sender.d.ts +6 -0
- package/dist/alert-sender.js +61 -0
- package/dist/description-builder.d.ts +32 -0
- package/dist/description-builder.js +95 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/senders/index.d.ts +2 -0
- package/dist/senders/index.js +18 -0
- package/dist/senders/telegram.d.ts +22 -0
- package/dist/senders/telegram.js +55 -0
- package/dist/senders/types.d.ts +13 -0
- package/dist/senders/types.js +2 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +242 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +38 -0
- package/dist/webhook-handler.d.ts +15 -0
- package/dist/webhook-handler.js +71 -0
- package/package.json +50 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Daniil Prylepa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TelegramAlertSender = void 0;
|
|
7
|
+
const node_telegram_bot_api_1 = __importDefault(require("node-telegram-bot-api"));
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
class TelegramAlertSender {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
sendAlert(params) {
|
|
14
|
+
const send = async () => {
|
|
15
|
+
const awaitedParams = params instanceof Promise ? await params : params;
|
|
16
|
+
const { title, description, hashtags, links, silent = false, } = awaitedParams;
|
|
17
|
+
const bot = new node_telegram_bot_api_1.default(this.config.telegram.botToken);
|
|
18
|
+
// Build message
|
|
19
|
+
let message = `*${(0, utils_1.escapeMarkdown)(title)}*\n\n`;
|
|
20
|
+
if (description) {
|
|
21
|
+
message += `${description}\n`;
|
|
22
|
+
}
|
|
23
|
+
if (links && links.length > 0) {
|
|
24
|
+
for (const link of links) {
|
|
25
|
+
message += `🔗 [${(0, utils_1.escapeMarkdown)(link.label)}](${link.url})\n`;
|
|
26
|
+
}
|
|
27
|
+
message += '\n';
|
|
28
|
+
}
|
|
29
|
+
if (hashtags && hashtags.length > 0) {
|
|
30
|
+
const formattedTags = hashtags
|
|
31
|
+
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
|
|
32
|
+
.join(' ');
|
|
33
|
+
message += formattedTags;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await bot.sendMessage(this.config.telegram.chatId, message.trim(), {
|
|
37
|
+
parse_mode: 'Markdown',
|
|
38
|
+
disable_web_page_preview: true,
|
|
39
|
+
disable_notification: silent,
|
|
40
|
+
message_thread_id: this.config.telegram.threadId,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('Failed to send Telegram alert:', error);
|
|
45
|
+
throw new Error(`Failed to send Telegram alert: ${error}`);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
if (this.config.deferExecution) {
|
|
50
|
+
this.config.deferExecution(send());
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
send().catch(console.error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('Error in sendAlert:', error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.TelegramAlertSender = TelegramAlertSender;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PolarAlertsConfig } from './types';
|
|
2
|
+
import type { Product } from '@polar-sh/sdk/models/components/product.js';
|
|
3
|
+
import type { OrderProduct } from '@polar-sh/sdk/models/components/orderproduct.js';
|
|
4
|
+
import type { Customer } from '@polar-sh/sdk/models/components/customer.js';
|
|
5
|
+
import type { CheckoutProduct } from '@polar-sh/sdk/models/components/checkoutproduct.js';
|
|
6
|
+
type FieldFormat = 'code' | 'italic' | 'plain';
|
|
7
|
+
export declare class AlertDescriptionBuilder {
|
|
8
|
+
private sections;
|
|
9
|
+
private config;
|
|
10
|
+
private escapeMarkdown;
|
|
11
|
+
constructor({ config, escapeMarkdown, }: {
|
|
12
|
+
config: PolarAlertsConfig;
|
|
13
|
+
escapeMarkdown: (text: string) => string;
|
|
14
|
+
});
|
|
15
|
+
custom(text: string | Promise<string>): this;
|
|
16
|
+
separator(): this;
|
|
17
|
+
field(label: string, value: string | undefined | null, format?: FieldFormat): this;
|
|
18
|
+
dateField(label: string, date: Date | null | undefined, dateFormat?: string): this;
|
|
19
|
+
moneyField(label: string, amountInCents: number, condition?: any): this;
|
|
20
|
+
productInfo(product: Product | CheckoutProduct | OrderProduct, amount?: number): this;
|
|
21
|
+
hashtags(hashtags: string[], condition?: boolean): this;
|
|
22
|
+
link(label: string, url: string, condition?: boolean): this;
|
|
23
|
+
customerInfo(customer: {
|
|
24
|
+
id: string;
|
|
25
|
+
name?: string | null;
|
|
26
|
+
email?: string | null;
|
|
27
|
+
billingAddress?: Customer['billingAddress'];
|
|
28
|
+
createdAt?: Date;
|
|
29
|
+
}): this;
|
|
30
|
+
build(): Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AlertDescriptionBuilder = void 0;
|
|
4
|
+
const date_fns_1 = require("date-fns");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
class AlertDescriptionBuilder {
|
|
7
|
+
constructor({ config, escapeMarkdown, }) {
|
|
8
|
+
this.sections = [];
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.escapeMarkdown = escapeMarkdown;
|
|
11
|
+
}
|
|
12
|
+
custom(text) {
|
|
13
|
+
this.sections.push(text);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
separator() {
|
|
17
|
+
this.sections.push('');
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
field(label, value, format = 'code') {
|
|
21
|
+
if (value) {
|
|
22
|
+
const escaped = this.escapeMarkdown(value);
|
|
23
|
+
const formatted = format === 'code'
|
|
24
|
+
? `\`${escaped}\``
|
|
25
|
+
: format === 'italic'
|
|
26
|
+
? `*${escaped}*`
|
|
27
|
+
: escaped;
|
|
28
|
+
this.sections.push(`*${label}* - ${formatted}`);
|
|
29
|
+
}
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
dateField(label, date, dateFormat = 'MMM d, yyyy h:mm a') {
|
|
33
|
+
if (date) {
|
|
34
|
+
this.field(label, (0, date_fns_1.format)(date, dateFormat));
|
|
35
|
+
}
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
moneyField(label, amountInCents, condition = true) {
|
|
39
|
+
if (condition) {
|
|
40
|
+
const text = `*${label}* - *$${amountInCents / 100}*`;
|
|
41
|
+
this.sections.push(text);
|
|
42
|
+
}
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
productInfo(product, amount) {
|
|
46
|
+
let text = `[${this.escapeMarkdown(product.name)}](${(0, utils_1.getProductLink)(this.config, product.id)})`;
|
|
47
|
+
if (amount !== undefined) {
|
|
48
|
+
text += ` (*$${amount / 100}*`;
|
|
49
|
+
if (product.recurringInterval) {
|
|
50
|
+
text += `/${this.escapeMarkdown(product.recurringInterval)}`;
|
|
51
|
+
}
|
|
52
|
+
text += ')';
|
|
53
|
+
}
|
|
54
|
+
this.sections.push(text);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
hashtags(hashtags, condition = true) {
|
|
58
|
+
if (condition && hashtags && hashtags.length > 0) {
|
|
59
|
+
const formattedTags = hashtags
|
|
60
|
+
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
|
|
61
|
+
.join(' ');
|
|
62
|
+
this.sections.push(formattedTags);
|
|
63
|
+
}
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
link(label, url, condition = true) {
|
|
67
|
+
if (condition && label && url) {
|
|
68
|
+
this.sections.push(`🔗 [${this.escapeMarkdown(label)}](${url})`);
|
|
69
|
+
}
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
customerInfo(customer) {
|
|
73
|
+
const country = customer.billingAddress?.country;
|
|
74
|
+
const flag = country ? (0, utils_1.getCountryFlag)(country) : '';
|
|
75
|
+
let section = `${flag ? `${flag} ` : ''}${this.escapeMarkdown(customer.name ?? '')}\n\`${customer.email}\``;
|
|
76
|
+
if (customer.createdAt) {
|
|
77
|
+
section += `\n*Created* - \`${(0, date_fns_1.format)(customer.createdAt, 'PPP')}\``;
|
|
78
|
+
}
|
|
79
|
+
section += '\n';
|
|
80
|
+
section += `\n🔗 [View Customer](${(0, utils_1.getCustomerLink)(this.config, customer.id)})\n`;
|
|
81
|
+
this.sections.push(section);
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
async build() {
|
|
85
|
+
const sectionPromises = this.sections.map(async (section) => {
|
|
86
|
+
if (typeof section === 'string') {
|
|
87
|
+
return Promise.resolve(section);
|
|
88
|
+
}
|
|
89
|
+
return section;
|
|
90
|
+
});
|
|
91
|
+
const sections = await Promise.all(sectionPromises);
|
|
92
|
+
return sections.join('\n');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.AlertDescriptionBuilder = AlertDescriptionBuilder;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PolarAlertsClient = void 0;
|
|
4
|
+
var webhook_handler_1 = require("./webhook-handler");
|
|
5
|
+
Object.defineProperty(exports, "PolarAlertsClient", { enumerable: true, get: function () { return webhook_handler_1.PolarAlertsClient; } });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./telegram"), exports);
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AlertsSenderConfig, AlertsSender, AlertParams } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Telegram alert configuration options
|
|
4
|
+
*/
|
|
5
|
+
export interface TelegramAlertsConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Your Telegram bot token (obtain via @BotFather on Telegram).
|
|
8
|
+
*/
|
|
9
|
+
botToken: string;
|
|
10
|
+
/** The chat ID where alerts will be sent. Can be set to a group, channel, or user id. */
|
|
11
|
+
chatId: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional: Set a thread ID when sending alerts to a specific thread on telegram (group topics).
|
|
14
|
+
*/
|
|
15
|
+
threadId?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class TelegramAlertSender implements AlertsSender {
|
|
18
|
+
private config;
|
|
19
|
+
constructor(config: AlertsSenderConfig & TelegramAlertsConfig);
|
|
20
|
+
sendAlert(params: AlertParams | Promise<AlertParams>): void;
|
|
21
|
+
escapeMarkdown(text: string): string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TelegramAlertSender = void 0;
|
|
7
|
+
const node_telegram_bot_api_1 = __importDefault(require("node-telegram-bot-api"));
|
|
8
|
+
class TelegramAlertSender {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
}
|
|
12
|
+
sendAlert(params) {
|
|
13
|
+
const send = async () => {
|
|
14
|
+
const awaitedParams = params instanceof Promise ? await params : params;
|
|
15
|
+
const { title, description, silent = false } = awaitedParams;
|
|
16
|
+
const bot = new node_telegram_bot_api_1.default(this.config.botToken);
|
|
17
|
+
// Build message
|
|
18
|
+
let message = `*${this.escapeMarkdown(title)}*\n\n`;
|
|
19
|
+
if (description) {
|
|
20
|
+
message += `${description}\n`;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await bot.sendMessage(this.config.chatId, message.trim(), {
|
|
24
|
+
parse_mode: 'Markdown',
|
|
25
|
+
disable_web_page_preview: true,
|
|
26
|
+
disable_notification: silent,
|
|
27
|
+
message_thread_id: this.config.threadId,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error('Failed to send Telegram alert:', error);
|
|
32
|
+
throw new Error(`Failed to send Telegram alert: ${error}`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
if (this.config.waitUntil) {
|
|
37
|
+
this.config.waitUntil(send());
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
send().catch(console.error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('Error in sendAlert:', error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// This escapeMarkdown method is for Telegram v1 Markdown formatting.
|
|
48
|
+
escapeMarkdown(text) {
|
|
49
|
+
return text
|
|
50
|
+
.replace(/\*/g, '\\*')
|
|
51
|
+
.replace(/`/g, '\\`')
|
|
52
|
+
.replace(/\[/g, '\\[');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.TelegramAlertSender = TelegramAlertSender;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PolarAlertsConfig } from '../types';
|
|
2
|
+
export interface AlertsSenderConfig {
|
|
3
|
+
waitUntil?: PolarAlertsConfig['waitUntil'];
|
|
4
|
+
}
|
|
5
|
+
export interface AlertsSender {
|
|
6
|
+
sendAlert(params: AlertParams | Promise<AlertParams>): void;
|
|
7
|
+
escapeMarkdown(text: string): string;
|
|
8
|
+
}
|
|
9
|
+
export interface AlertParams {
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
silent?: boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PolarAlertsConfig } from './types';
|
|
2
|
+
import { AlertParams } from './senders/types';
|
|
3
|
+
export declare function createAlertTemplates({ config, escapeMarkdown, }: {
|
|
4
|
+
config: PolarAlertsConfig;
|
|
5
|
+
escapeMarkdown: (text: string) => string;
|
|
6
|
+
}): Record<string, (params: {
|
|
7
|
+
data: any;
|
|
8
|
+
}) => Promise<AlertParams | undefined>>;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAlertTemplates = createAlertTemplates;
|
|
4
|
+
const description_builder_1 = require("./description-builder");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
function createAlertTemplates({ config, escapeMarkdown, }) {
|
|
7
|
+
return {
|
|
8
|
+
['checkout.created']: async ({ data: checkout, }) => {
|
|
9
|
+
const description = new description_builder_1.AlertDescriptionBuilder({
|
|
10
|
+
config,
|
|
11
|
+
escapeMarkdown,
|
|
12
|
+
});
|
|
13
|
+
checkout.products.forEach((product) => {
|
|
14
|
+
description.productInfo(product, product.prices.find((price) => price.amountType === 'fixed')
|
|
15
|
+
?.priceAmount ?? 0);
|
|
16
|
+
});
|
|
17
|
+
description
|
|
18
|
+
.separator()
|
|
19
|
+
.field('Status', checkout.status.toUpperCase())
|
|
20
|
+
.dateField('Created at', checkout.createdAt)
|
|
21
|
+
.dateField('Expires at', checkout.expiresAt)
|
|
22
|
+
.separator()
|
|
23
|
+
.field('🏷️ Discount', checkout.discount
|
|
24
|
+
? `${checkout.discount.name} ($${checkout.discount.code})`
|
|
25
|
+
: undefined, 'italic')
|
|
26
|
+
.separator()
|
|
27
|
+
.link('View Checkout', (0, utils_1.getSubscriptionLink)(config, checkout.id))
|
|
28
|
+
.separator();
|
|
29
|
+
if (checkout.customerId) {
|
|
30
|
+
description.customerInfo({
|
|
31
|
+
id: checkout.customerId,
|
|
32
|
+
name: checkout.customerName,
|
|
33
|
+
email: checkout.customerEmail,
|
|
34
|
+
billingAddress: checkout.customerBillingAddress,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
title: '🛒🆕 Checkout Created',
|
|
39
|
+
description: await description
|
|
40
|
+
.hashtags(['checkout', 'created'])
|
|
41
|
+
.build(),
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
['checkout.updated']: async ({ data: checkout, }) => {
|
|
45
|
+
const description = new description_builder_1.AlertDescriptionBuilder({
|
|
46
|
+
config,
|
|
47
|
+
escapeMarkdown,
|
|
48
|
+
});
|
|
49
|
+
checkout.products.forEach((product) => {
|
|
50
|
+
description.productInfo(product, product.prices.find((price) => price.amountType === 'fixed')
|
|
51
|
+
?.priceAmount ?? 0);
|
|
52
|
+
});
|
|
53
|
+
description
|
|
54
|
+
.separator()
|
|
55
|
+
.field('Status', checkout.status.toUpperCase())
|
|
56
|
+
.dateField('Created at', checkout.createdAt)
|
|
57
|
+
.dateField('Expires at', checkout.expiresAt)
|
|
58
|
+
.separator()
|
|
59
|
+
.field('🏷️ Discount', checkout.discount
|
|
60
|
+
? `${checkout.discount.name} ($${checkout.discount.code})`
|
|
61
|
+
: undefined, 'italic')
|
|
62
|
+
.separator()
|
|
63
|
+
.link('View Checkout', (0, utils_1.getSubscriptionLink)(config, checkout.id))
|
|
64
|
+
.separator();
|
|
65
|
+
if (checkout.customerId) {
|
|
66
|
+
description.customerInfo({
|
|
67
|
+
id: checkout.customerId,
|
|
68
|
+
name: checkout.customerName,
|
|
69
|
+
email: checkout.customerEmail,
|
|
70
|
+
billingAddress: checkout.customerBillingAddress,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
title: '🛒🔁 Checkout Updated',
|
|
75
|
+
description: await description
|
|
76
|
+
.hashtags(['checkout', 'updated'])
|
|
77
|
+
.build(),
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
['subscription.created']: async ({ data: subscription, }) => ({
|
|
81
|
+
title: '🔁✅ Subscription Created',
|
|
82
|
+
description: await new description_builder_1.AlertDescriptionBuilder({
|
|
83
|
+
config,
|
|
84
|
+
escapeMarkdown,
|
|
85
|
+
})
|
|
86
|
+
.productInfo(subscription.product, subscription.amount)
|
|
87
|
+
.separator()
|
|
88
|
+
.dateField('Started on', subscription.startedAt)
|
|
89
|
+
.separator()
|
|
90
|
+
.link('View Subscription', (0, utils_1.getSubscriptionLink)(config, subscription.id))
|
|
91
|
+
.separator()
|
|
92
|
+
.customerInfo(subscription.customer)
|
|
93
|
+
.hashtags(['subscription', 'created'])
|
|
94
|
+
.build(),
|
|
95
|
+
}),
|
|
96
|
+
['subscription.canceled']: async ({ data: subscription, }) => ({
|
|
97
|
+
title: '🔁❌ Subscription Canceled',
|
|
98
|
+
description: await new description_builder_1.AlertDescriptionBuilder({
|
|
99
|
+
config,
|
|
100
|
+
escapeMarkdown,
|
|
101
|
+
})
|
|
102
|
+
.productInfo(subscription.product, subscription.amount)
|
|
103
|
+
.separator()
|
|
104
|
+
.field('❔ Cancellation reason', subscription.customerCancellationReason?.toUpperCase())
|
|
105
|
+
.field('💬 Comment', subscription.customerCancellationComment)
|
|
106
|
+
.dateField('Started on', subscription.startedAt)
|
|
107
|
+
.dateField('Ends on', subscription.endsAt)
|
|
108
|
+
.separator()
|
|
109
|
+
.link('View Subscription', (0, utils_1.getSubscriptionLink)(config, subscription.id))
|
|
110
|
+
.separator()
|
|
111
|
+
.customerInfo(subscription.customer)
|
|
112
|
+
.hashtags(['subscription', 'canceled'])
|
|
113
|
+
.build(),
|
|
114
|
+
}),
|
|
115
|
+
['subscription.uncanceled']: async ({ data: subscription, }) => ({
|
|
116
|
+
title: '🔁✅ Subscription Uncanceled',
|
|
117
|
+
description: await new description_builder_1.AlertDescriptionBuilder({
|
|
118
|
+
config,
|
|
119
|
+
escapeMarkdown,
|
|
120
|
+
})
|
|
121
|
+
.productInfo(subscription.product, subscription.amount)
|
|
122
|
+
.separator()
|
|
123
|
+
.field('Status', subscription.status.toUpperCase())
|
|
124
|
+
.separator()
|
|
125
|
+
.dateField('Started on', subscription.startedAt)
|
|
126
|
+
.dateField('Current period start', subscription.currentPeriodStart)
|
|
127
|
+
.dateField('Current period end', subscription.currentPeriodEnd)
|
|
128
|
+
.separator()
|
|
129
|
+
.link('View Subscription', (0, utils_1.getSubscriptionLink)(config, subscription.id))
|
|
130
|
+
.separator()
|
|
131
|
+
.customerInfo(subscription.customer)
|
|
132
|
+
.hashtags(['subscription', 'uncanceled'])
|
|
133
|
+
.build(),
|
|
134
|
+
}),
|
|
135
|
+
['subscription.updated']: async ({ data: subscription, }) => {
|
|
136
|
+
if (subscription.status !== 'past_due') {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
title: '🔁⚠️ Subscription Payment Past Due',
|
|
141
|
+
description: await new description_builder_1.AlertDescriptionBuilder({
|
|
142
|
+
config,
|
|
143
|
+
escapeMarkdown,
|
|
144
|
+
})
|
|
145
|
+
.productInfo(subscription.product, subscription.amount)
|
|
146
|
+
.separator()
|
|
147
|
+
.field('Status', subscription.status.toUpperCase())
|
|
148
|
+
.separator()
|
|
149
|
+
.dateField('Started on', subscription.startedAt)
|
|
150
|
+
.dateField('Current period start', subscription.currentPeriodStart)
|
|
151
|
+
.dateField('Current period end', subscription.currentPeriodEnd)
|
|
152
|
+
.separator()
|
|
153
|
+
.link('View Subscription', (0, utils_1.getSubscriptionLink)(config, subscription.id))
|
|
154
|
+
.separator()
|
|
155
|
+
.customerInfo(subscription.customer)
|
|
156
|
+
.hashtags(['subscription', 'past_due'])
|
|
157
|
+
.build(),
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
['order.created']: async ({ data: order }) => {
|
|
161
|
+
if (!order.product) {
|
|
162
|
+
throw new Error('Product not found in order.');
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
title: '💰🆕 Order Created',
|
|
166
|
+
description: await new description_builder_1.AlertDescriptionBuilder({
|
|
167
|
+
config,
|
|
168
|
+
escapeMarkdown,
|
|
169
|
+
})
|
|
170
|
+
.productInfo(order.product, order.subtotalAmount)
|
|
171
|
+
.separator()
|
|
172
|
+
.field('🏷️ Discount', order.discount
|
|
173
|
+
? `$${order.discountAmount / 100} (${order.discount?.name || ''})`
|
|
174
|
+
: undefined, 'italic')
|
|
175
|
+
.moneyField('💵 Total', order.netAmount)
|
|
176
|
+
.moneyField('🏛️ Taxes', order.taxAmount, order.taxAmount > 0)
|
|
177
|
+
.separator()
|
|
178
|
+
.field('Status', order.status.toUpperCase())
|
|
179
|
+
.dateField('Created on', order.createdAt)
|
|
180
|
+
.separator()
|
|
181
|
+
.link('View Order', (0, utils_1.getOrderLink)(config, order.id))
|
|
182
|
+
.separator()
|
|
183
|
+
.customerInfo(order.customer)
|
|
184
|
+
.hashtags(['order', 'created'])
|
|
185
|
+
.build(),
|
|
186
|
+
silent: false,
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
['order.paid']: async ({ data: order }) => {
|
|
190
|
+
if (!order.product) {
|
|
191
|
+
throw new Error('Product not found in order.');
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
title: '💰 Order Paid',
|
|
195
|
+
description: await new description_builder_1.AlertDescriptionBuilder({
|
|
196
|
+
config,
|
|
197
|
+
escapeMarkdown,
|
|
198
|
+
})
|
|
199
|
+
.productInfo(order.product, order.subtotalAmount)
|
|
200
|
+
.separator()
|
|
201
|
+
.field('🏷️ Discount', order.discount
|
|
202
|
+
? `$${order.discountAmount / 100} (${order.discount?.name || ''})`
|
|
203
|
+
: undefined, 'italic')
|
|
204
|
+
.moneyField('💵 Total', order.netAmount)
|
|
205
|
+
.moneyField('🏛️ Taxes', order.taxAmount, order.taxAmount > 0)
|
|
206
|
+
.separator()
|
|
207
|
+
.field('Billing reason', order.billingReason.toUpperCase())
|
|
208
|
+
.dateField('Paid on', order.createdAt)
|
|
209
|
+
.separator()
|
|
210
|
+
.link('View Order', (0, utils_1.getOrderLink)(config, order.id))
|
|
211
|
+
.separator()
|
|
212
|
+
.customerInfo(order.customer)
|
|
213
|
+
.hashtags(['order', 'paid'])
|
|
214
|
+
.build(),
|
|
215
|
+
silent: order.billingReason === 'subscription_cycle',
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
['order.refunded']: async ({ data: order }) => {
|
|
219
|
+
if (!order.product) {
|
|
220
|
+
throw new Error('Product not found in order.');
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
title: '💰❌ Order Refunded',
|
|
224
|
+
description: await new description_builder_1.AlertDescriptionBuilder({
|
|
225
|
+
config,
|
|
226
|
+
escapeMarkdown,
|
|
227
|
+
})
|
|
228
|
+
.productInfo(order.product, order.subtotalAmount)
|
|
229
|
+
.separator()
|
|
230
|
+
.moneyField('💵 Total', order.netAmount)
|
|
231
|
+
.moneyField('🏛️ Taxes', order.taxAmount, order.taxAmount > 0)
|
|
232
|
+
.dateField('Refunded on', order.createdAt)
|
|
233
|
+
.separator()
|
|
234
|
+
.link('View Order', (0, utils_1.getOrderLink)(config, order.id))
|
|
235
|
+
.separator()
|
|
236
|
+
.customerInfo(order.customer)
|
|
237
|
+
.hashtags(['order', 'refunded'])
|
|
238
|
+
.build(),
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TelegramAlertsConfig } from './senders';
|
|
2
|
+
import { createAlertTemplates } from './templates';
|
|
3
|
+
export type EventType = keyof ReturnType<typeof createAlertTemplates>;
|
|
4
|
+
export interface PolarAlertsConfig {
|
|
5
|
+
/** Your Polar server environment. Used to construct dashboard links. */
|
|
6
|
+
polarServer: 'production' | 'sandbox';
|
|
7
|
+
/**
|
|
8
|
+
* Your Polar organization slug
|
|
9
|
+
*/
|
|
10
|
+
polarOrganizationSlug: string;
|
|
11
|
+
/**
|
|
12
|
+
* Enable/disable specific event types
|
|
13
|
+
*/
|
|
14
|
+
events?: Record<EventType, boolean> | 'all';
|
|
15
|
+
/**
|
|
16
|
+
* Telegram alert configuration options
|
|
17
|
+
*/
|
|
18
|
+
telegram?: TelegramAlertsConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Optional function to defer execution (e.g., Vercel's waitUntil)
|
|
21
|
+
* If not provided, alerts may be unreliable in serverless environments.
|
|
22
|
+
*
|
|
23
|
+
* See `waitUntil` documentation in
|
|
24
|
+
* [Vercel](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil) and
|
|
25
|
+
* [Cloudflare](https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil)
|
|
26
|
+
* for more details.
|
|
27
|
+
*/
|
|
28
|
+
waitUntil?: (promise: Promise<any>) => void;
|
|
29
|
+
}
|
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PolarAlertsConfig } from './types';
|
|
2
|
+
export declare function getCountryFlag(countryCode: string): string;
|
|
3
|
+
export declare function getCustomerLink(config: PolarAlertsConfig, customerId: string): string;
|
|
4
|
+
export declare function getOrderLink(config: PolarAlertsConfig, orderId: string): string;
|
|
5
|
+
export declare function getProductLink(config: PolarAlertsConfig, productId: string): string;
|
|
6
|
+
export declare function getSubscriptionLink(config: PolarAlertsConfig, subscriptionId: string): string;
|
|
7
|
+
export declare function getCheckoutLink(config: PolarAlertsConfig, checkoutId: string): string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCountryFlag = getCountryFlag;
|
|
4
|
+
exports.getCustomerLink = getCustomerLink;
|
|
5
|
+
exports.getOrderLink = getOrderLink;
|
|
6
|
+
exports.getProductLink = getProductLink;
|
|
7
|
+
exports.getSubscriptionLink = getSubscriptionLink;
|
|
8
|
+
exports.getCheckoutLink = getCheckoutLink;
|
|
9
|
+
function getCountryFlag(countryCode) {
|
|
10
|
+
const code = countryCode.toUpperCase();
|
|
11
|
+
const codePoints = code
|
|
12
|
+
.split('')
|
|
13
|
+
.map((char) => 127397 + char.charCodeAt(0));
|
|
14
|
+
return String.fromCodePoint(...codePoints);
|
|
15
|
+
}
|
|
16
|
+
function getCustomerLink(config, customerId) {
|
|
17
|
+
return polarDashboardLink(config, `customers/${customerId}`);
|
|
18
|
+
}
|
|
19
|
+
function getOrderLink(config, orderId) {
|
|
20
|
+
return polarDashboardLink(config, `orders/${orderId}`);
|
|
21
|
+
}
|
|
22
|
+
function getProductLink(config, productId) {
|
|
23
|
+
return polarDashboardLink(config, `products/${productId}`);
|
|
24
|
+
}
|
|
25
|
+
function getSubscriptionLink(config, subscriptionId) {
|
|
26
|
+
return polarDashboardLink(config, `subscriptions/${subscriptionId}`);
|
|
27
|
+
}
|
|
28
|
+
function getCheckoutLink(config, checkoutId) {
|
|
29
|
+
return polarDashboardLink(config, `sales/checkouts/${checkoutId}`);
|
|
30
|
+
}
|
|
31
|
+
function polarDashboardLink(config, path) {
|
|
32
|
+
if (config.polarServer === 'production') {
|
|
33
|
+
return `https://polar.sh/dashboard/${config.polarOrganizationSlug}/${path}`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return `https://sandbox.polar.sh/dashboard/${config.polarOrganizationSlug}/${path}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AlertParams } from './senders';
|
|
2
|
+
import { PolarAlertsConfig } from './types';
|
|
3
|
+
import type { validateEvent } from '@polar-sh/sdk/webhooks';
|
|
4
|
+
type WebhookPayload = ReturnType<typeof validateEvent>;
|
|
5
|
+
export declare class PolarAlertsClient {
|
|
6
|
+
private senders;
|
|
7
|
+
private config;
|
|
8
|
+
constructor(config: PolarAlertsConfig);
|
|
9
|
+
sendAlert(params: AlertParams | Promise<AlertParams>): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Handle a Polar webhook event and send the appropriate alert
|
|
12
|
+
*/
|
|
13
|
+
handleWebhook(payload: WebhookPayload): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PolarAlertsClient = void 0;
|
|
4
|
+
const senders_1 = require("./senders");
|
|
5
|
+
const templates_1 = require("./templates");
|
|
6
|
+
const DEFAULT_EVENT_CONFIG = {
|
|
7
|
+
'checkout.created': false,
|
|
8
|
+
'checkout.updated': false,
|
|
9
|
+
'subscription.created': true,
|
|
10
|
+
'subscription.canceled': true,
|
|
11
|
+
'subscription.uncanceled': true,
|
|
12
|
+
'subscription.updated': true,
|
|
13
|
+
'order.created': false,
|
|
14
|
+
'order.paid': true,
|
|
15
|
+
'order.refunded': true,
|
|
16
|
+
};
|
|
17
|
+
class PolarAlertsClient {
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.senders = [];
|
|
20
|
+
this.config = config;
|
|
21
|
+
if (config.telegram) {
|
|
22
|
+
this.senders.push(new senders_1.TelegramAlertSender({
|
|
23
|
+
...config.telegram,
|
|
24
|
+
waitUntil: config.waitUntil,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async sendAlert(params) {
|
|
29
|
+
for (const sender of this.senders) {
|
|
30
|
+
sender.sendAlert(params);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Handle a Polar webhook event and send the appropriate alert
|
|
35
|
+
*/
|
|
36
|
+
async handleWebhook(payload) {
|
|
37
|
+
const eventType = payload.type;
|
|
38
|
+
const eventConfig = this.config.events === 'all'
|
|
39
|
+
? {}
|
|
40
|
+
: {
|
|
41
|
+
...DEFAULT_EVENT_CONFIG,
|
|
42
|
+
...this.config.events,
|
|
43
|
+
};
|
|
44
|
+
// Default to true if not explicitly set to false
|
|
45
|
+
const isEventEnabled = (eventName) => eventConfig[eventName] ?? true;
|
|
46
|
+
if (!isEventEnabled(eventType)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
for (const sender of this.senders) {
|
|
51
|
+
const templates = (0, templates_1.createAlertTemplates)({
|
|
52
|
+
config: this.config,
|
|
53
|
+
escapeMarkdown: sender.escapeMarkdown,
|
|
54
|
+
});
|
|
55
|
+
if (templates[eventType]) {
|
|
56
|
+
const alert = await templates[eventType]({
|
|
57
|
+
data: payload.data,
|
|
58
|
+
});
|
|
59
|
+
if (alert) {
|
|
60
|
+
sender.sendAlert(alert);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`Error handling webhook event ${eventType}:`, error);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.PolarAlertsClient = PolarAlertsClient;
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "polar-alerts",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Automatic Telegram alerts for Polar events with built-in webhook handling",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"dev": "tsc --watch"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"polar",
|
|
19
|
+
"telegram",
|
|
20
|
+
"alerts",
|
|
21
|
+
"notifications",
|
|
22
|
+
"webhooks",
|
|
23
|
+
"payments",
|
|
24
|
+
"subscriptions",
|
|
25
|
+
"bot",
|
|
26
|
+
"typescript"
|
|
27
|
+
],
|
|
28
|
+
"author": "Daniil Prylepa",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"node-telegram-bot-api": "^0.66.0",
|
|
32
|
+
"date-fns": "^4.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@polar-sh/sdk": ">=0.40.0 <1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@polar-sh/sdk": "^0.40.3",
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"@types/node-telegram-bot-api": "^0.64.0",
|
|
41
|
+
"typescript": "^5.0.0"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/world1dan/polar-alerts.git"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
}
|
|
50
|
+
}
|