| 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 | }); |