feat: Telegram config panel - Alpine store

keyboardstaff committed Mar 20, 2026 at 10:23 UTC 0504a90a355b813914419676f2f5680056669a23
2 files changed +418
plugins/_telegram_integration/webui/config.html new
+311
@@ -0,0 +1,311 @@
1 +<html>
2 +<head>
3 + <title>Telegram Integration</title>
4 + <script type="module">
5 + import { store } from "/plugins/_telegram_integration/webui/telegram-config-store.js";
6 + </script>
7 +</head>
8 +
9 +<body>
10 + <div x-data x-init="$store.telegramConfig.init()">
11 + <template x-if="config">
12 + <div class="tg-page">
13 + <div class="section-title">Telegram Integration</div>
14 + <div class="section-description">
15 + Configure Telegram bots to communicate with Agent Zero via Telegram.
16 + Each bot connects to a Telegram Bot API token and handles messages from allowed users.
17 + </div>
18 +
19 + <!-- Bot cards -->
20 + <template x-for="(bot, idx) in (config.bots || [])" :key="idx">
21 + <div class="bot-card">
22 + <div class="bot-card-header" @click="$store.telegramConfig.toggle(idx)">
23 + <span class="material-symbols-outlined bot-expand-icon"
24 + :class="{ 'expanded': $store.telegramConfig.expandedIdx === idx }">chevron_right</span>
25 + <span class="bot-card-name" x-text="bot.name || '(unnamed)'"></span>
26 + <span class="bot-card-summary" x-show="$store.telegramConfig.expandedIdx !== idx"
27 + x-text="bot.enabled ? 'Enabled' : 'Disabled'"></span>
28 + <button class="text-button bot-delete-btn"
29 + @click.stop="$confirmClick($event, () => $store.telegramConfig.removeBot(config, idx))"
30 + title="Remove bot">
31 + <span class="material-symbols-outlined">close</span>
32 + </button>
33 + </div>
34 +
35 + <div class="bot-card-body" x-show="$store.telegramConfig.expandedIdx === idx" x-transition.opacity>
36 +
37 + <div class="field">
38 + <div class="field-label">
39 + <div class="field-title">Enabled</div>
40 + <div class="field-description">Enable or disable this Telegram bot</div>
41 + </div>
42 + <div class="field-control">
43 + <label class="toggle">
44 + <input type="checkbox" x-model="bot.enabled" />
45 + <span class="toggler"></span>
46 + </label>
47 + </div>
48 + </div>
49 +
50 + <div class="field">
51 + <div class="field-label">
52 + <div class="field-title">Bot Name</div>
53 + <div class="field-description">Unique identifier for this bot configuration</div>
54 + </div>
55 + <div class="field-control">
56 + <input type="text" x-model="bot.name" placeholder="e.g. my_bot" />
57 + </div>
58 + </div>
59 +
60 + <div class="field">
61 + <div class="field-label">
62 + <div class="field-title">Bot Token</div>
63 + <div class="field-description">Token from @BotFather on Telegram</div>
64 + </div>
65 + <div class="field-control">
66 + <input type="password" x-model="bot.token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" />
67 + </div>
68 + </div>
69 +
70 + <div class="field">
71 + <div class="field-label">
72 + <div class="field-title">Mode</div>
73 + <div class="field-description">Polling: no public IP needed. Webhook: requires HTTPS endpoint</div>
74 + </div>
75 + <div class="field-control">
76 + <select x-model="bot.mode">
77 + <option value="polling">Polling</option>
78 + <option value="webhook">Webhook</option>
79 + </select>
80 + </div>
81 + </div>
82 +
83 + <div class="field" x-show="bot.mode === 'webhook'">
84 + <div class="field-label">
85 + <div class="field-title">Webhook URL</div>
86 + <div class="field-description">Public HTTPS URL for Telegram to send updates to</div>
87 + </div>
88 + <div class="field-control">
89 + <input type="text" x-model="bot.webhook_url" placeholder="https://yourdomain.com/telegram/my_bot" />
90 + </div>
91 + </div>
92 +
93 + <div class="field" x-show="bot.mode === 'webhook'">
94 + <div class="field-label">
95 + <div class="field-title">Webhook Secret</div>
96 + <div class="field-description">Optional shared secret for webhook verification</div>
97 + </div>
98 + <div class="field-control">
99 + <input type="password" x-model="bot.webhook_secret" />
100 + </div>
101 + </div>
102 +
103 + <div class="field">
104 + <div class="field-label">
105 + <div class="field-title">Allowed Users</div>
106 + <div class="field-description">Comma-separated Telegram user IDs or @usernames. Empty = allow all</div>
107 + </div>
108 + <div class="field-control">
109 + <input type="text"
110 + :value="$store.telegramConfig.whitelistText(bot)"
111 + @input="$store.telegramConfig.setWhitelist(bot, $event.target.value)"
112 + placeholder="123456789, @username" />
113 + </div>
114 + </div>
115 +
116 + <div class="field">
117 + <div class="field-label">
118 + <div class="field-title">Group Mode</div>
119 + <div class="field-description">How the bot responds in group chats</div>
120 + </div>
121 + <div class="field-control">
122 + <select x-model="bot.group_mode">
123 + <option value="mention">Respond when @mentioned or replied to</option>
124 + <option value="all">Respond to every message</option>
125 + <option value="off">Ignore group messages</option>
126 + </select>
127 + </div>
128 + </div>
129 +
130 + <div class="field">
131 + <div class="field-label">
132 + <div class="field-title">User → Project Mapping</div>
133 + <div class="field-description">Map user IDs to projects. Format: user_id=project_name, comma-separated</div>
134 + </div>
135 + <div class="field-control">
136 + <input type="text"
137 + :value="$store.telegramConfig.userProjectsText(bot)"
138 + @input="$store.telegramConfig.setUserProjects(bot, $event.target.value)"
139 + placeholder="123456=my_project, 789012=other_project" />
140 + </div>
141 + </div>
142 +
143 + <div class="field">
144 + <div class="field-label">
145 + <div class="field-title">Default Project</div>
146 + <div class="field-description">Fallback project if user not in the mapping above</div>
147 + </div>
148 + <div class="field-control">
149 + <select x-model="bot.default_project">
150 + <option value="">No project</option>
151 + <template x-for="proj in $store.telegramConfig.projects" :key="proj.name">
152 + <option :value="proj.name" x-text="proj.title || proj.name"></option>
153 + </template>
154 + </select>
155 + </div>
156 + </div>
157 +
158 + <div class="field">
159 + <div class="field-label">
160 + <div class="field-title">Attachment Max Age</div>
161 + <div class="field-description">Auto-delete downloaded attachments older than this many hours. 0 = keep forever</div>
162 + </div>
163 + <div class="field-control">
164 + <input type="number" x-model.number="bot.attachment_max_age_hours" min="0" step="1" placeholder="0" />
165 + </div>
166 + </div>
167 +
168 + <div class="field">
169 + <div class="field-label">
170 + <div class="field-title">Agent Instructions</div>
171 + <div class="field-description">Extra instructions for the agent in Telegram chats</div>
172 + </div>
173 + <div class="field-control">
174 + <textarea x-model="bot.agent_instructions" rows="3" placeholder="e.g. Always respond concisely for mobile..."></textarea>
175 + </div>
176 + </div>
177 +
178 + <!-- Test connection + results -->
179 + <div class="bot-test-row">
180 + <button class="btn btn-field"
181 + @click.stop="$store.telegramConfig.testConnection(config, idx)"
182 + :disabled="$store.telegramConfig.testing === idx">
183 + <span x-show="$store.telegramConfig.testing !== idx">Test Connection</span>
184 + <span x-show="$store.telegramConfig.testing === idx">Testing...</span>
185 + </button>
186 + <template x-if="$store.telegramConfig.testResults && $store.telegramConfig.expandedIdx === idx">
187 + <div class="bot-test-results">
188 + <template x-for="r in $store.telegramConfig.testResults.results" :key="r.test">
189 + <div class="bot-test-result-row">
190 + <span class="bot-test-icon"
191 + x-text="r.ok ? '✓' : '✗'"
192 + :class="r.ok ? 'ok' : 'fail'"></span>
193 + <span class="bot-test-label" x-text="r.test"></span>
194 + <span class="bot-test-msg" x-text="r.message"></span>
195 + </div>
196 + </template>
197 + </div>
198 + </template>
199 + </div>
200 +
201 + </div>
202 + </div>
203 + </template>
204 +
205 + <button class="text-button bot-add-btn" @click="$store.telegramConfig.addBot(config)">
206 + <span class="material-symbols-outlined">add</span>
207 + <span>Add Bot</span>
208 + </button>
209 + </div>
210 + </template>
211 + </div>
212 +
213 + <style>
214 + .tg-page {
215 + display: flex;
216 + flex-direction: column;
217 + gap: 8px;
218 + }
219 + .bot-card {
220 + border: 1px solid var(--color-border);
221 + border-radius: 6px;
222 + overflow: hidden;
223 + }
224 + .bot-card-header {
225 + display: flex;
226 + align-items: center;
227 + gap: 6px;
228 + padding: 8px 10px;
229 + cursor: pointer;
230 + font-size: 0.85rem;
231 + }
232 + .bot-card-header:hover {
233 + background: var(--color-background-hover, rgba(255,255,255,0.04));
234 + }
235 + .bot-expand-icon {
236 + font-size: 16px;
237 + transition: transform 0.15s ease;
238 + }
239 + .bot-expand-icon.expanded {
240 + transform: rotate(90deg);
241 + }
242 + .bot-card-name {
243 + font-weight: 500;
244 + }
245 + .bot-card-summary {
246 + flex: 1;
247 + text-align: right;
248 + opacity: 0.5;
249 + font-size: 0.75rem;
250 + overflow: hidden;
251 + text-overflow: ellipsis;
252 + white-space: nowrap;
253 + }
254 + .bot-delete-btn {
255 + margin-left: auto;
256 + opacity: 0.5;
257 + padding: 2px !important;
258 + }
259 + .bot-delete-btn:hover {
260 + opacity: 1;
261 + color: var(--color-error, #f44) !important;
262 + }
263 + .bot-delete-btn .material-symbols-outlined {
264 + font-size: 16px;
265 + }
266 + .bot-card-body {
267 + padding: 4px 12px 12px;
268 + border-top: 1px solid var(--color-border);
269 + }
270 + .bot-add-btn {
271 + margin-top: 4px;
272 + }
273 + .bot-test-row {
274 + margin-top: 12px;
275 + display: flex;
276 + align-items: flex-start;
277 + gap: 12px;
278 + flex-wrap: wrap;
279 + }
280 + .bot-test-results {
281 + padding: 4px 0;
282 + font-size: 0.85rem;
283 + flex: 1;
284 + min-width: 200px;
285 + }
286 + .bot-test-result-row {
287 + display: flex;
288 + align-items: center;
289 + gap: 8px;
290 + padding: 4px 0;
291 + }
292 + .bot-test-icon {
293 + font-weight: bold;
294 + }
295 + .bot-test-icon.ok {
296 + color: #4caf50;
297 + }
298 + .bot-test-icon.fail {
299 + color: #f44336;
300 + }
301 + .bot-test-label {
302 + font-weight: 500;
303 + min-width: 80px;
304 + }
305 + .bot-test-msg {
306 + opacity: 0.8;
307 + }
308 + </style>
309 +</body>
310 +
311 +</html>
plugins/_telegram_integration/webui/telegram-config-store.js new
+107
@@ -0,0 +1,107 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +const API_BASE = "/plugins/_telegram_integration";
4 +
5 +export const store = createStore("telegramConfig", {
6 + projects: [],
7 + expandedIdx: null,
8 + testing: null,
9 + testResults: null,
10 + _loaded: false,
11 +
12 + async init() {
13 + if (this._loaded) return;
14 + try {
15 + const { callJsonApi } = await import("/js/api.js");
16 + const res = await callJsonApi("projects", { action: "list" });
17 + this.projects = res.data || [];
18 + } catch (_) {
19 + this.projects = [];
20 + }
21 + this._loaded = true;
22 + },
23 +
24 + defaultBot() {
25 + return {
26 + name: "",
27 + enabled: false,
28 + token: "",
29 + mode: "polling",
30 + webhook_url: "",
31 + webhook_secret: "",
32 + allowed_users: [],
33 + group_mode: "mention",
34 + user_projects: {},
35 + default_project: "",
36 + agent_instructions: "",
37 + attachment_max_age_hours: 0,
38 + };
39 + },
40 +
41 + addBot(config) {
42 + if (!config.bots) config.bots = [];
43 + const bot = this.defaultBot();
44 + bot.name = "bot_" + (config.bots.length + 1);
45 + config.bots.push(bot);
46 + this.expandedIdx = config.bots.length - 1;
47 + },
48 +
49 + removeBot(config, idx) {
50 + config.bots.splice(idx, 1);
51 + this.expandedIdx = null;
52 + },
53 +
54 + toggle(idx) {
55 + this.expandedIdx = this.expandedIdx === idx ? null : idx;
56 + this.testResults = null;
57 + },
58 +
59 + whitelistText(bot) {
60 + return (bot.allowed_users || []).join(", ");
61 + },
62 +
63 + setWhitelist(bot, val) {
64 + bot.allowed_users = val
65 + .split(",")
66 + .map((s) => s.trim())
67 + .filter((s) => s);
68 + },
69 +
70 + userProjectsText(bot) {
71 + const up = bot.user_projects || {};
72 + return Object.entries(up)
73 + .map(([k, v]) => k + "=" + v)
74 + .join(", ");
75 + },
76 +
77 + setUserProjects(bot, val) {
78 + const obj = {};
79 + val
80 + .split(",")
81 + .map((s) => s.trim())
82 + .filter((s) => s)
83 + .forEach((item) => {
84 + const [k, v] = item.split("=").map((p) => p.trim());
85 + if (k && v) obj[k] = v;
86 + });
87 + bot.user_projects = obj;
88 + },
89 +
90 + async testConnection(config, idx) {
91 + this.testing = idx;
92 + this.testResults = null;
93 + try {
94 + const { callJsonApi } = await import("/js/api.js");
95 + const res = await callJsonApi(`${API_BASE}/test_connection`, {
96 + bot: config.bots[idx],
97 + });
98 + this.testResults = res;
99 + } catch (e) {
100 + this.testResults = {
101 + success: false,
102 + results: [{ test: "Connection", ok: false, message: String(e) }],
103 + };
104 + }
105 + this.testing = null;
106 + },
107 +});