package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) const telegramPage = ` Hermes on Telegram
← Back to Hermes

Hermes on Telegram

Link this Keycloak account to a private Telegram chat. Messages will use the same isolated Hermes tenant as the WebUI.

Checking Telegram…

Codes expire after 10 minutes. Only direct messages are accepted; group messages are ignored.

` const bridgeCSS = ` #hermes-telegram-shortcut{position:fixed;right:18px;bottom:18px;z-index:9999;padding:10px 14px;border-radius:999px;background:#229ed9;color:#fff;text-decoration:none;font:600 14px system-ui,sans-serif;box-shadow:0 5px 20px #0005} .hermes-link-page{margin:0;min-height:100vh;display:grid;place-items:center;background:#0f172a;color:#e2e8f0;font:16px/1.5 system-ui,sans-serif} .hermes-link-card{width:min(620px,calc(100% - 40px));box-sizing:border-box;padding:32px;border:1px solid #334155;border-radius:18px;background:#111827;box-shadow:0 20px 60px #0006} .hermes-link-card h1{margin:.6rem 0}.hermes-back{color:#7dd3fc}.hermes-link-actions{display:flex;gap:12px;flex-wrap:wrap;margin:24px 0} .hermes-link-card button{border:0;border-radius:10px;padding:11px 16px;background:#229ed9;color:#fff;font-weight:700;cursor:pointer}.hermes-link-card button.secondary{background:#334155} #telegram-result{padding:16px;border-radius:10px;background:#1e293b;overflow-wrap:anywhere}#telegram-result a{color:#7dd3fc}.hermes-fine-print{color:#94a3b8;font-size:13px} ` const bridgeJS = `(() => { const page = document.querySelector('[data-telegram-page]'); if (!page) { if (!document.getElementById('hermes-telegram-shortcut')) { const link = document.createElement('a'); link.id = 'hermes-telegram-shortcut'; link.href = '/telegram'; link.textContent = 'Telegram'; link.setAttribute('aria-label', 'Connect Hermes to Telegram'); document.body.appendChild(link); } return; } const status = document.getElementById('telegram-status'); const result = document.getElementById('telegram-result'); const linkButton = document.getElementById('telegram-link'); const unlinkButton = document.getElementById('telegram-unlink'); const action = async (path) => { const response = await fetch(path, {method:'POST',headers:{'Content-Type':'application/json','X-Hermes-Action':'telegram-link'},body:'{}'}); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || 'Request failed'); return payload; }; const refresh = async () => { try { const response = await fetch('/api/telegram/status', {cache:'no-store'}); const payload = await response.json(); if (!payload.configured) { status.textContent = 'Telegram is prepared, but the bot token has not been added by the operator yet.'; linkButton.disabled = true; unlinkButton.hidden = true; return; } status.textContent = payload.linked ? 'Telegram is linked to this private account.' : 'Telegram is ready to link.'; unlinkButton.hidden = !payload.linked; } catch (_) { status.textContent = 'Telegram status is temporarily unavailable.'; } }; linkButton.addEventListener('click', async () => { try { const payload = await action('/api/telegram/link'); result.hidden = false; result.replaceChildren(); const text = document.createElement('p'); text.textContent = 'Send /link ' + payload.code + ' to the Hermes bot. This code expires at ' + new Date(payload.expires_at).toLocaleTimeString() + '.'; result.appendChild(text); if (payload.deep_link) { const anchor = document.createElement('a'); anchor.href = payload.deep_link; anchor.rel = 'noopener noreferrer'; anchor.textContent = 'Open Telegram and link now'; result.appendChild(anchor); } } catch (error) { status.textContent = error.message; } }); unlinkButton.addEventListener('click', async () => { try { await action('/api/telegram/unlink'); result.hidden = true; await refresh(); } catch (error) { status.textContent = error.message; } }); refresh(); })();` func writeJSON(writer http.ResponseWriter, status int, value any) { writer.Header().Set("Content-Type", "application/json") writer.Header().Set("Cache-Control", "no-store") writer.WriteHeader(status) _ = json.NewEncoder(writer).Encode(value) } func validTelegramAction(request *http.Request) bool { return request.Header.Get("X-Hermes-Action") == "telegram-link" && strings.HasPrefix(request.Header.Get("Content-Type"), "application/json") } func (router *tenantRouter) serveTelegramWeb(writer http.ResponseWriter, request *http.Request, subject string) bool { switch request.URL.Path { case "/hermes-chat-bridge.css": if request.Method != http.MethodGet { http.Error(writer, "method not allowed", http.StatusMethodNotAllowed) return true } writer.Header().Set("Content-Type", "text/css; charset=utf-8") writer.Header().Set("Cache-Control", "public, max-age=3600") _, _ = io.WriteString(writer, bridgeCSS) return true case "/hermes-chat-bridge.js": if request.Method != http.MethodGet { http.Error(writer, "method not allowed", http.StatusMethodNotAllowed) return true } writer.Header().Set("Content-Type", "application/javascript; charset=utf-8") writer.Header().Set("Cache-Control", "public, max-age=3600") _, _ = io.WriteString(writer, bridgeJS) return true case "/telegram": if request.Method != http.MethodGet { http.Error(writer, "method not allowed", http.StatusMethodNotAllowed) return true } writer.Header().Set("Content-Type", "text/html; charset=utf-8") writer.Header().Set("Cache-Control", "no-store") writer.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'self'") _, _ = io.WriteString(writer, telegramPage) return true case "/api/telegram/status": if request.Method != http.MethodGet { writeJSON(writer, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) return true } linked, err := router.telegramLinked(subject) if err != nil { writeJSON(writer, http.StatusServiceUnavailable, map[string]string{"error": err.Error()}) return true } username := "" if router.telegram != nil { username = router.telegram.username() } writeJSON(writer, http.StatusOK, map[string]any{ "configured": router.telegram != nil, "linked": linked, "bot_username": username, }) return true case "/api/telegram/link": if request.Method != http.MethodPost || !validTelegramAction(request) { writeJSON(writer, http.StatusForbidden, map[string]string{"error": "same-origin action required"}) return true } if router.telegram == nil { writeJSON(writer, http.StatusServiceUnavailable, map[string]string{"error": "Telegram bot token is not configured"}) return true } code, expires, err := router.createLink(subject) if err != nil { writeJSON(writer, http.StatusServiceUnavailable, map[string]string{"error": err.Error()}) return true } username := router.telegram.username() deepLink := "" if username != "" { deepLink = fmt.Sprintf("https://t.me/%s?start=%s", url.PathEscape(username), url.QueryEscape(code)) } writeJSON(writer, http.StatusOK, map[string]any{ "code": code, "expires_at": expires.Format(time.RFC3339), "deep_link": deepLink, }) return true case "/api/telegram/unlink": if request.Method != http.MethodPost || !validTelegramAction(request) { writeJSON(writer, http.StatusForbidden, map[string]string{"error": "same-origin action required"}) return true } if err := router.unlinkTelegram(subject); err != nil { writeJSON(writer, http.StatusServiceUnavailable, map[string]string{"error": err.Error()}) return true } writeJSON(writer, http.StatusOK, map[string]bool{"unlinked": true}) return true default: return false } } func injectChatBridge(response *http.Response) error { if !strings.Contains(response.Header.Get("Content-Type"), "text/html") { return nil } body, err := io.ReadAll(response.Body) if err != nil { return err } _ = response.Body.Close() content := string(body) if !strings.Contains(content, "hermes-chat-bridge.js") { content = strings.Replace(content, "", ``, 1) content = strings.Replace(content, "", ``, 1) } response.Body = io.NopCloser(strings.NewReader(content)) response.ContentLength = int64(len(content)) response.Header.Set("Content-Length", strconv.Itoa(len(content))) response.Header.Set("Cache-Control", "no-store") response.Header.Del("ETag") return nil }