const sessionStatusEl = document.querySelector("#session-status"); const sessionUserEl = document.querySelector("#session-user"); const loginFeedbackEl = document.querySelector("#login-feedback"); const sessionSummaryTextEl = document.querySelector("#session-summary-text"); const sessionDetailsEl = document.querySelector("#session-details"); const toggleSessionBtn = document.querySelector("#toggle-session"); const startLoginForm = document.querySelector("#start-login-form"); const verifyLoginForm = document.querySelector("#verify-login-form"); const logoutButton = document.querySelector("#logout-button"); const hooksCountEl = document.querySelector("#hooks-count"); const hooksListEl = document.querySelector("#hooks-list"); const hookTemplate = document.querySelector("#hook-template"); const createHookForm = document.querySelector("#create-hook-form"); const toggleCreateBtn = document.querySelector("#toggle-create"); const recentChatsBtn = document.querySelector("#recent-chats-btn"); const recentChatsModal = document.querySelector("#recent-chats-modal"); const recentChatsListEl = document.querySelector("#recent-chats-list"); const closeRecentChatsBtn = document.querySelector("#close-recent-chats"); const recentChatsSearchEl = document.querySelector("#recent-chats-search"); let sessionDetailsVisible = false; let sessionDetailsTouched = false; let createFormVisible = false; let createFormTouched = false; let lastFocusedElement = null; let recentChatsData = []; if (recentChatsSearchEl) { recentChatsSearchEl.disabled = true; recentChatsSearchEl.addEventListener("input", () => { applyRecentChatsFilter(); }); recentChatsSearchEl.addEventListener("keydown", (event) => { if (event.key === "Escape" && recentChatsSearchEl.value) { event.preventDefault(); recentChatsSearchEl.value = ""; applyRecentChatsFilter(); } }); } function setSessionDetailsVisibility(show, { fromUser = false } = {}) { if (!toggleSessionBtn || !sessionDetailsEl) return; sessionDetailsVisible = show; if (fromUser) { sessionDetailsTouched = true; } sessionDetailsEl.classList.toggle("hidden", !show); toggleSessionBtn.setAttribute("aria-expanded", String(show)); toggleSessionBtn.textContent = show ? "Hide session controls" : "Manage session"; } function setCreateFormVisibility(show, { fromUser = false } = {}) { if (!toggleCreateBtn) return; createFormVisible = show; if (fromUser) { createFormTouched = true; } createHookForm.classList.toggle("hidden", !show); toggleCreateBtn.setAttribute("aria-expanded", String(show)); toggleCreateBtn.textContent = show ? "Hide form" : "New Hook"; if (show) { const chatInput = document.querySelector("#hook-chat-id"); if (chatInput) { chatInput.focus(); } } } if (toggleCreateBtn) { toggleCreateBtn.addEventListener("click", () => { setCreateFormVisibility(!createFormVisible, { fromUser: true }); }); setCreateFormVisibility(false); } if (toggleSessionBtn) { toggleSessionBtn.addEventListener("click", () => { setSessionDetailsVisibility(!sessionDetailsVisible, { fromUser: true }); }); setSessionDetailsVisibility(false); } async function fetchJSON(url, options = {}) { const response = await fetch(url, { headers: { "Content-Type": "application/json" }, ...options, }); if (!response.ok) { const contentType = response.headers.get("content-type") || ""; let message = "Request failed"; if (contentType.includes("application/json")) { try { const payload = await response.json(); if (payload && typeof payload === "object") { message = payload.detail || JSON.stringify(payload); } } catch { message = await response.text(); } } else { const text = await response.text(); if (text) { message = text; } } throw new Error(message || "Request failed"); } if (response.status === 204) { return null; } return response.json(); } function updateSessionUI(status) { if (status.authorized) { sessionStatusEl.textContent = "Authorized"; sessionStatusEl.style.background = "rgba(100, 221, 155, 0.12)"; sessionStatusEl.style.color = "#64dd9b"; if (sessionSummaryTextEl) { sessionSummaryTextEl.textContent = status.user ? `Logged in as ${status.user}` : "Session ready"; } sessionUserEl.textContent = `Logged in as ${status.user}`; loginFeedbackEl.textContent = "Session ready. You can trigger hooks."; startLoginForm.classList.add("hidden"); verifyLoginForm.classList.add("hidden"); logoutButton.classList.remove("hidden"); } else { sessionStatusEl.textContent = status.code_sent ? "Awaiting code" : "Not authorized"; sessionStatusEl.style.background = "rgba(79, 140, 255, 0.15)"; sessionStatusEl.style.color = "#4f8cff"; if (sessionSummaryTextEl) { sessionSummaryTextEl.textContent = status.code_sent ? "Waiting for verification" : "Login required"; } sessionUserEl.textContent = status.phone_number ? `Phone number: ${status.phone_number}` : "Set a phone number to begin"; logoutButton.classList.add("hidden"); if (status.code_sent) { verifyLoginForm.classList.remove("hidden"); loginFeedbackEl.textContent = "Code sent. Check your Telegram messages."; } else { verifyLoginForm.classList.add("hidden"); loginFeedbackEl.textContent = "Start by sending a login code to your phone."; } startLoginForm.classList.remove("hidden"); } const shouldShowDetails = !status.authorized || status.code_sent; if (!sessionDetailsTouched) { setSessionDetailsVisibility(shouldShowDetails); } else if (shouldShowDetails && !sessionDetailsVisible) { setSessionDetailsVisibility(true); } setRecentChatsAvailability(status.authorized); if (!status.authorized) { closeRecentChatsDialog(); } } async function refreshAll() { try { const status = await fetchJSON("/api/status"); updateSessionUI(status); } catch (error) { loginFeedbackEl.textContent = `Failed to refresh status: ${error.message}`; } await loadHooks(); } function setRecentChatsAvailability(isAuthorized) { if (!recentChatsBtn) return; recentChatsBtn.disabled = !isAuthorized; recentChatsBtn.title = isAuthorized ? "Browse your recent Telegram chats" : "Authorize the session to view recent chats"; if (!isAuthorized && recentChatsSearchEl) { recentChatsSearchEl.value = ""; recentChatsSearchEl.disabled = true; } } function applyRecentChatsFilter() { if (!recentChatsListEl) return; const query = recentChatsSearchEl ? recentChatsSearchEl.value.trim().toLowerCase() : ""; const source = Array.isArray(recentChatsData) ? recentChatsData : []; const filtered = !query ? source : source.filter((chat) => { const parts = [ chat.display_name, chat.chat_id, chat.username ? `@${chat.username}` : null, chat.phone_number, chat.chat_type, ] .filter(Boolean) .map((part) => String(part).toLowerCase()); return parts.some((part) => part.includes(query)); }); renderRecentChats(filtered); } async function openRecentChatsDialog() { if (!recentChatsModal || !recentChatsListEl) return; if (recentChatsModal.classList.contains("hidden") && document.activeElement instanceof HTMLElement) { lastFocusedElement = document.activeElement; } recentChatsModal.classList.remove("hidden"); recentChatsModal.scrollTop = 0; recentChatsListEl.innerHTML = ""; recentChatsData = []; if (recentChatsSearchEl) { recentChatsSearchEl.value = ""; recentChatsSearchEl.disabled = true; } const loading = document.createElement("p"); loading.className = "feedback"; loading.textContent = "Loading recent chats…"; recentChatsListEl.appendChild(loading); try { const chats = await fetchJSON("/api/recent-chats"); recentChatsData = Array.isArray(chats) ? chats : []; if (recentChatsSearchEl) { recentChatsSearchEl.disabled = false; recentChatsSearchEl.focus(); } applyRecentChatsFilter(); } catch (error) { recentChatsListEl.innerHTML = ""; const message = document.createElement("p"); message.className = "feedback"; const text = typeof error?.message === "string" ? error.message : "Unable to load recent chats."; message.textContent = text.includes("Session not authorized") ? "Authorize the session to view recent chats." : `Unable to load recent chats: ${text}`; recentChatsListEl.appendChild(message); recentChatsData = []; if (recentChatsSearchEl) { recentChatsSearchEl.disabled = true; } } document.addEventListener("keydown", handleRecentChatsKeydown); if (recentChatsModal && (!recentChatsSearchEl || recentChatsSearchEl.disabled)) { const focusTarget = recentChatsModal.querySelector("button, [href], input, textarea, [tabindex]:not([tabindex='-1'])"); if (focusTarget instanceof HTMLElement) { focusTarget.focus(); } } } function renderRecentChats(chats) { if (!recentChatsListEl) return; recentChatsListEl.innerHTML = ""; if (!Array.isArray(chats) || !chats.length) { const empty = document.createElement("p"); empty.className = "feedback"; empty.textContent = "No recent chats available. Start a conversation in Telegram to see it here."; recentChatsListEl.appendChild(empty); return; } chats.forEach((chat) => { const item = document.createElement("div"); item.className = "recent-chat-item"; const details = document.createElement("div"); details.className = "recent-chat-details"; const nameEl = document.createElement("span"); nameEl.className = "recent-chat-name"; nameEl.textContent = chat.display_name || chat.chat_id; const metaEl = document.createElement("span"); metaEl.className = "recent-chat-meta"; const metaParts = []; if (chat.chat_type) { metaParts.push(String(chat.chat_type).toUpperCase()); } if (chat.last_used_at) { metaParts.push(`Last activity ${new Date(chat.last_used_at).toLocaleString()}`); } metaEl.textContent = metaParts.length ? metaParts.join(" • ") : "Unknown chat"; const feedbackEl = document.createElement("span"); feedbackEl.className = "recent-chat-feedback"; let feedbackTimeout; const setFeedback = (message, isError = false) => { feedbackEl.textContent = message; feedbackEl.style.color = isError ? "#ffbac7" : message ? "#64dd9b" : ""; if (feedbackTimeout) { clearTimeout(feedbackTimeout); feedbackTimeout = undefined; } if (message) { feedbackTimeout = window.setTimeout(() => { feedbackEl.textContent = ""; feedbackEl.style.color = ""; feedbackTimeout = undefined; }, 2400); } }; details.appendChild(nameEl); details.appendChild(metaEl); details.appendChild(feedbackEl); item.appendChild(details); const appendRow = (labelText, displayValue, copyValue, options = {}) => { const { required = false, valueClasses = [] } = options; const rawValue = displayValue ?? ""; const valueString = rawValue !== null && rawValue !== undefined ? String(rawValue) : ""; const hasValue = valueString.trim() !== ""; if (!required && !hasValue) { return; } const row = document.createElement("div"); row.className = "recent-chat-row"; const labelEl = document.createElement("span"); labelEl.className = "recent-chat-label"; labelEl.textContent = labelText; const actionsEl = document.createElement("div"); actionsEl.className = "recent-chat-actions"; const valueButton = document.createElement("button"); valueButton.type = "button"; valueButton.className = "recent-chat-value-button"; if (Array.isArray(valueClasses)) { valueClasses.forEach((cls) => valueButton.classList.add(cls)); } else if (typeof valueClasses === "string" && valueClasses) { valueButton.classList.add(valueClasses); } valueButton.setAttribute("aria-label", `Copy ${labelText.toLowerCase()} for ${nameEl.textContent}`); const valueTextEl = document.createElement("span"); valueTextEl.className = "recent-chat-value"; valueTextEl.textContent = hasValue ? valueString : "—"; valueButton.appendChild(valueTextEl); const iconEl = document.createElement("span"); iconEl.className = "recent-chat-copy-icon"; iconEl.innerHTML = ` `; valueButton.appendChild(iconEl); const canCopy = hasValue && copyValue !== null && copyValue !== undefined && copyValue !== ""; if (canCopy) { valueButton.addEventListener("click", async () => { try { await navigator.clipboard.writeText(String(copyValue)); setFeedback(`${labelText} copied to clipboard.`); } catch (err) { setFeedback(`Copy failed: ${err.message}`, true); } }); } else { valueButton.disabled = true; valueButton.setAttribute("aria-disabled", "true"); } actionsEl.appendChild(valueButton); row.appendChild(labelEl); row.appendChild(actionsEl); item.appendChild(row); }; appendRow( "Chat ID", typeof chat.chat_id === "number" || typeof chat.chat_id === "bigint" ? String(chat.chat_id) : chat.chat_id, chat.chat_id, { required: true, valueClasses: ["mono"] }, ); appendRow( "Username", chat.username ? `@${chat.username}` : "", chat.username ? `@${chat.username}` : "", {}, ); appendRow( "Phone", chat.phone_number ? String(chat.phone_number) : "", chat.phone_number ? String(chat.phone_number) : "", {}, ); recentChatsListEl.appendChild(item); }); } function closeRecentChatsDialog() { if (!recentChatsModal || recentChatsModal.classList.contains("hidden")) return; recentChatsModal.classList.add("hidden"); document.removeEventListener("keydown", handleRecentChatsKeydown); if (lastFocusedElement && typeof lastFocusedElement.focus === "function") { lastFocusedElement.focus(); } } function handleRecentChatsKeydown(event) { if (event.key === "Escape") { event.preventDefault(); closeRecentChatsDialog(); } } if (recentChatsBtn) { recentChatsBtn.addEventListener("click", () => { openRecentChatsDialog(); }); } if (closeRecentChatsBtn) { closeRecentChatsBtn.addEventListener("click", () => { closeRecentChatsDialog(); }); } if (recentChatsModal) { recentChatsModal.addEventListener("click", (event) => { if (event.target === recentChatsModal) { closeRecentChatsDialog(); } }); } async function loadHooks() { try { const hooks = await fetchJSON("/api/hooks"); hooksCountEl.textContent = hooks.length; hooksListEl.innerHTML = ""; if (!hooks.length) { if (!createFormTouched) { setCreateFormVisibility(true); } const empty = document.createElement("p"); empty.className = "feedback"; empty.textContent = "No hooks yet. Use the New Hook button above to create one."; hooksListEl.appendChild(empty); return; } hooks.forEach((hook) => { const node = hookTemplate.content.cloneNode(true); node.querySelector("h3").textContent = hook.chat_id; node.querySelector(".hook-date").textContent = new Date(hook.created_at).toLocaleString(); const lastRunEl = node.querySelector(".hook-last-run"); if (lastRunEl) { lastRunEl.textContent = hook.last_triggered_at ? `Last triggered ${new Date(hook.last_triggered_at).toLocaleString()}` : "Never triggered yet"; } node.querySelector(".hook-message").textContent = hook.message; node.querySelector(".hook-url").textContent = hook.action_url; node.querySelector(".hook-id").textContent = hook.hook_id; const feedbackEl = node.querySelector(".hook-feedback"); const editForm = node.querySelector(".edit-hook-form"); const editIdInput = editForm.querySelector(".edit-id"); const editChatInput = editForm.querySelector(".edit-chat"); const editMessageInput = editForm.querySelector(".edit-message"); const saveEditBtn = editForm.querySelector(".save-edit"); const cancelEditBtn = editForm.querySelector(".cancel-edit"); const editDetailsBtn = node.querySelector(".edit-details"); const setFeedback = (text = "", color = "") => { feedbackEl.textContent = text; feedbackEl.style.color = color; }; const toggleEditForm = (show) => { editForm.classList.toggle("hidden", !show); editDetailsBtn.disabled = show; if (show) { editIdInput.value = hook.hook_id; editChatInput.value = hook.chat_id; editMessageInput.value = hook.message; requestAnimationFrame(() => { editIdInput.focus(); }); } }; editDetailsBtn.addEventListener("click", () => { toggleEditForm(true); }); cancelEditBtn.addEventListener("click", () => { toggleEditForm(false); setFeedback(); }); const copyBtn = node.querySelector(".copy"); copyBtn.addEventListener("click", async () => { try { await navigator.clipboard.writeText(hook.action_url); setFeedback("Hook URL copied to clipboard.", "#64dd9b"); setTimeout(() => setFeedback(), 2000); } catch (err) { setFeedback(`Copy failed: ${err.message}`, "#ffbac7"); setTimeout(() => setFeedback(), 2500); } }); const triggerBtn = node.querySelector(".trigger"); triggerBtn.addEventListener("click", async () => { triggerBtn.disabled = true; setFeedback("Sending message…"); try { const result = await fetchJSON(`/action/${hook.hook_id}`); setFeedback(`Status: ${result.status}`, "#64dd9b"); } catch (err) { setFeedback(`Failed: ${err.message}`, "#ffbac7"); } finally { setTimeout(() => { triggerBtn.disabled = false; setFeedback(); }, 2500); } }); editForm.addEventListener("submit", async (event) => { event.preventDefault(); const updatedId = editIdInput.value.trim(); const updatedChat = editChatInput.value.trim(); const updatedMessage = editMessageInput.value.trim(); if (!updatedId || !updatedChat || !updatedMessage) { setFeedback("Hook ID, chat ID, and message are required.", "#ffbac7"); return; } saveEditBtn.disabled = true; cancelEditBtn.disabled = true; const originalSaveText = saveEditBtn.textContent; saveEditBtn.textContent = "Saving…"; setFeedback(); try { await fetchJSON(`/api/hooks/${hook.hook_id}`, { method: "PATCH", body: JSON.stringify({ hook_id: updatedId, chat_id: updatedChat, message: updatedMessage }), }); setFeedback("Hook updated.", "#64dd9b"); toggleEditForm(false); await loadHooks(); } catch (err) { setFeedback(`Update failed: ${err.message}`, "#ffbac7"); } finally { saveEditBtn.disabled = false; cancelEditBtn.disabled = false; saveEditBtn.textContent = originalSaveText; setTimeout(() => { setFeedback(); }, 2500); } }); const deleteBtn = node.querySelector(".delete"); deleteBtn.addEventListener("click", async () => { if (!confirm("Delete this hook?")) return; try { await fetchJSON(`/api/hooks/${hook.hook_id}`, { method: "DELETE" }); await loadHooks(); } catch (err) { alert(`Failed to delete hook: ${err.message}`); } }); hooksListEl.appendChild(node); }); } catch (error) { hooksListEl.innerHTML = ""; const para = document.createElement("p"); para.className = "feedback"; para.textContent = `Failed to load hooks: ${error.message}`; hooksListEl.appendChild(para); } } startLoginForm.addEventListener("submit", async (event) => { event.preventDefault(); const phoneNumber = document.querySelector("#phone-number").value || null; loginFeedbackEl.textContent = "Sending code…"; try { const status = await fetchJSON("/api/login/start", { method: "POST", body: JSON.stringify({ phone_number: phoneNumber }), }); updateSessionUI(status); loginFeedbackEl.textContent = "Code sent successfully."; } catch (error) { loginFeedbackEl.textContent = `Failed to send code: ${error.message}`; } }); verifyLoginForm.addEventListener("submit", async (event) => { event.preventDefault(); const code = document.querySelector("#verification-code").value; const password = document.querySelector("#twofactor-password").value || null; loginFeedbackEl.textContent = "Verifying…"; try { const status = await fetchJSON("/api/login/verify", { method: "POST", body: JSON.stringify({ code, password }), }); updateSessionUI(status); loginFeedbackEl.textContent = status.authorized ? "Login completed." : "Enter your password to finish."; } catch (error) { loginFeedbackEl.textContent = `Verification failed: ${error.message}`; } }); logoutButton.addEventListener("click", async () => { try { const status = await fetchJSON("/api/logout", { method: "POST" }); updateSessionUI(status); loginFeedbackEl.textContent = "Logged out."; } catch (error) { loginFeedbackEl.textContent = `Logout failed: ${error.message}`; } }); createHookForm.addEventListener("submit", async (event) => { event.preventDefault(); const chatId = document.querySelector("#hook-chat-id").value.trim(); const message = document.querySelector("#hook-message").value.trim(); if (!chatId || !message) { alert("Chat ID and message are required."); return; } const submitBtn = createHookForm.querySelector("button[type='submit']"); const originalText = submitBtn.textContent; submitBtn.disabled = true; submitBtn.textContent = "Creating…"; try { await fetchJSON("/api/hooks", { method: "POST", body: JSON.stringify({ chat_id: chatId, message }), }); createHookForm.reset(); await loadHooks(); } catch (error) { alert(`Failed to create hook: ${error.message}`); } finally { submitBtn.disabled = false; submitBtn.textContent = originalText; } }); window.addEventListener("DOMContentLoaded", async () => { await refreshAll(); try { const status = await fetchJSON("/api/status"); const phoneInput = document.querySelector("#phone-number"); if (status.phone_number && !phoneInput.value) { phoneInput.value = status.phone_number; } } catch (err) { console.warn("Unable to preload phone number", err); } });