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