@soulbatical/tetra-core 0.10.4 → 0.11.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/README.md +78 -38
- package/dist/core/createApp.d.ts +1 -1
- package/dist/core/createApp.d.ts.map +1 -1
- package/dist/core/createApp.js +77 -2
- package/dist/core/createApp.js.map +1 -1
- package/dist/core/dualWriteProxy.d.ts +7 -2
- package/dist/core/dualWriteProxy.d.ts.map +1 -1
- package/dist/core/dualWriteProxy.js +16 -5
- package/dist/core/dualWriteProxy.js.map +1 -1
- package/dist/core/routeContext.d.ts +24 -0
- package/dist/core/routeContext.d.ts.map +1 -1
- package/dist/core/routeContext.js +31 -4
- package/dist/core/routeContext.js.map +1 -1
- package/dist/core/systemDb.d.ts +2 -2
- package/dist/core/systemDb.js +2 -2
- package/dist/generators/rls-checker.d.ts +1 -1
- package/dist/generators/rls-checker.js +1 -1
- package/dist/generators/rls-exec-sql.d.ts +1 -1
- package/dist/generators/rls-exec-sql.js +1 -1
- package/dist/generators/rpc/index.d.ts +1 -1
- package/dist/generators/rpc/index.js +1 -1
- package/dist/index.d.ts +3 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -32
- package/dist/index.js.map +1 -1
- package/dist/middleware/securityMiddleware.d.ts +1 -1
- package/dist/middleware/securityMiddleware.d.ts.map +1 -1
- package/dist/middleware/validateBody.d.ts.map +1 -1
- package/dist/middleware/validateBody.js +51 -8
- package/dist/middleware/validateBody.js.map +1 -1
- package/dist/shared/rfc7807ErrorResponse.d.ts +7 -0
- package/dist/shared/rfc7807ErrorResponse.d.ts.map +1 -1
- package/dist/shared/rfc7807ErrorResponse.js +19 -5
- package/dist/shared/rfc7807ErrorResponse.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +16 -1
- package/dist/utils/logger.js.map +1 -1
- package/package.json +33 -77
- package/dist/affiliate.d.ts +0 -11
- package/dist/affiliate.d.ts.map +0 -1
- package/dist/affiliate.js +0 -10
- package/dist/affiliate.js.map +0 -1
- package/dist/billing.d.ts +0 -8
- package/dist/billing.d.ts.map +0 -1
- package/dist/billing.js +0 -7
- package/dist/billing.js.map +0 -1
- package/dist/email.d.ts +0 -9
- package/dist/email.d.ts.map +0 -1
- package/dist/email.js +0 -8
- package/dist/email.js.map +0 -1
- package/dist/generators/rls-exec-sql.sql +0 -57
- package/dist/generators.d.ts +0 -15
- package/dist/generators.d.ts.map +0 -1
- package/dist/generators.js +0 -12
- package/dist/generators.js.map +0 -1
- package/dist/mcp.d.ts +0 -8
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js +0 -7
- package/dist/mcp.js.map +0 -1
- package/dist/planner.d.ts +0 -8
- package/dist/planner.d.ts.map +0 -1
- package/dist/planner.js +0 -7
- package/dist/planner.js.map +0 -1
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts +0 -47
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +0 -1
- package/dist/shared/affiliate/AffiliateAttributionService.js +0 -308
- package/dist/shared/affiliate/AffiliateAttributionService.js.map +0 -1
- package/dist/shared/affiliate/AffiliateClickService.d.ts +0 -35
- package/dist/shared/affiliate/AffiliateClickService.d.ts.map +0 -1
- package/dist/shared/affiliate/AffiliateClickService.js +0 -87
- package/dist/shared/affiliate/AffiliateClickService.js.map +0 -1
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +0 -11
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +0 -1
- package/dist/shared/affiliate/affiliateFeatureConfig.js +0 -242
- package/dist/shared/affiliate/affiliateFeatureConfig.js.map +0 -1
- package/dist/shared/affiliate/index.d.ts +0 -11
- package/dist/shared/affiliate/index.d.ts.map +0 -1
- package/dist/shared/affiliate/index.js +0 -13
- package/dist/shared/affiliate/index.js.map +0 -1
- package/dist/shared/affiliate/routes.d.ts +0 -87
- package/dist/shared/affiliate/routes.d.ts.map +0 -1
- package/dist/shared/affiliate/routes.js +0 -404
- package/dist/shared/affiliate/routes.js.map +0 -1
- package/dist/shared/affiliate/types.d.ts +0 -170
- package/dist/shared/affiliate/types.d.ts.map +0 -1
- package/dist/shared/affiliate/types.js +0 -11
- package/dist/shared/affiliate/types.js.map +0 -1
- package/dist/shared/billing/BillingService.d.ts +0 -56
- package/dist/shared/billing/BillingService.d.ts.map +0 -1
- package/dist/shared/billing/BillingService.js +0 -588
- package/dist/shared/billing/BillingService.js.map +0 -1
- package/dist/shared/billing/SeatBillingService.d.ts +0 -106
- package/dist/shared/billing/SeatBillingService.d.ts.map +0 -1
- package/dist/shared/billing/SeatBillingService.js +0 -292
- package/dist/shared/billing/SeatBillingService.js.map +0 -1
- package/dist/shared/billing/index.d.ts +0 -30
- package/dist/shared/billing/index.d.ts.map +0 -1
- package/dist/shared/billing/index.js +0 -27
- package/dist/shared/billing/index.js.map +0 -1
- package/dist/shared/billing/routes.d.ts +0 -45
- package/dist/shared/billing/routes.d.ts.map +0 -1
- package/dist/shared/billing/routes.js +0 -184
- package/dist/shared/billing/routes.js.map +0 -1
- package/dist/shared/billing/seat-pricing.d.ts +0 -53
- package/dist/shared/billing/seat-pricing.d.ts.map +0 -1
- package/dist/shared/billing/seat-pricing.js +0 -81
- package/dist/shared/billing/seat-pricing.js.map +0 -1
- package/dist/shared/billing/types.d.ts +0 -109
- package/dist/shared/billing/types.d.ts.map +0 -1
- package/dist/shared/billing/types.js +0 -8
- package/dist/shared/billing/types.js.map +0 -1
- package/dist/shared/email/EmailService.d.ts +0 -64
- package/dist/shared/email/EmailService.d.ts.map +0 -1
- package/dist/shared/email/EmailService.js +0 -300
- package/dist/shared/email/EmailService.js.map +0 -1
- package/dist/shared/email/adminRoutes.d.ts +0 -30
- package/dist/shared/email/adminRoutes.d.ts.map +0 -1
- package/dist/shared/email/adminRoutes.js +0 -227
- package/dist/shared/email/adminRoutes.js.map +0 -1
- package/dist/shared/email/gmail.d.ts +0 -208
- package/dist/shared/email/gmail.d.ts.map +0 -1
- package/dist/shared/email/gmail.js +0 -626
- package/dist/shared/email/gmail.js.map +0 -1
- package/dist/shared/email/index.d.ts +0 -15
- package/dist/shared/email/index.d.ts.map +0 -1
- package/dist/shared/email/index.js +0 -18
- package/dist/shared/email/index.js.map +0 -1
- package/dist/shared/email/mailgun.d.ts +0 -18
- package/dist/shared/email/mailgun.d.ts.map +0 -1
- package/dist/shared/email/mailgun.js +0 -76
- package/dist/shared/email/mailgun.js.map +0 -1
- package/dist/shared/email/sanitize.d.ts +0 -25
- package/dist/shared/email/sanitize.d.ts.map +0 -1
- package/dist/shared/email/sanitize.js +0 -39
- package/dist/shared/email/sanitize.js.map +0 -1
- package/dist/shared/email/smtp.d.ts +0 -20
- package/dist/shared/email/smtp.d.ts.map +0 -1
- package/dist/shared/email/smtp.js +0 -53
- package/dist/shared/email/smtp.js.map +0 -1
- package/dist/shared/email/types.d.ts +0 -113
- package/dist/shared/email/types.d.ts.map +0 -1
- package/dist/shared/email/types.js +0 -7
- package/dist/shared/email/types.js.map +0 -1
- package/dist/shared/email/webhookRoutes.d.ts +0 -29
- package/dist/shared/email/webhookRoutes.d.ts.map +0 -1
- package/dist/shared/email/webhookRoutes.js +0 -125
- package/dist/shared/email/webhookRoutes.js.map +0 -1
- package/dist/shared/mcp/index.d.ts +0 -51
- package/dist/shared/mcp/index.d.ts.map +0 -1
- package/dist/shared/mcp/index.js +0 -51
- package/dist/shared/mcp/index.js.map +0 -1
- package/dist/shared/mcp/mcp-auth-routes.d.ts +0 -26
- package/dist/shared/mcp/mcp-auth-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-auth-routes.js +0 -141
- package/dist/shared/mcp/mcp-auth-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-db.d.ts +0 -99
- package/dist/shared/mcp/mcp-db.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-db.js +0 -106
- package/dist/shared/mcp/mcp-db.js.map +0 -1
- package/dist/shared/mcp/mcp-routes.d.ts +0 -29
- package/dist/shared/mcp/mcp-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-routes.js +0 -171
- package/dist/shared/mcp/mcp-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-tokens-routes.d.ts +0 -35
- package/dist/shared/mcp/mcp-tokens-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-tokens-routes.js +0 -94
- package/dist/shared/mcp/mcp-tokens-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-usage-routes.d.ts +0 -17
- package/dist/shared/mcp/mcp-usage-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-usage-routes.js +0 -81
- package/dist/shared/mcp/mcp-usage-routes.js.map +0 -1
- package/dist/shared/mcp/tenant-context.d.ts +0 -59
- package/dist/shared/mcp/tenant-context.d.ts.map +0 -1
- package/dist/shared/mcp/tenant-context.js +0 -136
- package/dist/shared/mcp/tenant-context.js.map +0 -1
- package/dist/shared/mcp/types.d.ts +0 -74
- package/dist/shared/mcp/types.d.ts.map +0 -1
- package/dist/shared/mcp/types.js +0 -7
- package/dist/shared/mcp/types.js.map +0 -1
- package/dist/shared/planner/GoogleCalendarService.d.ts +0 -137
- package/dist/shared/planner/GoogleCalendarService.d.ts.map +0 -1
- package/dist/shared/planner/GoogleCalendarService.js +0 -525
- package/dist/shared/planner/GoogleCalendarService.js.map +0 -1
- package/dist/shared/planner/PlannerService.d.ts +0 -264
- package/dist/shared/planner/PlannerService.d.ts.map +0 -1
- package/dist/shared/planner/PlannerService.js +0 -1393
- package/dist/shared/planner/PlannerService.js.map +0 -1
- package/dist/shared/planner/index.d.ts +0 -37
- package/dist/shared/planner/index.d.ts.map +0 -1
- package/dist/shared/planner/index.js +0 -35
- package/dist/shared/planner/index.js.map +0 -1
- package/dist/shared/planner/intervals.d.ts +0 -60
- package/dist/shared/planner/intervals.d.ts.map +0 -1
- package/dist/shared/planner/intervals.js +0 -141
- package/dist/shared/planner/intervals.js.map +0 -1
- package/dist/shared/planner/routes.d.ts +0 -69
- package/dist/shared/planner/routes.d.ts.map +0 -1
- package/dist/shared/planner/routes.js +0 -770
- package/dist/shared/planner/routes.js.map +0 -1
- package/dist/shared/planner/types.d.ts +0 -328
- package/dist/shared/planner/types.d.ts.map +0 -1
- package/dist/shared/planner/types.js +0 -9
- package/dist/shared/planner/types.js.map +0 -1
- package/dist/shared/storage/ImageProcessingService.d.ts +0 -32
- package/dist/shared/storage/ImageProcessingService.d.ts.map +0 -1
- package/dist/shared/storage/ImageProcessingService.js +0 -127
- package/dist/shared/storage/ImageProcessingService.js.map +0 -1
- package/dist/shared/storage/StorageProxyService.d.ts +0 -47
- package/dist/shared/storage/StorageProxyService.d.ts.map +0 -1
- package/dist/shared/storage/StorageProxyService.js +0 -196
- package/dist/shared/storage/StorageProxyService.js.map +0 -1
- package/dist/shared/storage/StorageUploadService.d.ts +0 -126
- package/dist/shared/storage/StorageUploadService.d.ts.map +0 -1
- package/dist/shared/storage/StorageUploadService.js +0 -206
- package/dist/shared/storage/StorageUploadService.js.map +0 -1
- package/dist/shared/storage/creative-urls.d.ts +0 -14
- package/dist/shared/storage/creative-urls.d.ts.map +0 -1
- package/dist/shared/storage/creative-urls.js +0 -30
- package/dist/shared/storage/creative-urls.js.map +0 -1
- package/dist/shared/storage/index.d.ts +0 -28
- package/dist/shared/storage/index.d.ts.map +0 -1
- package/dist/shared/storage/index.js +0 -27
- package/dist/shared/storage/index.js.map +0 -1
- package/dist/shared/storage/routes.d.ts +0 -42
- package/dist/shared/storage/routes.d.ts.map +0 -1
- package/dist/shared/storage/routes.js +0 -160
- package/dist/shared/storage/routes.js.map +0 -1
- package/dist/shared/storage/types.d.ts +0 -53
- package/dist/shared/storage/types.d.ts.map +0 -1
- package/dist/shared/storage/types.js +0 -2
- package/dist/shared/storage/types.js.map +0 -1
- package/dist/shared/telegram/index.d.ts +0 -4
- package/dist/shared/telegram/index.d.ts.map +0 -1
- package/dist/shared/telegram/index.js +0 -3
- package/dist/shared/telegram/index.js.map +0 -1
- package/dist/shared/telegram/routes.d.ts +0 -43
- package/dist/shared/telegram/routes.d.ts.map +0 -1
- package/dist/shared/telegram/routes.js +0 -868
- package/dist/shared/telegram/routes.js.map +0 -1
- package/dist/shared/telegram/types.d.ts +0 -168
- package/dist/shared/telegram/types.d.ts.map +0 -1
- package/dist/shared/telegram/types.js +0 -7
- package/dist/shared/telegram/types.js.map +0 -1
- package/dist/shared/telegram/utils.d.ts +0 -44
- package/dist/shared/telegram/utils.d.ts.map +0 -1
- package/dist/shared/telegram/utils.js +0 -121
- package/dist/shared/telegram/utils.js.map +0 -1
- package/dist/storage.d.ts +0 -9
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js +0 -8
- package/dist/storage.js.map +0 -1
- package/dist/telemetry.d.ts +0 -9
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -8
- package/dist/telemetry.js.map +0 -1
- package/scripts/postinstall.js +0 -79
- package/src/shared/affiliate/migrations/001_create_affiliates.sql +0 -49
- package/src/shared/affiliate/migrations/002_create_affiliate_commissions.sql +0 -31
- package/src/shared/affiliate/migrations/003_create_affiliate_clicks.sql +0 -26
- package/src/shared/affiliate/migrations/004_create_affiliate_payments.sql +0 -34
- package/src/shared/affiliate/migrations/005_create_affiliate_tier_history.sql +0 -19
- package/src/shared/affiliate/migrations/006_create_affiliate_rpc_functions.sql +0 -209
- package/src/shared/affiliate/migrations/007_create_affiliate_rls_policies.sql +0 -123
- package/src/shared/billing/migrations/00000000000001_billing.sql +0 -114
- package/src/shared/email/migrations/000_create_email_logs.sql +0 -27
- package/src/shared/email/migrations/001_create_email_templates.sql +0 -27
- package/src/shared/email/migrations/002_add_rls_baseline_policies.sql +0 -37
- package/src/shared/email/migrations/003_create_gmail_accounts.sql +0 -82
- package/src/shared/email/migrations/004_add_email_logs_tracking_columns.sql +0 -15
- package/src/shared/mcp/migrations/001_mcp_api_tokens.sql +0 -21
- package/src/shared/mcp/migrations/002_mcp_audit_log.sql +0 -16
|
@@ -1,868 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Telegram Route Factory — addTelegramRoutes()
|
|
3
|
-
*
|
|
4
|
-
* Creates all Telegram integration routes and services:
|
|
5
|
-
* - Webhook receiver (POST /webhook)
|
|
6
|
-
* - Admin CRUD: settings, connect, disconnect, test, rules, presets
|
|
7
|
-
* - Notification service (notifyEvent, editMessage, sendMessage)
|
|
8
|
-
* - Webhook auto-registration
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* ```typescript
|
|
12
|
-
* import { addTelegramRoutes } from '@soulbatical/tetra-core';
|
|
13
|
-
*
|
|
14
|
-
* const telegram = addTelegramRoutes({
|
|
15
|
-
* botToken: process.env.MY_TELEGRAM_BOT_TOKEN!,
|
|
16
|
-
* botUsername: 'myapp_bot',
|
|
17
|
-
* appName: 'MyApp',
|
|
18
|
-
* dashboardUrl: 'https://myapp.com/settings/telegram',
|
|
19
|
-
* presets: [...],
|
|
20
|
-
* onMessage: async (ctx) => {
|
|
21
|
-
* // Handle free-text messages — return string to reply
|
|
22
|
-
* return `You said: ${ctx.text}`;
|
|
23
|
-
* },
|
|
24
|
-
* });
|
|
25
|
-
*
|
|
26
|
-
* // Mount in createApp routes:
|
|
27
|
-
* { prefix: '/api/public/telegram/webhook', access: 'public', rateLimit: 'none', router: telegram.webhookRouter },
|
|
28
|
-
* { prefix: '/api/admin/telegram', access: 'admin', router: telegram.adminRouter, middleware: [...] },
|
|
29
|
-
*
|
|
30
|
-
* // Register webhook on startup (in services):
|
|
31
|
-
* { name: 'TelegramWebhook', start: () => telegram.registerWebhook(baseUrl) },
|
|
32
|
-
*
|
|
33
|
-
* // Send notifications from anywhere:
|
|
34
|
-
* telegram.notifyEvent(orgId, projectId, 'campaign_published', 'Campaign X is live!');
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
import { Router } from 'express';
|
|
38
|
-
import crypto from 'crypto';
|
|
39
|
-
import { RFC7807ErrorResponse } from '../rfc7807ErrorResponse.js';
|
|
40
|
-
import { createLogger } from '../../utils/logger.js';
|
|
41
|
-
import { systemDB } from '../../core/systemDb.js';
|
|
42
|
-
import { adminDB } from '../../core/adminDb.js';
|
|
43
|
-
import { splitMessage, containsSecrets, generateIdempotencyHash, isDuplicate, markSent, } from './utils.js';
|
|
44
|
-
const log = createLogger('telegram');
|
|
45
|
-
// ── Telegram API Helpers ────────────────────────────────────
|
|
46
|
-
export class TelegramSecretError extends Error {
|
|
47
|
-
secrets;
|
|
48
|
-
constructor(secrets) {
|
|
49
|
-
super(`Message blocked: contains ${secrets.length} secret(s). Never send keys/tokens to Telegram.`);
|
|
50
|
-
this.secrets = secrets;
|
|
51
|
-
this.name = 'TelegramSecretError';
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
async function telegramApi(botToken, method, body) {
|
|
55
|
-
const res = await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
|
|
56
|
-
method: 'POST',
|
|
57
|
-
headers: { 'Content-Type': 'application/json' },
|
|
58
|
-
body: JSON.stringify(body),
|
|
59
|
-
});
|
|
60
|
-
return res.json();
|
|
61
|
-
}
|
|
62
|
-
async function telegramApiFormData(botToken, method, formData) {
|
|
63
|
-
const res = await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
|
|
64
|
-
method: 'POST',
|
|
65
|
-
body: formData,
|
|
66
|
-
});
|
|
67
|
-
return res.json();
|
|
68
|
-
}
|
|
69
|
-
function getOrgId(req) {
|
|
70
|
-
const user = req.user;
|
|
71
|
-
if (!user)
|
|
72
|
-
return undefined;
|
|
73
|
-
return user.active_organization_id || user.organizationId || user.activeOrganizationId;
|
|
74
|
-
}
|
|
75
|
-
function getUserId(req) {
|
|
76
|
-
return req.user?.id;
|
|
77
|
-
}
|
|
78
|
-
// ── Rule Evaluator ──────────────────────────────────────────
|
|
79
|
-
async function evaluateRules(botToken, userId, ctx) {
|
|
80
|
-
const db = systemDB('telegram-rules');
|
|
81
|
-
try {
|
|
82
|
-
const { data: rules } = await db
|
|
83
|
-
.from('telegram_rules')
|
|
84
|
-
.select('*')
|
|
85
|
-
.eq('organization_id', ctx.organizationId)
|
|
86
|
-
.eq('enabled', true)
|
|
87
|
-
.or(`user_id.is.null,user_id.eq.${userId}`)
|
|
88
|
-
.order('user_id', { ascending: true, nullsFirst: true })
|
|
89
|
-
.order('priority', { ascending: true });
|
|
90
|
-
if (!rules || rules.length === 0) {
|
|
91
|
-
return { action: 'notify', matchedRule: null };
|
|
92
|
-
}
|
|
93
|
-
for (const rule of rules) {
|
|
94
|
-
if (matchesConditions(rule.conditions, ctx)) {
|
|
95
|
-
return { action: rule.action, matchedRule: rule };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return { action: 'notify', matchedRule: null };
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
log.error('Rule evaluation failed, defaulting to notify:', err);
|
|
102
|
-
return { action: 'notify', matchedRule: null };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function matchesConditions(conditions, ctx) {
|
|
106
|
-
if (!conditions || Object.keys(conditions).length === 0)
|
|
107
|
-
return true;
|
|
108
|
-
if (conditions.event_types?.length) {
|
|
109
|
-
if (!conditions.event_types.includes(ctx.eventType))
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
if (conditions.project_ids?.length) {
|
|
113
|
-
if (!ctx.projectId || !conditions.project_ids.includes(ctx.projectId))
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
if (conditions.custom_types?.length) {
|
|
117
|
-
if (!ctx.customType || !conditions.custom_types.includes(ctx.customType))
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
if (conditions.keywords?.length) {
|
|
121
|
-
if (!ctx.message)
|
|
122
|
-
return false;
|
|
123
|
-
const msgLower = ctx.message.toLowerCase();
|
|
124
|
-
if (!conditions.keywords.some(kw => msgLower.includes(kw.toLowerCase())))
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
// ── Factory ─────────────────────────────────────────────────
|
|
130
|
-
export function addTelegramRoutes(config) {
|
|
131
|
-
const { botToken, botUsername, presets = [], knownEvents } = config;
|
|
132
|
-
const appName = config.appName || 'App';
|
|
133
|
-
const dashboardUrl = config.dashboardUrl || '';
|
|
134
|
-
const blockSecrets = config.blockSecrets !== false; // default: true
|
|
135
|
-
const VALID_ACTIONS = ['notify', 'silent'];
|
|
136
|
-
// ── Notification Service ────────────────────────────────
|
|
137
|
-
async function sendMessage(chatId, text, keyboard) {
|
|
138
|
-
// Secret blocking
|
|
139
|
-
if (blockSecrets) {
|
|
140
|
-
const secrets = containsSecrets(text);
|
|
141
|
-
if (secrets.length > 0) {
|
|
142
|
-
throw new TelegramSecretError(secrets);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// Idempotency check
|
|
146
|
-
const hash = generateIdempotencyHash(chatId, text);
|
|
147
|
-
if (isDuplicate(hash)) {
|
|
148
|
-
log.info(`Duplicate blocked for chat ${chatId}: ${text.slice(0, 50)}...`);
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
// Split long messages
|
|
152
|
-
const chunks = splitMessage(text);
|
|
153
|
-
let lastOk = false;
|
|
154
|
-
for (const chunk of chunks) {
|
|
155
|
-
const body = {
|
|
156
|
-
chat_id: chatId,
|
|
157
|
-
text: chunk,
|
|
158
|
-
parse_mode: 'Markdown',
|
|
159
|
-
};
|
|
160
|
-
// Only add keyboard to the last chunk
|
|
161
|
-
if (keyboard && chunk === chunks[chunks.length - 1]) {
|
|
162
|
-
body.reply_markup = keyboard;
|
|
163
|
-
}
|
|
164
|
-
let result = await telegramApi(botToken, 'sendMessage', body);
|
|
165
|
-
// Markdown fallback: retry without parse_mode on parse errors
|
|
166
|
-
if (!result.ok && result.description?.includes("can't parse")) {
|
|
167
|
-
delete body.parse_mode;
|
|
168
|
-
result = await telegramApi(botToken, 'sendMessage', body);
|
|
169
|
-
}
|
|
170
|
-
lastOk = result.ok;
|
|
171
|
-
// Fire onMessageSent hook
|
|
172
|
-
if (result.ok && config.onMessageSent) {
|
|
173
|
-
try {
|
|
174
|
-
config.onMessageSent({
|
|
175
|
-
chatId,
|
|
176
|
-
type: 'message',
|
|
177
|
-
text: chunk,
|
|
178
|
-
messageId: result.result?.message_id,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
catch { /* fire and forget */ }
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
markSent(hash);
|
|
185
|
-
return lastOk;
|
|
186
|
-
}
|
|
187
|
-
async function editMessage(chatId, messageId, newText, keyboard) {
|
|
188
|
-
const body = {
|
|
189
|
-
chat_id: chatId,
|
|
190
|
-
message_id: messageId,
|
|
191
|
-
text: newText,
|
|
192
|
-
parse_mode: 'Markdown',
|
|
193
|
-
};
|
|
194
|
-
if (keyboard)
|
|
195
|
-
body.reply_markup = keyboard;
|
|
196
|
-
let result = await telegramApi(botToken, 'editMessageText', body);
|
|
197
|
-
// Markdown fallback
|
|
198
|
-
if (!result.ok && result.description?.includes("can't parse")) {
|
|
199
|
-
delete body.parse_mode;
|
|
200
|
-
result = await telegramApi(botToken, 'editMessageText', body);
|
|
201
|
-
}
|
|
202
|
-
return result.ok;
|
|
203
|
-
}
|
|
204
|
-
async function sendPhoto(chatId, photo, options) {
|
|
205
|
-
// Secret blocking on caption
|
|
206
|
-
if (blockSecrets && options?.caption) {
|
|
207
|
-
const secrets = containsSecrets(options.caption);
|
|
208
|
-
if (secrets.length > 0)
|
|
209
|
-
throw new TelegramSecretError(secrets);
|
|
210
|
-
}
|
|
211
|
-
if (typeof photo === 'string') {
|
|
212
|
-
// URL — send as JSON
|
|
213
|
-
const body = {
|
|
214
|
-
chat_id: chatId,
|
|
215
|
-
photo,
|
|
216
|
-
parse_mode: 'Markdown',
|
|
217
|
-
};
|
|
218
|
-
if (options?.caption)
|
|
219
|
-
body.caption = options.caption;
|
|
220
|
-
let result = await telegramApi(botToken, 'sendPhoto', body);
|
|
221
|
-
// Markdown fallback on caption
|
|
222
|
-
if (!result.ok && result.description?.includes("can't parse")) {
|
|
223
|
-
delete body.parse_mode;
|
|
224
|
-
result = await telegramApi(botToken, 'sendPhoto', body);
|
|
225
|
-
}
|
|
226
|
-
if (result.ok && config.onMessageSent) {
|
|
227
|
-
try {
|
|
228
|
-
config.onMessageSent({
|
|
229
|
-
chatId,
|
|
230
|
-
type: 'photo',
|
|
231
|
-
text: options?.caption || '[photo]',
|
|
232
|
-
messageId: result.result?.message_id,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
catch { /* fire and forget */ }
|
|
236
|
-
}
|
|
237
|
-
return result.ok;
|
|
238
|
-
}
|
|
239
|
-
// Buffer — send as FormData
|
|
240
|
-
const formData = new FormData();
|
|
241
|
-
formData.append('chat_id', String(chatId));
|
|
242
|
-
formData.append('photo', new Blob([new Uint8Array(photo)], { type: 'image/png' }), options?.fileName || 'photo.png');
|
|
243
|
-
if (options?.caption) {
|
|
244
|
-
formData.append('caption', options.caption);
|
|
245
|
-
formData.append('parse_mode', 'Markdown');
|
|
246
|
-
}
|
|
247
|
-
let result = await telegramApiFormData(botToken, 'sendPhoto', formData);
|
|
248
|
-
// Markdown fallback on caption
|
|
249
|
-
if (!result.ok && result.description?.includes("can't parse") && options?.caption) {
|
|
250
|
-
const retryForm = new FormData();
|
|
251
|
-
retryForm.append('chat_id', String(chatId));
|
|
252
|
-
retryForm.append('photo', new Blob([new Uint8Array(photo)], { type: 'image/png' }), options?.fileName || 'photo.png');
|
|
253
|
-
retryForm.append('caption', options.caption);
|
|
254
|
-
// No parse_mode
|
|
255
|
-
result = await telegramApiFormData(botToken, 'sendPhoto', retryForm);
|
|
256
|
-
}
|
|
257
|
-
if (result.ok && config.onMessageSent) {
|
|
258
|
-
try {
|
|
259
|
-
config.onMessageSent({
|
|
260
|
-
chatId,
|
|
261
|
-
type: 'photo',
|
|
262
|
-
text: options?.caption || '[photo]',
|
|
263
|
-
messageId: result.result?.message_id,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
catch { /* fire and forget */ }
|
|
267
|
-
}
|
|
268
|
-
return result.ok;
|
|
269
|
-
}
|
|
270
|
-
async function sendDocument(chatId, document, options) {
|
|
271
|
-
// Secret blocking on caption
|
|
272
|
-
if (blockSecrets && options?.caption) {
|
|
273
|
-
const secrets = containsSecrets(options.caption);
|
|
274
|
-
if (secrets.length > 0)
|
|
275
|
-
throw new TelegramSecretError(secrets);
|
|
276
|
-
}
|
|
277
|
-
if (typeof document === 'string') {
|
|
278
|
-
// URL — send as JSON
|
|
279
|
-
const body = {
|
|
280
|
-
chat_id: chatId,
|
|
281
|
-
document,
|
|
282
|
-
};
|
|
283
|
-
if (options?.caption)
|
|
284
|
-
body.caption = options.caption;
|
|
285
|
-
const result = await telegramApi(botToken, 'sendDocument', body);
|
|
286
|
-
if (result.ok && config.onMessageSent) {
|
|
287
|
-
try {
|
|
288
|
-
config.onMessageSent({
|
|
289
|
-
chatId,
|
|
290
|
-
type: 'document',
|
|
291
|
-
text: options?.caption || '[document]',
|
|
292
|
-
messageId: result.result?.message_id,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
catch { /* fire and forget */ }
|
|
296
|
-
}
|
|
297
|
-
return result.ok;
|
|
298
|
-
}
|
|
299
|
-
// Buffer — send as FormData
|
|
300
|
-
const formData = new FormData();
|
|
301
|
-
formData.append('chat_id', String(chatId));
|
|
302
|
-
formData.append('document', new Blob([new Uint8Array(document)]), options?.fileName || 'document');
|
|
303
|
-
if (options?.caption)
|
|
304
|
-
formData.append('caption', options.caption);
|
|
305
|
-
const result = await telegramApiFormData(botToken, 'sendDocument', formData);
|
|
306
|
-
if (result.ok && config.onMessageSent) {
|
|
307
|
-
try {
|
|
308
|
-
config.onMessageSent({
|
|
309
|
-
chatId,
|
|
310
|
-
type: 'document',
|
|
311
|
-
text: options?.caption || '[document]',
|
|
312
|
-
messageId: result.result?.message_id,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
catch { /* fire and forget */ }
|
|
316
|
-
}
|
|
317
|
-
return result.ok;
|
|
318
|
-
}
|
|
319
|
-
async function notifyEvent(organizationId, projectId, eventType, message, options) {
|
|
320
|
-
if (!organizationId)
|
|
321
|
-
return 0;
|
|
322
|
-
const db = systemDB('telegram-notify');
|
|
323
|
-
const { data: settings } = await db
|
|
324
|
-
.from('user_telegram_settings')
|
|
325
|
-
.select('user_id, telegram_chat_id, project_ids, notification_events, permissions')
|
|
326
|
-
.eq('organization_id', organizationId)
|
|
327
|
-
.eq('enabled', true);
|
|
328
|
-
if (!settings || settings.length === 0)
|
|
329
|
-
return 0;
|
|
330
|
-
let sentCount = 0;
|
|
331
|
-
for (const setting of settings) {
|
|
332
|
-
if (!setting.telegram_chat_id)
|
|
333
|
-
continue;
|
|
334
|
-
// Filter by project
|
|
335
|
-
if (setting.project_ids !== null && projectId) {
|
|
336
|
-
if (!setting.project_ids.includes(projectId))
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
if (setting.project_ids !== null && !projectId)
|
|
340
|
-
continue;
|
|
341
|
-
// Filter by notification_events
|
|
342
|
-
if (setting.notification_events !== null) {
|
|
343
|
-
if (!setting.notification_events.includes(eventType))
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
// Filter by required permission
|
|
347
|
-
if (options?.requiredPermission) {
|
|
348
|
-
if (!setting.permissions?.includes(options.requiredPermission))
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
// Evaluate rules engine
|
|
352
|
-
const { action } = await evaluateRules(botToken, setting.user_id, {
|
|
353
|
-
organizationId,
|
|
354
|
-
projectId,
|
|
355
|
-
eventType,
|
|
356
|
-
customType: options?.customType,
|
|
357
|
-
message,
|
|
358
|
-
});
|
|
359
|
-
if (action === 'silent')
|
|
360
|
-
continue;
|
|
361
|
-
const ok = await sendMessage(setting.telegram_chat_id, message, options?.keyboard);
|
|
362
|
-
if (ok)
|
|
363
|
-
sentCount++;
|
|
364
|
-
}
|
|
365
|
-
return sentCount;
|
|
366
|
-
}
|
|
367
|
-
// ── Webhook Router (public, no auth) ────────────────────
|
|
368
|
-
const webhookRouter = Router();
|
|
369
|
-
webhookRouter.post('/', async (req, res) => {
|
|
370
|
-
// Always return 200 to Telegram
|
|
371
|
-
res.sendStatus(200);
|
|
372
|
-
try {
|
|
373
|
-
const update = req.body;
|
|
374
|
-
// Handle callback queries
|
|
375
|
-
if (update?.callback_query && config.onCallbackQuery) {
|
|
376
|
-
const cq = update.callback_query;
|
|
377
|
-
const chatId = cq.message?.chat?.id;
|
|
378
|
-
const messageId = cq.message?.message_id;
|
|
379
|
-
// Answer callback to remove loading spinner
|
|
380
|
-
await telegramApi(botToken, 'answerCallbackQuery', { callback_query_id: cq.id });
|
|
381
|
-
// Look up user by chat_id
|
|
382
|
-
const db = systemDB('telegram-webhook');
|
|
383
|
-
const { data: settings } = await db
|
|
384
|
-
.from('user_telegram_settings')
|
|
385
|
-
.select('*')
|
|
386
|
-
.eq('telegram_chat_id', String(chatId))
|
|
387
|
-
.maybeSingle();
|
|
388
|
-
if (settings) {
|
|
389
|
-
const replyText = await config.onCallbackQuery({
|
|
390
|
-
chatId,
|
|
391
|
-
messageId,
|
|
392
|
-
callbackData: cq.data || '',
|
|
393
|
-
firstName: cq.from?.first_name || '',
|
|
394
|
-
userId: settings.user_id,
|
|
395
|
-
organizationId: settings.organization_id,
|
|
396
|
-
settings,
|
|
397
|
-
});
|
|
398
|
-
if (replyText) {
|
|
399
|
-
await editMessage(String(chatId), messageId, replyText);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const message = update?.message;
|
|
405
|
-
if (!message?.text)
|
|
406
|
-
return;
|
|
407
|
-
const text = message.text.trim();
|
|
408
|
-
const chatId = message.chat.id;
|
|
409
|
-
const firstName = message.from?.first_name || 'there';
|
|
410
|
-
// /start <token> — deep link pairing
|
|
411
|
-
if (text.startsWith('/start ')) {
|
|
412
|
-
const linkToken = text.slice(7).trim();
|
|
413
|
-
if (!linkToken || linkToken.length < 16)
|
|
414
|
-
return;
|
|
415
|
-
const db = systemDB('telegram-webhook');
|
|
416
|
-
const { data: settings, error } = await db
|
|
417
|
-
.from('user_telegram_settings')
|
|
418
|
-
.select('id, link_token_expires_at')
|
|
419
|
-
.eq('link_token', linkToken)
|
|
420
|
-
.maybeSingle();
|
|
421
|
-
if (error || !settings) {
|
|
422
|
-
await sendMessage(chatId, '\u274C This link is invalid or expired. Go back and try again.');
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
if (settings.link_token_expires_at && new Date(settings.link_token_expires_at) < new Date()) {
|
|
426
|
-
await sendMessage(chatId, `\u23F0 This link has expired. Go back to ${appName} and click "Connect Telegram" again.`);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
const { error: updateError } = await db
|
|
430
|
-
.from('user_telegram_settings')
|
|
431
|
-
.update({
|
|
432
|
-
telegram_chat_id: String(chatId),
|
|
433
|
-
enabled: true,
|
|
434
|
-
link_token: null,
|
|
435
|
-
link_token_expires_at: null,
|
|
436
|
-
updated_at: new Date().toISOString(),
|
|
437
|
-
})
|
|
438
|
-
.eq('id', settings.id);
|
|
439
|
-
if (updateError) {
|
|
440
|
-
log.error('Could not save chat_id:', updateError);
|
|
441
|
-
await sendMessage(chatId, '\u274C Something went wrong. Please try again.');
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
await sendMessage(chatId, `\u2705 *${appName} connected!*\n\nHi ${firstName}! You will now receive Telegram notifications.\n\nYou can customize which notifications you receive in the ${appName} dashboard.`);
|
|
445
|
-
log.info(`Telegram linked: chat ${chatId}`);
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
// /start — no token
|
|
449
|
-
if (text === '/start') {
|
|
450
|
-
const msg = config.welcomeMessage ||
|
|
451
|
-
`\u{1F680} *Welcome to ${appName}!*\n\nGo to your ${appName} dashboard and click "Connect Telegram" to enable notifications.${dashboardUrl ? `\n\n${dashboardUrl}` : ''}`;
|
|
452
|
-
await sendMessage(chatId, msg);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
// /help
|
|
456
|
-
if (text === '/help') {
|
|
457
|
-
const msg = config.helpMessage ||
|
|
458
|
-
`\u{1F680} *${appName} Bot*\n\nThis bot sends you notifications from ${appName}.\n\n*Setup:*\n1. Go to your ${appName} dashboard\n2. Click "Connect Telegram"\n3. Done!\n\n*Commands:*\n/start \u2014 Connect your account\n/help \u2014 This message\n/status \u2014 Check your status`;
|
|
459
|
-
await sendMessage(chatId, msg);
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
// /status
|
|
463
|
-
if (text === '/status') {
|
|
464
|
-
const db = systemDB('telegram-webhook');
|
|
465
|
-
const { data: settings } = await db
|
|
466
|
-
.from('user_telegram_settings')
|
|
467
|
-
.select('enabled, project_ids')
|
|
468
|
-
.eq('telegram_chat_id', String(chatId))
|
|
469
|
-
.maybeSingle();
|
|
470
|
-
if (!settings) {
|
|
471
|
-
await sendMessage(chatId, `\u274C Your Telegram is not yet linked to ${appName}.${dashboardUrl ? ` Go to ${dashboardUrl}` : ''}`);
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
const status = settings.enabled ? '\u2705 Active' : '\u23F8 Paused';
|
|
475
|
-
const projects = settings.project_ids ? `${settings.project_ids.length} project(s)` : 'All projects';
|
|
476
|
-
await sendMessage(chatId, `\u{1F680} *${appName} Status*\n\n*Notifications:* ${status}\n*Projects:* ${projects}`);
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
// Free-text messages — forward to onMessage handler if connected
|
|
480
|
-
if (config.onMessage) {
|
|
481
|
-
const db = systemDB('telegram-webhook');
|
|
482
|
-
const { data: settings } = await db
|
|
483
|
-
.from('user_telegram_settings')
|
|
484
|
-
.select('*')
|
|
485
|
-
.eq('telegram_chat_id', String(chatId))
|
|
486
|
-
.maybeSingle();
|
|
487
|
-
if (!settings) {
|
|
488
|
-
await sendMessage(chatId, `\u274C Your Telegram is not yet linked to ${appName}. Use /start to connect.`);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
if (!settings.enabled) {
|
|
492
|
-
await sendMessage(chatId, `\u23F8 Your notifications are paused. Enable them in the ${appName} dashboard.`);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
const reply = await config.onMessage({
|
|
496
|
-
chatId,
|
|
497
|
-
text,
|
|
498
|
-
firstName,
|
|
499
|
-
userId: settings.user_id,
|
|
500
|
-
organizationId: settings.organization_id,
|
|
501
|
-
settings,
|
|
502
|
-
});
|
|
503
|
-
if (reply) {
|
|
504
|
-
await sendMessage(chatId, reply);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
catch (err) {
|
|
509
|
-
log.error('Webhook processing error:', err);
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
// ── Admin Router (authenticated) ──────────────────────
|
|
513
|
-
const adminRouter = Router();
|
|
514
|
-
// GET /settings
|
|
515
|
-
adminRouter.get('/settings', async (req, res) => {
|
|
516
|
-
const userId = getUserId(req);
|
|
517
|
-
const orgId = getOrgId(req);
|
|
518
|
-
if (!orgId) {
|
|
519
|
-
res.json({ success: true, data: null, bot_available: true });
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
const db = await adminDB(req);
|
|
523
|
-
const { data, error } = await db
|
|
524
|
-
.from('user_telegram_settings')
|
|
525
|
-
.select('id, user_id, organization_id, telegram_chat_id, enabled, project_ids, notification_events, permissions, created_at, updated_at')
|
|
526
|
-
.eq('user_id', userId)
|
|
527
|
-
.eq('organization_id', orgId)
|
|
528
|
-
.maybeSingle();
|
|
529
|
-
if (error) {
|
|
530
|
-
RFC7807ErrorResponse.internalError(res, 'Could not retrieve Telegram settings.');
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
res.json({
|
|
534
|
-
success: true,
|
|
535
|
-
data: data ? { ...data, connected: !!data.telegram_chat_id } : null,
|
|
536
|
-
bot_available: true,
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
// POST /connect
|
|
540
|
-
adminRouter.post('/connect', async (req, res) => {
|
|
541
|
-
const userId = getUserId(req);
|
|
542
|
-
const orgId = getOrgId(req);
|
|
543
|
-
if (!orgId) {
|
|
544
|
-
RFC7807ErrorResponse.badRequest(res, 'No active organization.');
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
const linkToken = crypto.randomBytes(16).toString('hex');
|
|
548
|
-
const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
|
549
|
-
const db = await adminDB(req);
|
|
550
|
-
const { error } = await db
|
|
551
|
-
.from('user_telegram_settings')
|
|
552
|
-
.upsert({
|
|
553
|
-
user_id: userId,
|
|
554
|
-
organization_id: orgId,
|
|
555
|
-
link_token: linkToken,
|
|
556
|
-
link_token_expires_at: expiresAt,
|
|
557
|
-
updated_at: new Date().toISOString(),
|
|
558
|
-
}, { onConflict: 'user_id,organization_id' });
|
|
559
|
-
if (error) {
|
|
560
|
-
log.error('Connect token upsert error:', error);
|
|
561
|
-
RFC7807ErrorResponse.internalError(res, 'Could not create connection link.');
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
const deepLink = `https://t.me/${botUsername}?start=${linkToken}`;
|
|
565
|
-
res.json({ success: true, data: { deep_link: deepLink, expires_in: 600 } });
|
|
566
|
-
});
|
|
567
|
-
// POST /disconnect
|
|
568
|
-
adminRouter.post('/disconnect', async (req, res) => {
|
|
569
|
-
const userId = getUserId(req);
|
|
570
|
-
const orgId = getOrgId(req);
|
|
571
|
-
if (!orgId) {
|
|
572
|
-
RFC7807ErrorResponse.badRequest(res, 'No active organization.');
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const db = await adminDB(req);
|
|
576
|
-
await db
|
|
577
|
-
.from('user_telegram_settings')
|
|
578
|
-
.update({
|
|
579
|
-
telegram_chat_id: null,
|
|
580
|
-
enabled: false,
|
|
581
|
-
link_token: null,
|
|
582
|
-
link_token_expires_at: null,
|
|
583
|
-
updated_at: new Date().toISOString(),
|
|
584
|
-
})
|
|
585
|
-
.eq('user_id', userId)
|
|
586
|
-
.eq('organization_id', orgId);
|
|
587
|
-
res.json({ success: true, data: { message: 'Telegram disconnected.' } });
|
|
588
|
-
});
|
|
589
|
-
// PUT /settings
|
|
590
|
-
adminRouter.put('/settings', async (req, res) => {
|
|
591
|
-
const userId = getUserId(req);
|
|
592
|
-
const orgId = getOrgId(req);
|
|
593
|
-
if (!orgId) {
|
|
594
|
-
RFC7807ErrorResponse.badRequest(res, 'No active organization.');
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
const { enabled, project_ids, notification_events } = req.body;
|
|
598
|
-
if (project_ids != null && !Array.isArray(project_ids)) {
|
|
599
|
-
RFC7807ErrorResponse.badRequest(res, 'project_ids must be an array.');
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
if (notification_events != null) {
|
|
603
|
-
if (!Array.isArray(notification_events)) {
|
|
604
|
-
RFC7807ErrorResponse.badRequest(res, 'notification_events must be an array or null.');
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
if (knownEvents) {
|
|
608
|
-
const knownSet = new Set(knownEvents);
|
|
609
|
-
for (const evt of notification_events) {
|
|
610
|
-
if (typeof evt !== 'string' || !knownSet.has(evt)) {
|
|
611
|
-
RFC7807ErrorResponse.badRequest(res, `Unknown event type: ${evt}`);
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
const db = await adminDB(req);
|
|
618
|
-
const { data: existing } = await db
|
|
619
|
-
.from('user_telegram_settings')
|
|
620
|
-
.select('telegram_chat_id')
|
|
621
|
-
.eq('user_id', userId)
|
|
622
|
-
.eq('organization_id', orgId)
|
|
623
|
-
.maybeSingle();
|
|
624
|
-
if (enabled && (!existing || !existing.telegram_chat_id)) {
|
|
625
|
-
RFC7807ErrorResponse.badRequest(res, 'Connect Telegram first before enabling notifications.');
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
const updates = {
|
|
629
|
-
enabled: enabled ?? false,
|
|
630
|
-
project_ids: project_ids ?? null,
|
|
631
|
-
updated_at: new Date().toISOString(),
|
|
632
|
-
};
|
|
633
|
-
if (notification_events !== undefined)
|
|
634
|
-
updates.notification_events = notification_events;
|
|
635
|
-
const { data, error } = await db
|
|
636
|
-
.from('user_telegram_settings')
|
|
637
|
-
.update(updates)
|
|
638
|
-
.eq('user_id', userId)
|
|
639
|
-
.eq('organization_id', orgId)
|
|
640
|
-
.select()
|
|
641
|
-
.single();
|
|
642
|
-
if (error) {
|
|
643
|
-
RFC7807ErrorResponse.internalError(res, 'Could not save settings.');
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
res.json({ success: true, data });
|
|
647
|
-
});
|
|
648
|
-
// POST /test
|
|
649
|
-
adminRouter.post('/test', async (req, res) => {
|
|
650
|
-
const userId = getUserId(req);
|
|
651
|
-
const orgId = getOrgId(req);
|
|
652
|
-
if (!orgId) {
|
|
653
|
-
RFC7807ErrorResponse.badRequest(res, 'No active organization.');
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
const db = await adminDB(req);
|
|
657
|
-
const { data: settings } = await db
|
|
658
|
-
.from('user_telegram_settings')
|
|
659
|
-
.select('telegram_chat_id')
|
|
660
|
-
.eq('user_id', userId)
|
|
661
|
-
.eq('organization_id', orgId)
|
|
662
|
-
.maybeSingle();
|
|
663
|
-
if (!settings?.telegram_chat_id) {
|
|
664
|
-
RFC7807ErrorResponse.badRequest(res, 'Telegram is not connected.');
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
const ok = await sendMessage(settings.telegram_chat_id, `\u2705 *${appName} test* \u2014 Telegram works!\n\nYou will receive notifications here.`);
|
|
668
|
-
if (ok) {
|
|
669
|
-
res.json({ success: true, data: { message: 'Test message sent!' } });
|
|
670
|
-
}
|
|
671
|
-
else {
|
|
672
|
-
RFC7807ErrorResponse.badRequest(res, 'Could not send test message.');
|
|
673
|
-
}
|
|
674
|
-
});
|
|
675
|
-
// ── Rules CRUD ──────────────────────────────────────────
|
|
676
|
-
// GET /rules
|
|
677
|
-
adminRouter.get('/rules', async (req, res) => {
|
|
678
|
-
const userId = getUserId(req);
|
|
679
|
-
const orgId = getOrgId(req);
|
|
680
|
-
if (!orgId) {
|
|
681
|
-
res.json({ success: true, data: [] });
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
const db = await adminDB(req);
|
|
685
|
-
const { data, error } = await db
|
|
686
|
-
.from('telegram_rules')
|
|
687
|
-
.select('*')
|
|
688
|
-
.eq('organization_id', orgId)
|
|
689
|
-
.or(`user_id.is.null,user_id.eq.${userId}`)
|
|
690
|
-
.order('priority', { ascending: true });
|
|
691
|
-
if (error) {
|
|
692
|
-
RFC7807ErrorResponse.internalError(res, 'Could not retrieve rules.');
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
res.json({ success: true, data: data || [] });
|
|
696
|
-
});
|
|
697
|
-
// POST /rules
|
|
698
|
-
adminRouter.post('/rules', async (req, res) => {
|
|
699
|
-
const userId = getUserId(req);
|
|
700
|
-
const orgId = getOrgId(req);
|
|
701
|
-
if (!orgId) {
|
|
702
|
-
RFC7807ErrorResponse.badRequest(res, 'No active organization.');
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
const { name, conditions, action, priority, enabled } = req.body;
|
|
706
|
-
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
707
|
-
RFC7807ErrorResponse.badRequest(res, 'Name is required.');
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
if (action && !VALID_ACTIONS.includes(action)) {
|
|
711
|
-
RFC7807ErrorResponse.badRequest(res, `Invalid action: ${action}`);
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
const db = await adminDB(req);
|
|
715
|
-
const { data, error } = await db
|
|
716
|
-
.from('telegram_rules')
|
|
717
|
-
.insert({
|
|
718
|
-
organization_id: orgId,
|
|
719
|
-
user_id: userId,
|
|
720
|
-
name: name.trim(),
|
|
721
|
-
conditions: conditions || {},
|
|
722
|
-
action: action || 'notify',
|
|
723
|
-
priority: typeof priority === 'number' ? priority : 100,
|
|
724
|
-
enabled: enabled !== false,
|
|
725
|
-
})
|
|
726
|
-
.select()
|
|
727
|
-
.single();
|
|
728
|
-
if (error) {
|
|
729
|
-
RFC7807ErrorResponse.internalError(res, 'Could not create rule.');
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
res.json({ success: true, data });
|
|
733
|
-
});
|
|
734
|
-
// PUT /rules/:id
|
|
735
|
-
adminRouter.put('/rules/:id', async (req, res) => {
|
|
736
|
-
const userId = getUserId(req);
|
|
737
|
-
const orgId = getOrgId(req);
|
|
738
|
-
if (!orgId) {
|
|
739
|
-
RFC7807ErrorResponse.badRequest(res, 'No active organization.');
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
const { name, conditions, action, priority, enabled } = req.body;
|
|
743
|
-
if (action && !VALID_ACTIONS.includes(action)) {
|
|
744
|
-
RFC7807ErrorResponse.badRequest(res, `Invalid action: ${action}`);
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
const updates = { updated_at: new Date().toISOString() };
|
|
748
|
-
if (name !== undefined)
|
|
749
|
-
updates.name = name;
|
|
750
|
-
if (conditions !== undefined)
|
|
751
|
-
updates.conditions = conditions;
|
|
752
|
-
if (action !== undefined)
|
|
753
|
-
updates.action = action;
|
|
754
|
-
if (priority !== undefined)
|
|
755
|
-
updates.priority = priority;
|
|
756
|
-
if (enabled !== undefined)
|
|
757
|
-
updates.enabled = enabled;
|
|
758
|
-
const db = await adminDB(req);
|
|
759
|
-
const { data, error } = await db
|
|
760
|
-
.from('telegram_rules')
|
|
761
|
-
.update(updates)
|
|
762
|
-
.eq('id', req.params.id)
|
|
763
|
-
.eq('user_id', userId)
|
|
764
|
-
.eq('organization_id', orgId)
|
|
765
|
-
.select()
|
|
766
|
-
.single();
|
|
767
|
-
if (error || !data) {
|
|
768
|
-
RFC7807ErrorResponse.notFound(res, 'Rule not found.');
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
res.json({ success: true, data });
|
|
772
|
-
});
|
|
773
|
-
// DELETE /rules/:id
|
|
774
|
-
adminRouter.delete('/rules/:id', async (req, res) => {
|
|
775
|
-
const userId = getUserId(req);
|
|
776
|
-
const orgId = getOrgId(req);
|
|
777
|
-
if (!orgId) {
|
|
778
|
-
RFC7807ErrorResponse.badRequest(res, 'No active organization.');
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
const db = await adminDB(req);
|
|
782
|
-
await db
|
|
783
|
-
.from('telegram_rules')
|
|
784
|
-
.delete()
|
|
785
|
-
.eq('id', req.params.id)
|
|
786
|
-
.eq('user_id', userId)
|
|
787
|
-
.eq('organization_id', orgId);
|
|
788
|
-
res.json({ success: true, data: { message: 'Rule deleted.' } });
|
|
789
|
-
});
|
|
790
|
-
// GET /presets
|
|
791
|
-
adminRouter.get('/presets', async (_req, res) => {
|
|
792
|
-
res.json({ success: true, data: presets });
|
|
793
|
-
});
|
|
794
|
-
// POST /rules/from-preset
|
|
795
|
-
adminRouter.post('/rules/from-preset', async (req, res) => {
|
|
796
|
-
const userId = getUserId(req);
|
|
797
|
-
const orgId = getOrgId(req);
|
|
798
|
-
if (!orgId) {
|
|
799
|
-
RFC7807ErrorResponse.badRequest(res, 'No active organization.');
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
const { preset_key } = req.body;
|
|
803
|
-
const preset = presets.find(p => p.key === preset_key);
|
|
804
|
-
if (!preset) {
|
|
805
|
-
RFC7807ErrorResponse.badRequest(res, `Unknown preset: ${preset_key}`);
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
const db = await adminDB(req);
|
|
809
|
-
const { data: existing } = await db
|
|
810
|
-
.from('telegram_rules')
|
|
811
|
-
.select('id')
|
|
812
|
-
.eq('organization_id', orgId)
|
|
813
|
-
.eq('user_id', userId)
|
|
814
|
-
.eq('preset_key', preset_key)
|
|
815
|
-
.maybeSingle();
|
|
816
|
-
if (existing) {
|
|
817
|
-
RFC7807ErrorResponse.badRequest(res, 'This preset is already active.');
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const { data, error } = await db
|
|
821
|
-
.from('telegram_rules')
|
|
822
|
-
.insert({
|
|
823
|
-
organization_id: orgId,
|
|
824
|
-
user_id: userId,
|
|
825
|
-
name: preset.name_en,
|
|
826
|
-
conditions: preset.conditions,
|
|
827
|
-
action: preset.action,
|
|
828
|
-
priority: preset.priority,
|
|
829
|
-
preset_key: preset.key,
|
|
830
|
-
enabled: true,
|
|
831
|
-
})
|
|
832
|
-
.select()
|
|
833
|
-
.single();
|
|
834
|
-
if (error) {
|
|
835
|
-
RFC7807ErrorResponse.internalError(res, 'Could not create preset rule.');
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
res.json({ success: true, data });
|
|
839
|
-
});
|
|
840
|
-
// ── Webhook Registration ────────────────────────────────
|
|
841
|
-
async function registerWebhook(baseUrl) {
|
|
842
|
-
const webhookUrl = `https://${baseUrl.replace(/^https?:\/\//, '')}/api/public/telegram/webhook`;
|
|
843
|
-
try {
|
|
844
|
-
const result = await telegramApi(botToken, 'setWebhook', { url: webhookUrl });
|
|
845
|
-
if (result.ok) {
|
|
846
|
-
log.info(`Webhook registered: ${webhookUrl}`);
|
|
847
|
-
return true;
|
|
848
|
-
}
|
|
849
|
-
log.error(`Webhook registration failed: ${result.description}`);
|
|
850
|
-
return false;
|
|
851
|
-
}
|
|
852
|
-
catch (err) {
|
|
853
|
-
log.error('Webhook registration error:', err);
|
|
854
|
-
return false;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
return {
|
|
858
|
-
notifyEvent,
|
|
859
|
-
editMessage,
|
|
860
|
-
sendMessage,
|
|
861
|
-
sendPhoto,
|
|
862
|
-
sendDocument,
|
|
863
|
-
adminRouter,
|
|
864
|
-
webhookRouter,
|
|
865
|
-
registerWebhook,
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
//# sourceMappingURL=routes.js.map
|