main
js 347 lines 9.57 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import * as API from "/js/api.js";
3
4 const API_BASE = "/plugins/_telegram_integration";
5 const STEPS = [
6 {
7 title: "Connect your bot",
8 description: "Start with BotFather, then paste the bot token here.",
9 },
10 {
11 title: "Choose who can use it",
12 description: "Finish the core setup, choose access, and decide how messages arrive.",
13 },
14 {
15 title: "Shape the conversation",
16 description: "Choose how the bot behaves in groups and how the agent should reply.",
17 },
18 ];
19
20 const BOTFATHER_QR =
21 "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0AQAAAAB84SuKAAAA50lEQVR4nMWVQWoFMQxDnz+zl2/w73+suYF8Aheni98ux1BqglEgQjOx7ETzM+r1awt/v4+ILCAHLPjqJivLAx7zLwjh7Dxg+T/pKj84/4nr5FHPgxb6bRLjAY/50QE6sED3Uz79HZrr7/ZzPiM/X2B7w/cw263uFZ+2sOb6rMf8iyZNNiqt6lfFG1fembHxzxszKQ1a9a8SO26STf9RU8MUqUX/0DLSmULSpn4T6Kx+zn+d+cMdJrWYP4zvxjy91SeSt6mKjf51cinGgOznsVP2rn7dksaEU8uF/yPAGvsu/B///H59AYlVhAI4J5PTAAAAAElFTkSuQmCC";
22
23 function ensureConfig(config) {
24 if (!config || typeof config !== "object") return;
25 if (!Array.isArray(config.bots)) config.bots = [];
26 }
27
28 export const store = createStore("telegramConfig", {
29 config: null,
30 projects: [],
31 editing: null,
32 testing: null,
33 testResults: null,
34 testResultsFor: null,
35 botSteps: [],
36 didInit: false,
37 steps: STEPS,
38 botFatherQr: BOTFATHER_QR,
39 _projectsLoaded: false,
40 context: null,
41
42 get bots() {
43 ensureConfig(this.config);
44 return Array.isArray(this.config?.bots) ? this.config.bots : [];
45 },
46
47 get activeIndex() {
48 return typeof this.editing === "number" ? this.editing : -1;
49 },
50
51 get activeBot() {
52 return this.activeIndex >= 0 ? this.bots[this.activeIndex] || null : null;
53 },
54
55 get showFooterNav() {
56 return this.activeIndex >= 0;
57 },
58
59 get currentStep() {
60 return this.activeIndex >= 0 && typeof this.botSteps[this.activeIndex] === "number"
61 ? this.botSteps[this.activeIndex]
62 : 0;
63 },
64
65 get isFirstStep() {
66 return this.currentStep === 0;
67 },
68
69 get isLastStep() {
70 return this.currentStep >= this.steps.length - 1;
71 },
72
73 get nextDisabled() {
74 return !!this.stepBlockedReason();
75 },
76
77 get nextButtonLabel() {
78 return this.isLastStep ? "Done" : "Next";
79 },
80
81 get footerStepLabel() {
82 return `Step ${this.currentStep + 1} of ${this.steps.length}`;
83 },
84
85 async init(config, context = null) {
86 this.config = config || null;
87 this.context = context;
88 this.didInit = false;
89 ensureConfig(this.config);
90 this.editing = this.bots.length === 1 ? 0 : null;
91 this.testing = null;
92 this.testResults = null;
93 this.testResultsFor = null;
94 this.botSteps = this.bots.map((bot) => this.initialStepForBot(bot));
95 if (this.bots.length === 0) this._startInitialBotFlow();
96 this._installWizardFooter();
97 this.didInit = true;
98
99 if (this._projectsLoaded) return;
100 try {
101 const response = await API.callJsonApi("projects", { action: "list" });
102 this.projects = response.data || [];
103 } catch (_) {
104 this.projects = [];
105 }
106 this._projectsLoaded = true;
107 },
108
109 cleanup() {
110 if (this.context?.wizardFooter?.owner === "telegramConfig") {
111 this.context.wizardFooter = null;
112 }
113 this.config = null;
114 this.context = null;
115 this.editing = null;
116 this.testing = null;
117 this.testResults = null;
118 this.testResultsFor = null;
119 this.botSteps = [];
120 this.didInit = false;
121 },
122
123 defaultBot() {
124 return {
125 name: "",
126 enabled: false,
127 notify_messages: false,
128 token: "",
129 mode: "polling",
130 webhook_url: "",
131 webhook_secret: "",
132 allowed_users: [],
133 group_mode: "mention",
134 welcome_enabled: false,
135 welcome_message: "",
136 user_projects: {},
137 default_project: "",
138 agent_instructions: "",
139 attachment_max_age_hours: 0,
140 };
141 },
142
143 addBot() {
144 ensureConfig(this.config);
145 this.config.bots.push(this.defaultBot());
146 this.botSteps.push(0);
147 this.editing = this.config.bots.length - 1;
148 this.testResults = null;
149 this.testResultsFor = null;
150 },
151
152 removeBot(idx) {
153 this.bots.splice(idx, 1);
154 this.botSteps.splice(idx, 1);
155 if (this.editing === idx) this.editing = null;
156 if (this.editing !== null && this.editing > idx) this.editing -= 1;
157 if (this.testResultsFor === idx) {
158 this.testResults = null;
159 this.testResultsFor = null;
160 }
161 },
162
163 toggleEditing(idx) {
164 this.editing = this.editing === idx ? null : idx;
165 if (this.editing !== null && typeof this.botSteps[this.editing] !== "number") {
166 this.botSteps[this.editing] = this.initialStepForBot(this.bots[this.editing]);
167 }
168 if (this.testResultsFor !== idx) {
169 this.testResults = null;
170 this.testResultsFor = null;
171 }
172 },
173
174 currentStepMeta() {
175 return this.steps[this.currentStep] || this.steps[0];
176 },
177
178 setStep(step) {
179 if (this.activeIndex < 0) return;
180 const next = Math.max(0, Math.min(this.steps.length - 1, Number(step) || 0));
181 this.botSteps[this.activeIndex] = next;
182 },
183
184 previousStep() {
185 if (this.isFirstStep) return;
186 this.setStep(this.currentStep - 1);
187 },
188
189 nextStep() {
190 if (this.stepBlockedReason()) return;
191 if (this.isLastStep) {
192 this.editing = null;
193 return;
194 }
195 this.setStep(this.currentStep + 1);
196 },
197
198 stepBlockedReason() {
199 const bot = this.activeBot;
200 if (!bot) return "";
201 if (this.currentStep === 0 && !String(bot.token || "").trim()) {
202 return "Add your bot token first.";
203 }
204 if (this.currentStep === 1 && bot.mode === "webhook" && !String(bot.webhook_url || "").trim()) {
205 return "Add your webhook URL first.";
206 }
207 return "";
208 },
209
210 canTest(bot) {
211 if (!bot) return false;
212 if (!String(bot.token || "").trim()) return false;
213 if (bot.mode === "webhook" && !String(bot.webhook_url || "").trim()) return false;
214 return true;
215 },
216
217 botStatusLabel(bot) {
218 if (!String(bot?.token || "").trim()) return "New";
219 if (bot?.mode === "webhook" && !String(bot?.webhook_url || "").trim()) return "Needs URL";
220 if (bot?.enabled && this.canTest(bot)) return "Live";
221 if (this.canTest(bot)) return "Ready";
222 return "Needs info";
223 },
224
225 botStatusTone(bot) {
226 const label = this.botStatusLabel(bot);
227 if (label === "Live") return "success";
228 if (label === "Ready") return "ready";
229 if (label === "New") return "muted";
230 return "warning";
231 },
232
233 botTitle(bot, idx) {
234 return String(bot?.name || "").trim() || `Bot ${idx + 1}`;
235 },
236
237 botSubtitle(bot) {
238 const pieces = [bot.mode === "webhook" ? "Webhook" : "Polling"];
239 pieces.push(Array.isArray(bot.allowed_users) && bot.allowed_users.length > 0 ? "Private access" : "Open access");
240 if (bot.default_project) pieces.push(`Project: ${bot.default_project}`);
241 return pieces.join(" · ");
242 },
243
244 whitelistText(bot) {
245 return (bot.allowed_users || []).join(", ");
246 },
247
248 setWhitelist(bot, value) {
249 bot.allowed_users = value
250 .split(",")
251 .map((item) => item.trim())
252 .filter((item) => item);
253 },
254
255 userProjectsText(bot) {
256 return Object.entries(bot.user_projects || {})
257 .map(([userId, project]) => `${userId}=${project}`)
258 .join(", ");
259 },
260
261 setUserProjects(bot, value) {
262 const mapping = {};
263 value
264 .split(",")
265 .map((item) => item.trim())
266 .filter((item) => item)
267 .forEach((item) => {
268 const [userId, project] = item.split("=").map((part) => part.trim());
269 if (userId) mapping[userId] = project || "";
270 });
271 bot.user_projects = mapping;
272 },
273
274 accessWarning(bot) {
275 if (!bot?.enabled) return "";
276 if (Array.isArray(bot.allowed_users) && bot.allowed_users.length > 0) return "";
277 return "Allowed users is empty. Anyone who finds this bot can reach your Agent Zero.";
278 },
279
280 async testConnection(idx) {
281 const bot = this.bots[idx];
282 if (!this.canTest(bot)) return;
283
284 this.testing = idx;
285 this.testResults = null;
286 this.testResultsFor = idx;
287
288 try {
289 this.testResults = await API.callJsonApi(`${API_BASE}/test_connection`, { bot });
290 } catch (error) {
291 this.testResults = {
292 success: false,
293 results: [{ test: "Telegram bot", ok: false, message: String(error) }],
294 };
295 }
296
297 this.testing = null;
298 },
299
300 testButtonLabel(bot, idx) {
301 if (this.testing === idx) return "Checking...";
302 if (this.canTest(bot)) return "Check Telegram connection";
303 return "Fill in the basics first";
304 },
305
306 testIntro() {
307 return "We will validate the bot token with Telegram so you know this bot can connect.";
308 },
309
310 resultTitle(result) {
311 return result.test || "Check";
312 },
313
314 resultMessage(result) {
315 return result.message || (result.ok ? "Done." : "Something went wrong.");
316 },
317
318 initialStepForBot(bot) {
319 return String(bot?.token || "").trim() ? 1 : 0;
320 },
321
322 _startInitialBotFlow() {
323 this.addBot();
324 if (!this.context) return;
325 const toComparableJson = typeof this.context._toComparableJson === "function"
326 ? this.context._toComparableJson.bind(this.context)
327 : JSON.stringify;
328 this.context.settingsSnapshotJson = toComparableJson(this.context.settings);
329 },
330
331 _installWizardFooter() {
332 if (!this.context) return;
333 this.context.wizardFooter = {
334 owner: "telegramConfig",
335 visible: () => this.showFooterNav,
336 canGoBack: () => !this.isFirstStep,
337 backLabel: () => "Back",
338 note: () => this.footerStepLabel,
339 showNext: () => this.showFooterNav && !this.isLastStep,
340 nextLabel: () => this.nextButtonLabel,
341 nextDisabled: () => this.nextDisabled,
342 showSave: () => !this.showFooterNav || this.isLastStep,
343 onBack: () => this.previousStep(),
344 onNext: () => this.nextStep(),
345 };
346 },
347 });