@kieksme/csp-plugin-telegram 0.0.0-stage → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +14 -2
- package/dist/browser.d.ts +8 -0
- package/dist/browser.js +14 -0
- package/dist/chunk-CN5ESZIH.js +14 -0
- package/dist/server.d.ts +175 -0
- package/dist/server.js +558 -0
- package/package.json +50 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kieks.me
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kieksme/csp-plugin-telegram
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Eigenständiges CSP-Support-Plugin: ein Telegram-Forum-Thema je Portal-Chat, menschliche Übernahme, Antworten der zuständigen Person und manuelle Rückgabe oder Abschluss. Benötigt `chat`, PostgreSQL, einen Bot mit Themenrechten und eine private Forum-Supergruppe.
|
|
4
|
+
|
|
5
|
+
Installation nach Veröffentlichung:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm dlx @kieksme/csp-cli plugin add @kieksme/csp-plugin-telegram
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Offene Webseiten verwenden `chatSupport: { "mode": "telegram", "auth": "anonymous" }`; alternativ Entra-Login. Bot-Token und Webhook-Secret gehören ausschließlich auf den API-Host. Genau ein Supporttransport je Instanz.
|
|
12
|
+
|
|
13
|
+
[Einrichtung, Rechte, Architektur und Grenzen](https://github.com/kieksme/csp/wiki/telegram-support). Reale Telegram-Verbindung **noch ungetestet**; automatisierte Prüfungen verwenden synthetische Antworten.
|
|
14
|
+
|
|
15
|
+
Explicit human requests use the shared persistent waiting state and directed notifications, with current support membership checks. See [shared chat behaviour](../../docs/chat.md).
|
package/dist/browser.js
ADDED
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as _kieksme_csp_sdk from '@kieksme/csp-sdk';
|
|
2
|
+
import * as zod from 'zod';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
6
|
+
CSP_TELEGRAM_ENABLED: z.ZodDefault<z.ZodEnum<["true", "false"]>>;
|
|
7
|
+
CSP_TELEGRAM_DEMO: z.ZodDefault<z.ZodEnum<["true", "false"]>>;
|
|
8
|
+
CSP_DEMO: z.ZodOptional<z.ZodString>;
|
|
9
|
+
CSP_TELEGRAM_AUTH: z.ZodDefault<z.ZodEnum<["entra", "anonymous"]>>;
|
|
10
|
+
CSP_CHAT_SESSION_SECRET: z.ZodOptional<z.ZodString>;
|
|
11
|
+
CSP_CHAT_DATABASE_URL: z.ZodOptional<z.ZodString>;
|
|
12
|
+
CSP_CHAT_RETENTION_DAYS: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
CSP_ENTRA_TENANT_ID: z.ZodOptional<z.ZodString>;
|
|
14
|
+
CSP_ENTRA_API_AUDIENCE: z.ZodOptional<z.ZodString>;
|
|
15
|
+
CSP_ENTRA_API_SCOPE: z.ZodDefault<z.ZodString>;
|
|
16
|
+
CSP_ENTRA_PORTAL_CLIENT_ID: z.ZodOptional<z.ZodString>;
|
|
17
|
+
CSP_TELEGRAM_BOT_TOKEN: z.ZodOptional<z.ZodString>;
|
|
18
|
+
CSP_TELEGRAM_WEBHOOK_SECRET: z.ZodOptional<z.ZodString>;
|
|
19
|
+
CSP_TELEGRAM_CHAT_ID: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
20
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS: z.ZodOptional<z.ZodPipeline<z.ZodEffects<z.ZodString, string[], string>, z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>>;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
CSP_TELEGRAM_ENABLED: "true" | "false";
|
|
23
|
+
CSP_TELEGRAM_DEMO: "true" | "false";
|
|
24
|
+
CSP_TELEGRAM_AUTH: "entra" | "anonymous";
|
|
25
|
+
CSP_CHAT_RETENTION_DAYS: number;
|
|
26
|
+
CSP_ENTRA_API_SCOPE: string;
|
|
27
|
+
CSP_DEMO?: string | undefined;
|
|
28
|
+
CSP_CHAT_SESSION_SECRET?: string | undefined;
|
|
29
|
+
CSP_CHAT_DATABASE_URL?: string | undefined;
|
|
30
|
+
CSP_ENTRA_TENANT_ID?: string | undefined;
|
|
31
|
+
CSP_ENTRA_API_AUDIENCE?: string | undefined;
|
|
32
|
+
CSP_ENTRA_PORTAL_CLIENT_ID?: string | undefined;
|
|
33
|
+
CSP_TELEGRAM_BOT_TOKEN?: string | undefined;
|
|
34
|
+
CSP_TELEGRAM_WEBHOOK_SECRET?: string | undefined;
|
|
35
|
+
CSP_TELEGRAM_CHAT_ID?: string | undefined;
|
|
36
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS?: string[] | undefined;
|
|
37
|
+
}, {
|
|
38
|
+
CSP_TELEGRAM_ENABLED?: "true" | "false" | undefined;
|
|
39
|
+
CSP_TELEGRAM_DEMO?: "true" | "false" | undefined;
|
|
40
|
+
CSP_DEMO?: string | undefined;
|
|
41
|
+
CSP_TELEGRAM_AUTH?: "entra" | "anonymous" | undefined;
|
|
42
|
+
CSP_CHAT_SESSION_SECRET?: string | undefined;
|
|
43
|
+
CSP_CHAT_DATABASE_URL?: string | undefined;
|
|
44
|
+
CSP_CHAT_RETENTION_DAYS?: number | undefined;
|
|
45
|
+
CSP_ENTRA_TENANT_ID?: string | undefined;
|
|
46
|
+
CSP_ENTRA_API_AUDIENCE?: string | undefined;
|
|
47
|
+
CSP_ENTRA_API_SCOPE?: string | undefined;
|
|
48
|
+
CSP_ENTRA_PORTAL_CLIENT_ID?: string | undefined;
|
|
49
|
+
CSP_TELEGRAM_BOT_TOKEN?: string | undefined;
|
|
50
|
+
CSP_TELEGRAM_WEBHOOK_SECRET?: string | undefined;
|
|
51
|
+
CSP_TELEGRAM_CHAT_ID?: string | undefined;
|
|
52
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS?: string | undefined;
|
|
53
|
+
}>, {
|
|
54
|
+
CSP_TELEGRAM_ENABLED: "true" | "false";
|
|
55
|
+
CSP_TELEGRAM_DEMO: "true" | "false";
|
|
56
|
+
CSP_TELEGRAM_AUTH: "entra" | "anonymous";
|
|
57
|
+
CSP_CHAT_RETENTION_DAYS: number;
|
|
58
|
+
CSP_ENTRA_API_SCOPE: string;
|
|
59
|
+
CSP_DEMO?: string | undefined;
|
|
60
|
+
CSP_CHAT_SESSION_SECRET?: string | undefined;
|
|
61
|
+
CSP_CHAT_DATABASE_URL?: string | undefined;
|
|
62
|
+
CSP_ENTRA_TENANT_ID?: string | undefined;
|
|
63
|
+
CSP_ENTRA_API_AUDIENCE?: string | undefined;
|
|
64
|
+
CSP_ENTRA_PORTAL_CLIENT_ID?: string | undefined;
|
|
65
|
+
CSP_TELEGRAM_BOT_TOKEN?: string | undefined;
|
|
66
|
+
CSP_TELEGRAM_WEBHOOK_SECRET?: string | undefined;
|
|
67
|
+
CSP_TELEGRAM_CHAT_ID?: string | undefined;
|
|
68
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS?: string[] | undefined;
|
|
69
|
+
}, {
|
|
70
|
+
CSP_TELEGRAM_ENABLED?: "true" | "false" | undefined;
|
|
71
|
+
CSP_TELEGRAM_DEMO?: "true" | "false" | undefined;
|
|
72
|
+
CSP_DEMO?: string | undefined;
|
|
73
|
+
CSP_TELEGRAM_AUTH?: "entra" | "anonymous" | undefined;
|
|
74
|
+
CSP_CHAT_SESSION_SECRET?: string | undefined;
|
|
75
|
+
CSP_CHAT_DATABASE_URL?: string | undefined;
|
|
76
|
+
CSP_CHAT_RETENTION_DAYS?: number | undefined;
|
|
77
|
+
CSP_ENTRA_TENANT_ID?: string | undefined;
|
|
78
|
+
CSP_ENTRA_API_AUDIENCE?: string | undefined;
|
|
79
|
+
CSP_ENTRA_API_SCOPE?: string | undefined;
|
|
80
|
+
CSP_ENTRA_PORTAL_CLIENT_ID?: string | undefined;
|
|
81
|
+
CSP_TELEGRAM_BOT_TOKEN?: string | undefined;
|
|
82
|
+
CSP_TELEGRAM_WEBHOOK_SECRET?: string | undefined;
|
|
83
|
+
CSP_TELEGRAM_CHAT_ID?: string | undefined;
|
|
84
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS?: string | undefined;
|
|
85
|
+
}>;
|
|
86
|
+
|
|
87
|
+
declare const _default: {
|
|
88
|
+
id: string;
|
|
89
|
+
sdkVersion: string;
|
|
90
|
+
requires: string[];
|
|
91
|
+
configSchema: zod.ZodEffects<zod.ZodObject<{
|
|
92
|
+
CSP_TELEGRAM_ENABLED: zod.ZodDefault<zod.ZodEnum<["true", "false"]>>;
|
|
93
|
+
CSP_TELEGRAM_DEMO: zod.ZodDefault<zod.ZodEnum<["true", "false"]>>;
|
|
94
|
+
CSP_DEMO: zod.ZodOptional<zod.ZodString>;
|
|
95
|
+
CSP_TELEGRAM_AUTH: zod.ZodDefault<zod.ZodEnum<["entra", "anonymous"]>>;
|
|
96
|
+
CSP_CHAT_SESSION_SECRET: zod.ZodOptional<zod.ZodString>;
|
|
97
|
+
CSP_CHAT_DATABASE_URL: zod.ZodOptional<zod.ZodString>;
|
|
98
|
+
CSP_CHAT_RETENTION_DAYS: zod.ZodDefault<zod.ZodNumber>;
|
|
99
|
+
CSP_ENTRA_TENANT_ID: zod.ZodOptional<zod.ZodString>;
|
|
100
|
+
CSP_ENTRA_API_AUDIENCE: zod.ZodOptional<zod.ZodString>;
|
|
101
|
+
CSP_ENTRA_API_SCOPE: zod.ZodDefault<zod.ZodString>;
|
|
102
|
+
CSP_ENTRA_PORTAL_CLIENT_ID: zod.ZodOptional<zod.ZodString>;
|
|
103
|
+
CSP_TELEGRAM_BOT_TOKEN: zod.ZodOptional<zod.ZodString>;
|
|
104
|
+
CSP_TELEGRAM_WEBHOOK_SECRET: zod.ZodOptional<zod.ZodString>;
|
|
105
|
+
CSP_TELEGRAM_CHAT_ID: zod.ZodOptional<zod.ZodEffects<zod.ZodString, string, string>>;
|
|
106
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS: zod.ZodOptional<zod.ZodPipeline<zod.ZodEffects<zod.ZodString, string[], string>, zod.ZodArray<zod.ZodEffects<zod.ZodString, string, string>, "many">>>;
|
|
107
|
+
}, "strip", zod.ZodTypeAny, {
|
|
108
|
+
CSP_TELEGRAM_ENABLED: "true" | "false";
|
|
109
|
+
CSP_TELEGRAM_DEMO: "true" | "false";
|
|
110
|
+
CSP_TELEGRAM_AUTH: "entra" | "anonymous";
|
|
111
|
+
CSP_CHAT_RETENTION_DAYS: number;
|
|
112
|
+
CSP_ENTRA_API_SCOPE: string;
|
|
113
|
+
CSP_DEMO?: string | undefined;
|
|
114
|
+
CSP_CHAT_SESSION_SECRET?: string | undefined;
|
|
115
|
+
CSP_CHAT_DATABASE_URL?: string | undefined;
|
|
116
|
+
CSP_ENTRA_TENANT_ID?: string | undefined;
|
|
117
|
+
CSP_ENTRA_API_AUDIENCE?: string | undefined;
|
|
118
|
+
CSP_ENTRA_PORTAL_CLIENT_ID?: string | undefined;
|
|
119
|
+
CSP_TELEGRAM_BOT_TOKEN?: string | undefined;
|
|
120
|
+
CSP_TELEGRAM_WEBHOOK_SECRET?: string | undefined;
|
|
121
|
+
CSP_TELEGRAM_CHAT_ID?: string | undefined;
|
|
122
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS?: string[] | undefined;
|
|
123
|
+
}, {
|
|
124
|
+
CSP_TELEGRAM_ENABLED?: "true" | "false" | undefined;
|
|
125
|
+
CSP_TELEGRAM_DEMO?: "true" | "false" | undefined;
|
|
126
|
+
CSP_DEMO?: string | undefined;
|
|
127
|
+
CSP_TELEGRAM_AUTH?: "entra" | "anonymous" | undefined;
|
|
128
|
+
CSP_CHAT_SESSION_SECRET?: string | undefined;
|
|
129
|
+
CSP_CHAT_DATABASE_URL?: string | undefined;
|
|
130
|
+
CSP_CHAT_RETENTION_DAYS?: number | undefined;
|
|
131
|
+
CSP_ENTRA_TENANT_ID?: string | undefined;
|
|
132
|
+
CSP_ENTRA_API_AUDIENCE?: string | undefined;
|
|
133
|
+
CSP_ENTRA_API_SCOPE?: string | undefined;
|
|
134
|
+
CSP_ENTRA_PORTAL_CLIENT_ID?: string | undefined;
|
|
135
|
+
CSP_TELEGRAM_BOT_TOKEN?: string | undefined;
|
|
136
|
+
CSP_TELEGRAM_WEBHOOK_SECRET?: string | undefined;
|
|
137
|
+
CSP_TELEGRAM_CHAT_ID?: string | undefined;
|
|
138
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS?: string | undefined;
|
|
139
|
+
}>, {
|
|
140
|
+
CSP_TELEGRAM_ENABLED: "true" | "false";
|
|
141
|
+
CSP_TELEGRAM_DEMO: "true" | "false";
|
|
142
|
+
CSP_TELEGRAM_AUTH: "entra" | "anonymous";
|
|
143
|
+
CSP_CHAT_RETENTION_DAYS: number;
|
|
144
|
+
CSP_ENTRA_API_SCOPE: string;
|
|
145
|
+
CSP_DEMO?: string | undefined;
|
|
146
|
+
CSP_CHAT_SESSION_SECRET?: string | undefined;
|
|
147
|
+
CSP_CHAT_DATABASE_URL?: string | undefined;
|
|
148
|
+
CSP_ENTRA_TENANT_ID?: string | undefined;
|
|
149
|
+
CSP_ENTRA_API_AUDIENCE?: string | undefined;
|
|
150
|
+
CSP_ENTRA_PORTAL_CLIENT_ID?: string | undefined;
|
|
151
|
+
CSP_TELEGRAM_BOT_TOKEN?: string | undefined;
|
|
152
|
+
CSP_TELEGRAM_WEBHOOK_SECRET?: string | undefined;
|
|
153
|
+
CSP_TELEGRAM_CHAT_ID?: string | undefined;
|
|
154
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS?: string[] | undefined;
|
|
155
|
+
}, {
|
|
156
|
+
CSP_TELEGRAM_ENABLED?: "true" | "false" | undefined;
|
|
157
|
+
CSP_TELEGRAM_DEMO?: "true" | "false" | undefined;
|
|
158
|
+
CSP_DEMO?: string | undefined;
|
|
159
|
+
CSP_TELEGRAM_AUTH?: "entra" | "anonymous" | undefined;
|
|
160
|
+
CSP_CHAT_SESSION_SECRET?: string | undefined;
|
|
161
|
+
CSP_CHAT_DATABASE_URL?: string | undefined;
|
|
162
|
+
CSP_CHAT_RETENTION_DAYS?: number | undefined;
|
|
163
|
+
CSP_ENTRA_TENANT_ID?: string | undefined;
|
|
164
|
+
CSP_ENTRA_API_AUDIENCE?: string | undefined;
|
|
165
|
+
CSP_ENTRA_API_SCOPE?: string | undefined;
|
|
166
|
+
CSP_ENTRA_PORTAL_CLIENT_ID?: string | undefined;
|
|
167
|
+
CSP_TELEGRAM_BOT_TOKEN?: string | undefined;
|
|
168
|
+
CSP_TELEGRAM_WEBHOOK_SECRET?: string | undefined;
|
|
169
|
+
CSP_TELEGRAM_CHAT_ID?: string | undefined;
|
|
170
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS?: string | undefined;
|
|
171
|
+
}>;
|
|
172
|
+
setup(ctx: _kieksme_csp_sdk.ServerContext): Promise<void>;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export { configSchema, _default as default };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cspPlugin
|
|
3
|
+
} from "./chunk-CN5ESZIH.js";
|
|
4
|
+
|
|
5
|
+
// src/server.ts
|
|
6
|
+
import {
|
|
7
|
+
MemoryStore,
|
|
8
|
+
PostgresStore as PostgresStore2,
|
|
9
|
+
portalAuthenticator,
|
|
10
|
+
setupAnonymousSessions,
|
|
11
|
+
setupConversations
|
|
12
|
+
} from "@kieksme/csp-core/support";
|
|
13
|
+
|
|
14
|
+
// src/config.ts
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var userId = z.string().regex(/^[1-9][0-9]{0,15}$/).refine((v) => Number.isSafeInteger(Number(v)));
|
|
17
|
+
var configSchema = z.object({
|
|
18
|
+
CSP_TELEGRAM_ENABLED: z.enum(["true", "false"]).default("false"),
|
|
19
|
+
CSP_TELEGRAM_DEMO: z.enum(["true", "false"]).default("false"),
|
|
20
|
+
CSP_DEMO: z.string().optional(),
|
|
21
|
+
CSP_TELEGRAM_AUTH: z.enum(["entra", "anonymous"]).default("entra"),
|
|
22
|
+
CSP_CHAT_SESSION_SECRET: z.string().regex(/^[a-f0-9]{64}$/i).optional(),
|
|
23
|
+
CSP_CHAT_DATABASE_URL: z.string().url().optional(),
|
|
24
|
+
CSP_CHAT_RETENTION_DAYS: z.coerce.number().int().min(1).max(365).default(30),
|
|
25
|
+
CSP_ENTRA_TENANT_ID: z.string().uuid().optional(),
|
|
26
|
+
CSP_ENTRA_API_AUDIENCE: z.string().uuid().optional(),
|
|
27
|
+
CSP_ENTRA_API_SCOPE: z.string().min(1).default("Chat.Access"),
|
|
28
|
+
CSP_ENTRA_PORTAL_CLIENT_ID: z.string().uuid().optional(),
|
|
29
|
+
CSP_TELEGRAM_BOT_TOKEN: z.string().regex(/^[1-9][0-9]{4,15}:[A-Za-z0-9_-]{20,}$/).optional(),
|
|
30
|
+
CSP_TELEGRAM_WEBHOOK_SECRET: z.string().regex(/^[A-Za-z0-9_-]{32,256}$/).optional(),
|
|
31
|
+
CSP_TELEGRAM_CHAT_ID: z.string().regex(/^-100[0-9]{5,13}$/).refine((v) => Number.isSafeInteger(Number(v))).optional(),
|
|
32
|
+
CSP_TELEGRAM_SUPPORT_USER_IDS: z.string().transform(
|
|
33
|
+
(v) => v.split(",").map((id) => id.trim()).filter(Boolean)
|
|
34
|
+
).pipe(z.array(userId).min(1).max(100)).optional()
|
|
35
|
+
}).superRefine((c, ctx) => {
|
|
36
|
+
if (c.CSP_TELEGRAM_DEMO === "true") {
|
|
37
|
+
if (c.CSP_TELEGRAM_ENABLED === "true" || c.CSP_DEMO !== "true")
|
|
38
|
+
ctx.addIssue({
|
|
39
|
+
code: "custom",
|
|
40
|
+
message: "Support demo requires CSP_DEMO=true and CSP_TELEGRAM_ENABLED=false"
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (c.CSP_TELEGRAM_ENABLED !== "true") return;
|
|
45
|
+
for (const key of [
|
|
46
|
+
"CSP_CHAT_DATABASE_URL",
|
|
47
|
+
"CSP_TELEGRAM_BOT_TOKEN",
|
|
48
|
+
"CSP_TELEGRAM_WEBHOOK_SECRET",
|
|
49
|
+
"CSP_TELEGRAM_CHAT_ID",
|
|
50
|
+
"CSP_TELEGRAM_SUPPORT_USER_IDS"
|
|
51
|
+
]) {
|
|
52
|
+
if (!c[key])
|
|
53
|
+
ctx.addIssue({
|
|
54
|
+
code: "custom",
|
|
55
|
+
path: [key],
|
|
56
|
+
message: "Required for Telegram support"
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
for (const key of c.CSP_TELEGRAM_AUTH === "anonymous" ? ["CSP_CHAT_SESSION_SECRET"] : [
|
|
60
|
+
"CSP_ENTRA_TENANT_ID",
|
|
61
|
+
"CSP_ENTRA_API_AUDIENCE",
|
|
62
|
+
"CSP_ENTRA_PORTAL_CLIENT_ID"
|
|
63
|
+
])
|
|
64
|
+
if (!c[key])
|
|
65
|
+
ctx.addIssue({
|
|
66
|
+
code: "custom",
|
|
67
|
+
path: [key],
|
|
68
|
+
message: "Required for Telegram authentication"
|
|
69
|
+
});
|
|
70
|
+
if (!/^postgres(?:ql)?:\/\//.test(c.CSP_CHAT_DATABASE_URL ?? ""))
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: "custom",
|
|
73
|
+
path: ["CSP_CHAT_DATABASE_URL"],
|
|
74
|
+
message: "Use a PostgreSQL connection URL"
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// src/telegram.ts
|
|
79
|
+
import { timingSafeEqual } from "crypto";
|
|
80
|
+
import { z as z2 } from "zod";
|
|
81
|
+
import {
|
|
82
|
+
action,
|
|
83
|
+
supportReply,
|
|
84
|
+
ChatError
|
|
85
|
+
} from "@kieksme/csp-core/support";
|
|
86
|
+
|
|
87
|
+
// src/inbox.ts
|
|
88
|
+
import { PostgresStore } from "@kieksme/csp-core/support";
|
|
89
|
+
var PostgresInbox = class {
|
|
90
|
+
constructor(store) {
|
|
91
|
+
this.store = store;
|
|
92
|
+
}
|
|
93
|
+
store;
|
|
94
|
+
async init() {
|
|
95
|
+
await this.store.pool.query(`CREATE TABLE IF NOT EXISTS csp_telegram_inbox (
|
|
96
|
+
seq bigserial PRIMARY KEY, id text UNIQUE NOT NULL, conversation_id uuid NOT NULL REFERENCES csp_conversations(id) ON DELETE CASCADE,
|
|
97
|
+
body jsonb, attempts integer NOT NULL DEFAULT 0, next_at timestamptz NOT NULL DEFAULT now());`);
|
|
98
|
+
}
|
|
99
|
+
async put(job) {
|
|
100
|
+
await this.store.pool.query({
|
|
101
|
+
text: "INSERT INTO csp_telegram_inbox(id,conversation_id,body) VALUES ($1,$2,$3) ON CONFLICT(id) DO NOTHING",
|
|
102
|
+
values: [job.id, job.conversationId, job]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async drain(process) {
|
|
106
|
+
const rows = await this.store.pool.query(`SELECT o.* FROM csp_telegram_inbox o WHERE body IS NOT NULL AND next_at<=now()
|
|
107
|
+
AND NOT EXISTS(SELECT 1 FROM csp_telegram_inbox p WHERE p.conversation_id=o.conversation_id AND p.seq<o.seq AND p.body IS NOT NULL) ORDER BY seq LIMIT 20`);
|
|
108
|
+
for (const row of rows.rows) {
|
|
109
|
+
try {
|
|
110
|
+
await process(row.body);
|
|
111
|
+
await this.store.pool.query(
|
|
112
|
+
"UPDATE csp_telegram_inbox SET body=NULL WHERE seq=$1",
|
|
113
|
+
[row.seq]
|
|
114
|
+
);
|
|
115
|
+
} catch {
|
|
116
|
+
await this.store.pool.query(
|
|
117
|
+
"UPDATE csp_telegram_inbox SET attempts=attempts+1,next_at=now()+$2 * interval '1 second' WHERE seq=$1",
|
|
118
|
+
[row.seq, Math.min(300, 2 ** Math.min(row.attempts + 1, 8))]
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
function inboxFor(store) {
|
|
125
|
+
if (!(store instanceof PostgresStore))
|
|
126
|
+
throw new Error("Live Telegram support requires PostgreSQL");
|
|
127
|
+
return new PostgresInbox(store);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/telegram.ts
|
|
131
|
+
var integer = z2.number().int().safe();
|
|
132
|
+
var userSchema = z2.object({
|
|
133
|
+
id: integer.positive(),
|
|
134
|
+
is_bot: z2.boolean(),
|
|
135
|
+
first_name: z2.string().min(1),
|
|
136
|
+
last_name: z2.string().optional()
|
|
137
|
+
});
|
|
138
|
+
var chatSchema = z2.object({
|
|
139
|
+
id: integer,
|
|
140
|
+
type: z2.string(),
|
|
141
|
+
is_forum: z2.boolean().optional(),
|
|
142
|
+
username: z2.string().optional()
|
|
143
|
+
});
|
|
144
|
+
var messageSchema = z2.object({
|
|
145
|
+
message_id: integer.positive(),
|
|
146
|
+
date: integer.nonnegative(),
|
|
147
|
+
chat: chatSchema,
|
|
148
|
+
message_thread_id: integer.positive().optional(),
|
|
149
|
+
from: userSchema.optional(),
|
|
150
|
+
text: z2.string().max(4096).optional(),
|
|
151
|
+
sender_chat: z2.unknown().optional(),
|
|
152
|
+
forward_origin: z2.unknown().optional(),
|
|
153
|
+
via_bot: z2.unknown().optional()
|
|
154
|
+
});
|
|
155
|
+
var updateSchema = z2.object({
|
|
156
|
+
update_id: integer.nonnegative(),
|
|
157
|
+
message: messageSchema.optional(),
|
|
158
|
+
callback_query: z2.object({
|
|
159
|
+
id: z2.string().min(1).max(200),
|
|
160
|
+
from: userSchema,
|
|
161
|
+
data: z2.string().max(64).optional(),
|
|
162
|
+
message: messageSchema.optional()
|
|
163
|
+
}).optional()
|
|
164
|
+
});
|
|
165
|
+
function verifyTelegramSecret(expected, supplied) {
|
|
166
|
+
if (typeof supplied !== "string") return false;
|
|
167
|
+
const a = Buffer.from(expected), b = Buffer.from(supplied);
|
|
168
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
169
|
+
}
|
|
170
|
+
function telegramApi(config, fetcher = fetch) {
|
|
171
|
+
let blockedUntil = 0;
|
|
172
|
+
return async (method, body = {}) => {
|
|
173
|
+
if (Date.now() < blockedUntil)
|
|
174
|
+
throw new Error("Telegram rate limit active");
|
|
175
|
+
let response;
|
|
176
|
+
try {
|
|
177
|
+
response = await fetcher(
|
|
178
|
+
`https://api.telegram.org/bot${config.CSP_TELEGRAM_BOT_TOKEN}/${method}`,
|
|
179
|
+
{
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify(body),
|
|
183
|
+
signal: AbortSignal.timeout(1e4)
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
} catch {
|
|
187
|
+
throw new Error("Telegram API temporarily unavailable");
|
|
188
|
+
}
|
|
189
|
+
let data;
|
|
190
|
+
try {
|
|
191
|
+
data = await response.json();
|
|
192
|
+
} catch {
|
|
193
|
+
throw new Error("Invalid Telegram response");
|
|
194
|
+
}
|
|
195
|
+
const result = z2.object({
|
|
196
|
+
ok: z2.boolean(),
|
|
197
|
+
result: z2.unknown().optional(),
|
|
198
|
+
error_code: integer.optional(),
|
|
199
|
+
description: z2.string().optional(),
|
|
200
|
+
parameters: z2.object({ retry_after: integer.positive().optional() }).optional()
|
|
201
|
+
}).safeParse(data);
|
|
202
|
+
if (response.status === 429 || result.success && result.data.error_code === 429)
|
|
203
|
+
blockedUntil = Date.now() + Math.min(
|
|
204
|
+
86400,
|
|
205
|
+
result.success ? result.data.parameters?.retry_after ?? 60 : 60
|
|
206
|
+
) * 1e3;
|
|
207
|
+
if (method === "editMessageText" && result.success && result.data.error_code === 400 && result.data.description === "Bad Request: message is not modified")
|
|
208
|
+
return true;
|
|
209
|
+
if (!response.ok || !result.success || !result.data.ok)
|
|
210
|
+
throw new Error("Telegram API request failed");
|
|
211
|
+
return result.data.result;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function chunks(text, limit) {
|
|
215
|
+
const result = [];
|
|
216
|
+
let part = "";
|
|
217
|
+
for (const char of text) {
|
|
218
|
+
if (part.length + char.length > limit) {
|
|
219
|
+
result.push(part);
|
|
220
|
+
part = "";
|
|
221
|
+
}
|
|
222
|
+
part += char;
|
|
223
|
+
}
|
|
224
|
+
if (part) result.push(part);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
function card(c) {
|
|
228
|
+
const status = c.status === "bot" || c.status === "waiting" ? c.status === "waiting" ? "Pers\xF6nlicher Support angefragt \xB7 KI pausiert" : "Digitaler Assistent" : c.status === "closed" ? c.resolution ? "Problem gel\xF6st" : "Abgeschlossen" : `Support: ${c.support?.name}`;
|
|
229
|
+
return {
|
|
230
|
+
text: `Portal-Support
|
|
231
|
+
Portal-Nutzer: ${c.owner.name}
|
|
232
|
+
Gespr\xE4ch: ${c.id}
|
|
233
|
+
Status: ${status}`,
|
|
234
|
+
reply_markup: {
|
|
235
|
+
inline_keyboard: c.status === "closed" ? [] : [
|
|
236
|
+
(c.status === "bot" || c.status === "waiting" ? [
|
|
237
|
+
["take", "\xDCbernehmen"],
|
|
238
|
+
["resolve", "Problem gel\xF6st"]
|
|
239
|
+
] : [
|
|
240
|
+
["release", "An Bot zur\xFCckgeben"],
|
|
241
|
+
["close", "Abschlie\xDFen"],
|
|
242
|
+
["resolve", "Problem gel\xF6st"]
|
|
243
|
+
]).map(([id, text]) => ({ text, callback_data: `${id}:${c.id}` }))
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function supportAuthorizer(config, api) {
|
|
249
|
+
return async (id) => {
|
|
250
|
+
if (!config.CSP_TELEGRAM_SUPPORT_USER_IDS?.includes(id)) return;
|
|
251
|
+
const member = z2.object({
|
|
252
|
+
status: z2.string(),
|
|
253
|
+
is_member: z2.boolean().optional(),
|
|
254
|
+
user: userSchema
|
|
255
|
+
}).parse(
|
|
256
|
+
await api("getChatMember", {
|
|
257
|
+
chat_id: config.CSP_TELEGRAM_CHAT_ID,
|
|
258
|
+
user_id: Number(id)
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
if (String(member.user.id) !== id || member.user.is_bot || !(["creator", "administrator", "member"].includes(member.status) || member.status === "restricted" && member.is_member === true))
|
|
262
|
+
return;
|
|
263
|
+
return {
|
|
264
|
+
id: `telegram:${config.CSP_TELEGRAM_CHAT_ID}:${id}`,
|
|
265
|
+
name: [member.user.first_name, member.user.last_name].filter(Boolean).join(" ").slice(0, 200)
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async function createTelegramBridge(app, store, config, fetcher = fetch, onTake = () => {
|
|
270
|
+
}, inbox = inboxFor(store)) {
|
|
271
|
+
const api = telegramApi(config, fetcher), authorize = supportAuthorizer(config, api), channel = `telegram:${config.CSP_TELEGRAM_CHAT_ID}`;
|
|
272
|
+
const bot = userSchema.parse(await api("getMe"));
|
|
273
|
+
if (!bot.is_bot) throw new Error("Telegram identity must be a bot");
|
|
274
|
+
const chat = chatSchema.parse(
|
|
275
|
+
await api("getChat", { chat_id: config.CSP_TELEGRAM_CHAT_ID })
|
|
276
|
+
);
|
|
277
|
+
if (String(chat.id) !== config.CSP_TELEGRAM_CHAT_ID || chat.type !== "supergroup" || chat.username || !chat.is_forum)
|
|
278
|
+
throw new Error("Telegram support requires a forum supergroup");
|
|
279
|
+
const rights = z2.object({
|
|
280
|
+
status: z2.literal("administrator"),
|
|
281
|
+
can_manage_topics: z2.literal(true),
|
|
282
|
+
user: userSchema
|
|
283
|
+
}).safeParse(
|
|
284
|
+
await api("getChatMember", {
|
|
285
|
+
chat_id: config.CSP_TELEGRAM_CHAT_ID,
|
|
286
|
+
user_id: bot.id
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
if (!rights.success || rights.data.user.id !== bot.id)
|
|
290
|
+
throw new Error("Telegram bot requires administrator topic permissions");
|
|
291
|
+
await inbox.init();
|
|
292
|
+
let working = false, closing = false;
|
|
293
|
+
const answer = async (job, text) => {
|
|
294
|
+
if (job.callbackId)
|
|
295
|
+
await api("answerCallbackQuery", {
|
|
296
|
+
callback_query_id: job.callbackId,
|
|
297
|
+
text,
|
|
298
|
+
show_alert: true
|
|
299
|
+
}).catch(() => {
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
const process = async (job) => {
|
|
303
|
+
const person = await authorize(job.user);
|
|
304
|
+
if (!person) {
|
|
305
|
+
await answer(
|
|
306
|
+
job,
|
|
307
|
+
"Keine Supportberechtigung. Freigabe und Gruppenmitgliedschaft sind erforderlich."
|
|
308
|
+
);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const taken = await store.change(job.conversationId, (c, queue) => {
|
|
313
|
+
if (c.thread?.conversationId !== channel || c.thread.rootId !== job.rootId || c.seen.includes(job.id))
|
|
314
|
+
return false;
|
|
315
|
+
const receivedAt = job.receivedAt ?? Date.now();
|
|
316
|
+
if (c.thread.lastUpdateAt && receivedAt - c.thread.lastUpdateAt > 7 * 864e5) {
|
|
317
|
+
c.supportActionAt = -1;
|
|
318
|
+
if (c.status === "support") c.supportSince = -1;
|
|
319
|
+
}
|
|
320
|
+
c.thread.lastUpdateAt = Math.max(
|
|
321
|
+
c.thread.lastUpdateAt ?? 0,
|
|
322
|
+
receivedAt
|
|
323
|
+
);
|
|
324
|
+
if (job.action) {
|
|
325
|
+
if (job.timestamp <= (c.supportActionAt ?? -1)) return false;
|
|
326
|
+
if (!["close", "resolve"].includes(job.action) && c.messages.length >= 500)
|
|
327
|
+
throw new ChatError(
|
|
328
|
+
409,
|
|
329
|
+
"Gespr\xE4chslimit erreicht. Bitte abschlie\xDFen."
|
|
330
|
+
);
|
|
331
|
+
const transition = job.action === "take" && (c.status === "bot" || c.status === "waiting") || job.action === "resolve" && c.status !== "closed";
|
|
332
|
+
action(c, job.action, person, queue);
|
|
333
|
+
c.supportActionAt = job.timestamp;
|
|
334
|
+
if (job.action === "take" && transition)
|
|
335
|
+
c.supportSince = job.timestamp;
|
|
336
|
+
c.seen.push(job.id);
|
|
337
|
+
return transition;
|
|
338
|
+
}
|
|
339
|
+
if (job.timestamp > (c.supportSince ?? Infinity))
|
|
340
|
+
supportReply(c, person, job.id, job.text ?? "", queue);
|
|
341
|
+
return false;
|
|
342
|
+
});
|
|
343
|
+
if (taken) onTake(job.conversationId);
|
|
344
|
+
await answer(job, "Support-Aktion verarbeitet.");
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (!(error instanceof ChatError)) throw error;
|
|
347
|
+
await answer(job, error.message);
|
|
348
|
+
if (!job.callbackId && job.text)
|
|
349
|
+
await api("sendMessage", {
|
|
350
|
+
chat_id: config.CSP_TELEGRAM_CHAT_ID,
|
|
351
|
+
message_thread_id: Number(job.rootId),
|
|
352
|
+
text: `Support-Antwort nicht weitergeleitet: ${error.message}`,
|
|
353
|
+
link_preview_options: { is_disabled: true }
|
|
354
|
+
}).catch(() => {
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const worker = async () => {
|
|
359
|
+
if (working || closing) return;
|
|
360
|
+
working = true;
|
|
361
|
+
try {
|
|
362
|
+
await inbox.drain(process);
|
|
363
|
+
} catch {
|
|
364
|
+
} finally {
|
|
365
|
+
working = false;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const interval = setInterval(() => void worker(), 1e3);
|
|
369
|
+
app.post(
|
|
370
|
+
"/api/telegram/webhook",
|
|
371
|
+
{
|
|
372
|
+
bodyLimit: 131072,
|
|
373
|
+
onRequest: async (req) => {
|
|
374
|
+
if (!verifyTelegramSecret(
|
|
375
|
+
config.CSP_TELEGRAM_WEBHOOK_SECRET,
|
|
376
|
+
req.headers["x-telegram-bot-api-secret-token"]
|
|
377
|
+
))
|
|
378
|
+
throw new ChatError(401, "Ung\xFCltiges Telegram-Webhook-Secret");
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
async (req, reply) => {
|
|
382
|
+
reply.header("Cache-Control", "no-store");
|
|
383
|
+
const parsed = updateSchema.safeParse(req.body);
|
|
384
|
+
if (!parsed.success)
|
|
385
|
+
throw new ChatError(400, "Ung\xFCltiges Telegram-Update");
|
|
386
|
+
const p = parsed.data, callback = p.callback_query, m = callback?.message ?? p.message, user = callback?.from ?? m?.from;
|
|
387
|
+
if (!m || !user || user.is_bot || m.sender_chat !== void 0 || m.forward_origin !== void 0 || m.via_bot !== void 0 || !m.message_thread_id || m.chat.type !== "supergroup" || String(m.chat.id) !== config.CSP_TELEGRAM_CHAT_ID)
|
|
388
|
+
return { ok: true };
|
|
389
|
+
const c = await store.findThread(channel, String(m.message_thread_id));
|
|
390
|
+
if (!c) return { ok: true };
|
|
391
|
+
let command;
|
|
392
|
+
if (callback) {
|
|
393
|
+
const match = /^(take|release|close|resolve):([a-f0-9-]{36})$/.exec(
|
|
394
|
+
callback.data ?? ""
|
|
395
|
+
);
|
|
396
|
+
if (!match || match[2] !== c.id || String(m.message_id) !== c.thread?.serviceUrl && String(m.message_id) !== c.supportRequest?.messageId || m.from?.id !== bot.id)
|
|
397
|
+
return { ok: true };
|
|
398
|
+
command = match[1];
|
|
399
|
+
} else if (!m.text?.trim()) return { ok: true };
|
|
400
|
+
await inbox.put({
|
|
401
|
+
id: `update:${p.update_id}`,
|
|
402
|
+
conversationId: c.id,
|
|
403
|
+
rootId: String(m.message_thread_id),
|
|
404
|
+
user: String(user.id),
|
|
405
|
+
timestamp: p.update_id,
|
|
406
|
+
receivedAt: Date.now(),
|
|
407
|
+
action: command,
|
|
408
|
+
text: callback ? void 0 : m.text,
|
|
409
|
+
callbackId: callback?.id
|
|
410
|
+
});
|
|
411
|
+
void worker();
|
|
412
|
+
return { ok: true };
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
app.addHook("preClose", async () => {
|
|
416
|
+
closing = true;
|
|
417
|
+
clearInterval(interval);
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
async send(job, c) {
|
|
421
|
+
if (job.kind === "create" && !c.thread) {
|
|
422
|
+
const topic = z2.object({ message_thread_id: integer.positive() }).parse(
|
|
423
|
+
await api("createForumTopic", {
|
|
424
|
+
chat_id: config.CSP_TELEGRAM_CHAT_ID,
|
|
425
|
+
name: chunks(`Support ${c.owner.name}`, 128)[0]
|
|
426
|
+
})
|
|
427
|
+
);
|
|
428
|
+
await store.change(c.id, (current) => {
|
|
429
|
+
current.thread = {
|
|
430
|
+
conversationId: channel,
|
|
431
|
+
rootId: String(topic.message_thread_id)
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
c = await store.get(c.id);
|
|
435
|
+
}
|
|
436
|
+
if (!c.thread || c.thread.conversationId !== channel)
|
|
437
|
+
throw new Error("Telegram topic missing");
|
|
438
|
+
const base = {
|
|
439
|
+
chat_id: config.CSP_TELEGRAM_CHAT_ID,
|
|
440
|
+
message_thread_id: Number(c.thread.rootId)
|
|
441
|
+
};
|
|
442
|
+
if (job.kind === "request") {
|
|
443
|
+
if (c.status !== "waiting" || c.supportRequest?.id !== job.id || c.supportRequest.messageId)
|
|
444
|
+
return;
|
|
445
|
+
const targets = [];
|
|
446
|
+
for (const id of job.targets ?? []) {
|
|
447
|
+
const person = await authorize(id);
|
|
448
|
+
if (person) targets.push({ id, name: person.name });
|
|
449
|
+
}
|
|
450
|
+
const names = targets.map((p) => p.name).join(", ");
|
|
451
|
+
const text = names + (names ? ": " : "") + "Pers\xF6nlicher Support angefragt. Bitte \xFCbernehmen.";
|
|
452
|
+
let offset = 0;
|
|
453
|
+
const entities = targets.map((p) => {
|
|
454
|
+
const entity = {
|
|
455
|
+
type: "text_mention",
|
|
456
|
+
offset,
|
|
457
|
+
length: p.name.length,
|
|
458
|
+
user: { id: Number(p.id) }
|
|
459
|
+
};
|
|
460
|
+
offset += p.name.length + 2;
|
|
461
|
+
return entity;
|
|
462
|
+
});
|
|
463
|
+
const result = z2.object({ message_id: integer.positive() }).parse(
|
|
464
|
+
await api("sendMessage", {
|
|
465
|
+
...base,
|
|
466
|
+
text,
|
|
467
|
+
entities,
|
|
468
|
+
reply_markup: card(c).reply_markup,
|
|
469
|
+
link_preview_options: { is_disabled: true }
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
await store.change(c.id, (current) => {
|
|
473
|
+
if (current.supportRequest?.id === job.id)
|
|
474
|
+
current.supportRequest.messageId = String(result.message_id);
|
|
475
|
+
});
|
|
476
|
+
} else if (job.kind === "create" || job.kind === "card") {
|
|
477
|
+
if (c.thread.serviceUrl) {
|
|
478
|
+
await api("editMessageText", {
|
|
479
|
+
chat_id: base.chat_id,
|
|
480
|
+
message_id: Number(c.thread.serviceUrl),
|
|
481
|
+
...card(c)
|
|
482
|
+
});
|
|
483
|
+
} else {
|
|
484
|
+
const result = z2.object({ message_id: integer.positive() }).parse(await api("sendMessage", { ...base, ...card(c) }));
|
|
485
|
+
await store.change(c.id, (current) => {
|
|
486
|
+
current.thread.serviceUrl = String(result.message_id);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
} else if (job.message) {
|
|
490
|
+
const m = job.message, text = `${m.kind === "user" ? c.owner.name : m.kind === "bot" ? "Digitaler Assistent" : m.name ?? "Support"}
|
|
491
|
+
${m.content}`;
|
|
492
|
+
for (const part of chunks(text, 3500))
|
|
493
|
+
await api("sendMessage", {
|
|
494
|
+
...base,
|
|
495
|
+
text: part,
|
|
496
|
+
link_preview_options: { is_disabled: true }
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
async close() {
|
|
501
|
+
closing = true;
|
|
502
|
+
clearInterval(interval);
|
|
503
|
+
while (working) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
504
|
+
},
|
|
505
|
+
drain: worker
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/server.ts
|
|
510
|
+
var server_default = {
|
|
511
|
+
id: "telegram",
|
|
512
|
+
sdkVersion: cspPlugin.sdkVersion,
|
|
513
|
+
requires: ["chat"],
|
|
514
|
+
configSchema,
|
|
515
|
+
async setup(ctx) {
|
|
516
|
+
const c = configSchema.parse(ctx.env), demo = c.CSP_TELEGRAM_DEMO === "true";
|
|
517
|
+
if (c.CSP_TELEGRAM_ENABLED !== "true" && !demo) {
|
|
518
|
+
if (ctx.config.chatSupport)
|
|
519
|
+
throw new Error(
|
|
520
|
+
"Profile requires support, but runtime support is disabled"
|
|
521
|
+
);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (ctx.config.chatSupport?.mode !== (demo ? "demo" : "telegram"))
|
|
525
|
+
throw new Error("Frontend and runtime support modes differ");
|
|
526
|
+
const anonymous = c.CSP_TELEGRAM_AUTH === "anonymous";
|
|
527
|
+
if (!demo && anonymous !== (ctx.config.chatSupport.auth === "anonymous"))
|
|
528
|
+
throw new Error("Frontend and runtime authentication modes differ");
|
|
529
|
+
if (!demo && !anonymous && (ctx.config.chatSupport.tenantId !== c.CSP_ENTRA_TENANT_ID || ctx.config.chatSupport.clientId !== c.CSP_ENTRA_PORTAL_CLIENT_ID || ctx.config.chatSupport.scope !== `api://${c.CSP_ENTRA_API_AUDIENCE}/${c.CSP_ENTRA_API_SCOPE}`))
|
|
530
|
+
throw new Error("Entra frontend and API configuration differ");
|
|
531
|
+
const store = demo ? new MemoryStore() : new PostgresStore2(c.CSP_CHAT_DATABASE_URL);
|
|
532
|
+
const authenticate = demo ? async () => ({
|
|
533
|
+
tenantId: "demo",
|
|
534
|
+
userId: "demo-user",
|
|
535
|
+
name: "Portal-Nutzer Demo"
|
|
536
|
+
}) : anonymous ? setupAnonymousSessions(ctx, c.CSP_CHAT_SESSION_SECRET) : portalAuthenticator(
|
|
537
|
+
c.CSP_ENTRA_TENANT_ID,
|
|
538
|
+
c.CSP_ENTRA_API_AUDIENCE,
|
|
539
|
+
c.CSP_ENTRA_API_SCOPE
|
|
540
|
+
);
|
|
541
|
+
try {
|
|
542
|
+
await setupConversations(
|
|
543
|
+
ctx,
|
|
544
|
+
store,
|
|
545
|
+
authenticate,
|
|
546
|
+
demo,
|
|
547
|
+
(onTake) => createTelegramBridge(ctx.app, store, c, ctx.fetch, onTake)
|
|
548
|
+
);
|
|
549
|
+
} catch (e) {
|
|
550
|
+
await store.close();
|
|
551
|
+
throw e;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
export {
|
|
556
|
+
configSchema,
|
|
557
|
+
server_default as default
|
|
558
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kieksme/csp-plugin-telegram",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
"./browser": {
|
|
12
|
+
"types": "./dist/browser.d.ts",
|
|
13
|
+
"import": "./dist/browser.js"
|
|
14
|
+
},
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./dist/server.d.ts",
|
|
17
|
+
"import": "./dist/server.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@kieksme/csp-sdk": "^0.12.0",
|
|
26
|
+
"@kieksme/csp-core": "^0.12.0",
|
|
27
|
+
"zod": "^3.25.76",
|
|
28
|
+
"fastify": "^5.6.1"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.12"
|
|
32
|
+
},
|
|
33
|
+
"cspPlugin": {
|
|
34
|
+
"id": "telegram",
|
|
35
|
+
"sdkVersion": "^0.12.0",
|
|
36
|
+
"browser": "./browser",
|
|
37
|
+
"server": "./server",
|
|
38
|
+
"requires": [
|
|
39
|
+
"chat"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/kieksme/csp.git",
|
|
45
|
+
"directory": "packages/plugin-telegram"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/kieksme/csp/issues"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup src/browser.tsx src/server.ts --format esm --dts --clean --external @kieksme/csp-sdk --external react --external react-dom"
|
|
52
|
+
}
|
|
6
53
|
}
|