@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.
Files changed (271) hide show
  1. package/README.md +78 -38
  2. package/dist/core/createApp.d.ts +1 -1
  3. package/dist/core/createApp.d.ts.map +1 -1
  4. package/dist/core/createApp.js +77 -2
  5. package/dist/core/createApp.js.map +1 -1
  6. package/dist/core/dualWriteProxy.d.ts +7 -2
  7. package/dist/core/dualWriteProxy.d.ts.map +1 -1
  8. package/dist/core/dualWriteProxy.js +16 -5
  9. package/dist/core/dualWriteProxy.js.map +1 -1
  10. package/dist/core/routeContext.d.ts +24 -0
  11. package/dist/core/routeContext.d.ts.map +1 -1
  12. package/dist/core/routeContext.js +31 -4
  13. package/dist/core/routeContext.js.map +1 -1
  14. package/dist/core/systemDb.d.ts +2 -2
  15. package/dist/core/systemDb.js +2 -2
  16. package/dist/generators/rls-checker.d.ts +1 -1
  17. package/dist/generators/rls-checker.js +1 -1
  18. package/dist/generators/rls-exec-sql.d.ts +1 -1
  19. package/dist/generators/rls-exec-sql.js +1 -1
  20. package/dist/generators/rpc/index.d.ts +1 -1
  21. package/dist/generators/rpc/index.js +1 -1
  22. package/dist/index.d.ts +3 -31
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -32
  25. package/dist/index.js.map +1 -1
  26. package/dist/middleware/securityMiddleware.d.ts +1 -1
  27. package/dist/middleware/securityMiddleware.d.ts.map +1 -1
  28. package/dist/middleware/validateBody.d.ts.map +1 -1
  29. package/dist/middleware/validateBody.js +51 -8
  30. package/dist/middleware/validateBody.js.map +1 -1
  31. package/dist/shared/rfc7807ErrorResponse.d.ts +7 -0
  32. package/dist/shared/rfc7807ErrorResponse.d.ts.map +1 -1
  33. package/dist/shared/rfc7807ErrorResponse.js +19 -5
  34. package/dist/shared/rfc7807ErrorResponse.js.map +1 -1
  35. package/dist/utils/logger.d.ts.map +1 -1
  36. package/dist/utils/logger.js +16 -1
  37. package/dist/utils/logger.js.map +1 -1
  38. package/package.json +33 -77
  39. package/dist/affiliate.d.ts +0 -11
  40. package/dist/affiliate.d.ts.map +0 -1
  41. package/dist/affiliate.js +0 -10
  42. package/dist/affiliate.js.map +0 -1
  43. package/dist/billing.d.ts +0 -8
  44. package/dist/billing.d.ts.map +0 -1
  45. package/dist/billing.js +0 -7
  46. package/dist/billing.js.map +0 -1
  47. package/dist/email.d.ts +0 -9
  48. package/dist/email.d.ts.map +0 -1
  49. package/dist/email.js +0 -8
  50. package/dist/email.js.map +0 -1
  51. package/dist/generators/rls-exec-sql.sql +0 -57
  52. package/dist/generators.d.ts +0 -15
  53. package/dist/generators.d.ts.map +0 -1
  54. package/dist/generators.js +0 -12
  55. package/dist/generators.js.map +0 -1
  56. package/dist/mcp.d.ts +0 -8
  57. package/dist/mcp.d.ts.map +0 -1
  58. package/dist/mcp.js +0 -7
  59. package/dist/mcp.js.map +0 -1
  60. package/dist/planner.d.ts +0 -8
  61. package/dist/planner.d.ts.map +0 -1
  62. package/dist/planner.js +0 -7
  63. package/dist/planner.js.map +0 -1
  64. package/dist/shared/affiliate/AffiliateAttributionService.d.ts +0 -47
  65. package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +0 -1
  66. package/dist/shared/affiliate/AffiliateAttributionService.js +0 -308
  67. package/dist/shared/affiliate/AffiliateAttributionService.js.map +0 -1
  68. package/dist/shared/affiliate/AffiliateClickService.d.ts +0 -35
  69. package/dist/shared/affiliate/AffiliateClickService.d.ts.map +0 -1
  70. package/dist/shared/affiliate/AffiliateClickService.js +0 -87
  71. package/dist/shared/affiliate/AffiliateClickService.js.map +0 -1
  72. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +0 -11
  73. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +0 -1
  74. package/dist/shared/affiliate/affiliateFeatureConfig.js +0 -242
  75. package/dist/shared/affiliate/affiliateFeatureConfig.js.map +0 -1
  76. package/dist/shared/affiliate/index.d.ts +0 -11
  77. package/dist/shared/affiliate/index.d.ts.map +0 -1
  78. package/dist/shared/affiliate/index.js +0 -13
  79. package/dist/shared/affiliate/index.js.map +0 -1
  80. package/dist/shared/affiliate/routes.d.ts +0 -87
  81. package/dist/shared/affiliate/routes.d.ts.map +0 -1
  82. package/dist/shared/affiliate/routes.js +0 -404
  83. package/dist/shared/affiliate/routes.js.map +0 -1
  84. package/dist/shared/affiliate/types.d.ts +0 -170
  85. package/dist/shared/affiliate/types.d.ts.map +0 -1
  86. package/dist/shared/affiliate/types.js +0 -11
  87. package/dist/shared/affiliate/types.js.map +0 -1
  88. package/dist/shared/billing/BillingService.d.ts +0 -56
  89. package/dist/shared/billing/BillingService.d.ts.map +0 -1
  90. package/dist/shared/billing/BillingService.js +0 -588
  91. package/dist/shared/billing/BillingService.js.map +0 -1
  92. package/dist/shared/billing/SeatBillingService.d.ts +0 -106
  93. package/dist/shared/billing/SeatBillingService.d.ts.map +0 -1
  94. package/dist/shared/billing/SeatBillingService.js +0 -292
  95. package/dist/shared/billing/SeatBillingService.js.map +0 -1
  96. package/dist/shared/billing/index.d.ts +0 -30
  97. package/dist/shared/billing/index.d.ts.map +0 -1
  98. package/dist/shared/billing/index.js +0 -27
  99. package/dist/shared/billing/index.js.map +0 -1
  100. package/dist/shared/billing/routes.d.ts +0 -45
  101. package/dist/shared/billing/routes.d.ts.map +0 -1
  102. package/dist/shared/billing/routes.js +0 -184
  103. package/dist/shared/billing/routes.js.map +0 -1
  104. package/dist/shared/billing/seat-pricing.d.ts +0 -53
  105. package/dist/shared/billing/seat-pricing.d.ts.map +0 -1
  106. package/dist/shared/billing/seat-pricing.js +0 -81
  107. package/dist/shared/billing/seat-pricing.js.map +0 -1
  108. package/dist/shared/billing/types.d.ts +0 -109
  109. package/dist/shared/billing/types.d.ts.map +0 -1
  110. package/dist/shared/billing/types.js +0 -8
  111. package/dist/shared/billing/types.js.map +0 -1
  112. package/dist/shared/email/EmailService.d.ts +0 -64
  113. package/dist/shared/email/EmailService.d.ts.map +0 -1
  114. package/dist/shared/email/EmailService.js +0 -300
  115. package/dist/shared/email/EmailService.js.map +0 -1
  116. package/dist/shared/email/adminRoutes.d.ts +0 -30
  117. package/dist/shared/email/adminRoutes.d.ts.map +0 -1
  118. package/dist/shared/email/adminRoutes.js +0 -227
  119. package/dist/shared/email/adminRoutes.js.map +0 -1
  120. package/dist/shared/email/gmail.d.ts +0 -208
  121. package/dist/shared/email/gmail.d.ts.map +0 -1
  122. package/dist/shared/email/gmail.js +0 -626
  123. package/dist/shared/email/gmail.js.map +0 -1
  124. package/dist/shared/email/index.d.ts +0 -15
  125. package/dist/shared/email/index.d.ts.map +0 -1
  126. package/dist/shared/email/index.js +0 -18
  127. package/dist/shared/email/index.js.map +0 -1
  128. package/dist/shared/email/mailgun.d.ts +0 -18
  129. package/dist/shared/email/mailgun.d.ts.map +0 -1
  130. package/dist/shared/email/mailgun.js +0 -76
  131. package/dist/shared/email/mailgun.js.map +0 -1
  132. package/dist/shared/email/sanitize.d.ts +0 -25
  133. package/dist/shared/email/sanitize.d.ts.map +0 -1
  134. package/dist/shared/email/sanitize.js +0 -39
  135. package/dist/shared/email/sanitize.js.map +0 -1
  136. package/dist/shared/email/smtp.d.ts +0 -20
  137. package/dist/shared/email/smtp.d.ts.map +0 -1
  138. package/dist/shared/email/smtp.js +0 -53
  139. package/dist/shared/email/smtp.js.map +0 -1
  140. package/dist/shared/email/types.d.ts +0 -113
  141. package/dist/shared/email/types.d.ts.map +0 -1
  142. package/dist/shared/email/types.js +0 -7
  143. package/dist/shared/email/types.js.map +0 -1
  144. package/dist/shared/email/webhookRoutes.d.ts +0 -29
  145. package/dist/shared/email/webhookRoutes.d.ts.map +0 -1
  146. package/dist/shared/email/webhookRoutes.js +0 -125
  147. package/dist/shared/email/webhookRoutes.js.map +0 -1
  148. package/dist/shared/mcp/index.d.ts +0 -51
  149. package/dist/shared/mcp/index.d.ts.map +0 -1
  150. package/dist/shared/mcp/index.js +0 -51
  151. package/dist/shared/mcp/index.js.map +0 -1
  152. package/dist/shared/mcp/mcp-auth-routes.d.ts +0 -26
  153. package/dist/shared/mcp/mcp-auth-routes.d.ts.map +0 -1
  154. package/dist/shared/mcp/mcp-auth-routes.js +0 -141
  155. package/dist/shared/mcp/mcp-auth-routes.js.map +0 -1
  156. package/dist/shared/mcp/mcp-db.d.ts +0 -99
  157. package/dist/shared/mcp/mcp-db.d.ts.map +0 -1
  158. package/dist/shared/mcp/mcp-db.js +0 -106
  159. package/dist/shared/mcp/mcp-db.js.map +0 -1
  160. package/dist/shared/mcp/mcp-routes.d.ts +0 -29
  161. package/dist/shared/mcp/mcp-routes.d.ts.map +0 -1
  162. package/dist/shared/mcp/mcp-routes.js +0 -171
  163. package/dist/shared/mcp/mcp-routes.js.map +0 -1
  164. package/dist/shared/mcp/mcp-tokens-routes.d.ts +0 -35
  165. package/dist/shared/mcp/mcp-tokens-routes.d.ts.map +0 -1
  166. package/dist/shared/mcp/mcp-tokens-routes.js +0 -94
  167. package/dist/shared/mcp/mcp-tokens-routes.js.map +0 -1
  168. package/dist/shared/mcp/mcp-usage-routes.d.ts +0 -17
  169. package/dist/shared/mcp/mcp-usage-routes.d.ts.map +0 -1
  170. package/dist/shared/mcp/mcp-usage-routes.js +0 -81
  171. package/dist/shared/mcp/mcp-usage-routes.js.map +0 -1
  172. package/dist/shared/mcp/tenant-context.d.ts +0 -59
  173. package/dist/shared/mcp/tenant-context.d.ts.map +0 -1
  174. package/dist/shared/mcp/tenant-context.js +0 -136
  175. package/dist/shared/mcp/tenant-context.js.map +0 -1
  176. package/dist/shared/mcp/types.d.ts +0 -74
  177. package/dist/shared/mcp/types.d.ts.map +0 -1
  178. package/dist/shared/mcp/types.js +0 -7
  179. package/dist/shared/mcp/types.js.map +0 -1
  180. package/dist/shared/planner/GoogleCalendarService.d.ts +0 -137
  181. package/dist/shared/planner/GoogleCalendarService.d.ts.map +0 -1
  182. package/dist/shared/planner/GoogleCalendarService.js +0 -525
  183. package/dist/shared/planner/GoogleCalendarService.js.map +0 -1
  184. package/dist/shared/planner/PlannerService.d.ts +0 -264
  185. package/dist/shared/planner/PlannerService.d.ts.map +0 -1
  186. package/dist/shared/planner/PlannerService.js +0 -1393
  187. package/dist/shared/planner/PlannerService.js.map +0 -1
  188. package/dist/shared/planner/index.d.ts +0 -37
  189. package/dist/shared/planner/index.d.ts.map +0 -1
  190. package/dist/shared/planner/index.js +0 -35
  191. package/dist/shared/planner/index.js.map +0 -1
  192. package/dist/shared/planner/intervals.d.ts +0 -60
  193. package/dist/shared/planner/intervals.d.ts.map +0 -1
  194. package/dist/shared/planner/intervals.js +0 -141
  195. package/dist/shared/planner/intervals.js.map +0 -1
  196. package/dist/shared/planner/routes.d.ts +0 -69
  197. package/dist/shared/planner/routes.d.ts.map +0 -1
  198. package/dist/shared/planner/routes.js +0 -770
  199. package/dist/shared/planner/routes.js.map +0 -1
  200. package/dist/shared/planner/types.d.ts +0 -328
  201. package/dist/shared/planner/types.d.ts.map +0 -1
  202. package/dist/shared/planner/types.js +0 -9
  203. package/dist/shared/planner/types.js.map +0 -1
  204. package/dist/shared/storage/ImageProcessingService.d.ts +0 -32
  205. package/dist/shared/storage/ImageProcessingService.d.ts.map +0 -1
  206. package/dist/shared/storage/ImageProcessingService.js +0 -127
  207. package/dist/shared/storage/ImageProcessingService.js.map +0 -1
  208. package/dist/shared/storage/StorageProxyService.d.ts +0 -47
  209. package/dist/shared/storage/StorageProxyService.d.ts.map +0 -1
  210. package/dist/shared/storage/StorageProxyService.js +0 -196
  211. package/dist/shared/storage/StorageProxyService.js.map +0 -1
  212. package/dist/shared/storage/StorageUploadService.d.ts +0 -126
  213. package/dist/shared/storage/StorageUploadService.d.ts.map +0 -1
  214. package/dist/shared/storage/StorageUploadService.js +0 -206
  215. package/dist/shared/storage/StorageUploadService.js.map +0 -1
  216. package/dist/shared/storage/creative-urls.d.ts +0 -14
  217. package/dist/shared/storage/creative-urls.d.ts.map +0 -1
  218. package/dist/shared/storage/creative-urls.js +0 -30
  219. package/dist/shared/storage/creative-urls.js.map +0 -1
  220. package/dist/shared/storage/index.d.ts +0 -28
  221. package/dist/shared/storage/index.d.ts.map +0 -1
  222. package/dist/shared/storage/index.js +0 -27
  223. package/dist/shared/storage/index.js.map +0 -1
  224. package/dist/shared/storage/routes.d.ts +0 -42
  225. package/dist/shared/storage/routes.d.ts.map +0 -1
  226. package/dist/shared/storage/routes.js +0 -160
  227. package/dist/shared/storage/routes.js.map +0 -1
  228. package/dist/shared/storage/types.d.ts +0 -53
  229. package/dist/shared/storage/types.d.ts.map +0 -1
  230. package/dist/shared/storage/types.js +0 -2
  231. package/dist/shared/storage/types.js.map +0 -1
  232. package/dist/shared/telegram/index.d.ts +0 -4
  233. package/dist/shared/telegram/index.d.ts.map +0 -1
  234. package/dist/shared/telegram/index.js +0 -3
  235. package/dist/shared/telegram/index.js.map +0 -1
  236. package/dist/shared/telegram/routes.d.ts +0 -43
  237. package/dist/shared/telegram/routes.d.ts.map +0 -1
  238. package/dist/shared/telegram/routes.js +0 -868
  239. package/dist/shared/telegram/routes.js.map +0 -1
  240. package/dist/shared/telegram/types.d.ts +0 -168
  241. package/dist/shared/telegram/types.d.ts.map +0 -1
  242. package/dist/shared/telegram/types.js +0 -7
  243. package/dist/shared/telegram/types.js.map +0 -1
  244. package/dist/shared/telegram/utils.d.ts +0 -44
  245. package/dist/shared/telegram/utils.d.ts.map +0 -1
  246. package/dist/shared/telegram/utils.js +0 -121
  247. package/dist/shared/telegram/utils.js.map +0 -1
  248. package/dist/storage.d.ts +0 -9
  249. package/dist/storage.d.ts.map +0 -1
  250. package/dist/storage.js +0 -8
  251. package/dist/storage.js.map +0 -1
  252. package/dist/telemetry.d.ts +0 -9
  253. package/dist/telemetry.d.ts.map +0 -1
  254. package/dist/telemetry.js +0 -8
  255. package/dist/telemetry.js.map +0 -1
  256. package/scripts/postinstall.js +0 -79
  257. package/src/shared/affiliate/migrations/001_create_affiliates.sql +0 -49
  258. package/src/shared/affiliate/migrations/002_create_affiliate_commissions.sql +0 -31
  259. package/src/shared/affiliate/migrations/003_create_affiliate_clicks.sql +0 -26
  260. package/src/shared/affiliate/migrations/004_create_affiliate_payments.sql +0 -34
  261. package/src/shared/affiliate/migrations/005_create_affiliate_tier_history.sql +0 -19
  262. package/src/shared/affiliate/migrations/006_create_affiliate_rpc_functions.sql +0 -209
  263. package/src/shared/affiliate/migrations/007_create_affiliate_rls_policies.sql +0 -123
  264. package/src/shared/billing/migrations/00000000000001_billing.sql +0 -114
  265. package/src/shared/email/migrations/000_create_email_logs.sql +0 -27
  266. package/src/shared/email/migrations/001_create_email_templates.sql +0 -27
  267. package/src/shared/email/migrations/002_add_rls_baseline_policies.sql +0 -37
  268. package/src/shared/email/migrations/003_create_gmail_accounts.sql +0 -82
  269. package/src/shared/email/migrations/004_add_email_logs_tracking_columns.sql +0 -15
  270. package/src/shared/mcp/migrations/001_mcp_api_tokens.sql +0 -21
  271. 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