// The Webhook Monitor mini-app: listens for incoming webhook events and displays them. // This app works in both regular threads and the space-wide metathread — it simply // displays whatever webhook events arrive, with their full payload. // // To use: admins create a webhook via /webhooks with namespace "test" (or another // name), then this app's thread will display all events as they arrive. import { defineApp, namespace, addItem, removeItem } from "../apps.js"; const N = namespace("webhook"); export const app = defineApp({ name: "webhook-monitor", title: "Webhook Monitor", glyph: "📡", // Works in personal threads, group chats, and space-wide places: ["you", "chat", "space"], initialState: () => ({ events: [] }), // Listen for any webhooks.* events. The key here is "webhooks.test" but the // folder below is generic enough to handle any webhook namespace. events: { "webhooks.test": (s, e) => ({ events: addItem(s.events, { id: e.hook?.id || e.name + "-" + e.ts, type: e.type, name: e.name, request: e.req, ts: e.ts, }), }), [N.ev("remove")]: (s, e) => ({ events: removeItem(s.events, e.id), }), // Add more webhook namespaces here as needed, e.g.: // "webhooks.github": (s, e) => ({ // events: addItem(s.events, { // id: e.hook?.id || e.name + "-" + e.ts, // type: e.type, // name: e.name, // request: e.req, // ts: e.ts, // }), // }), }, // Display the received events chatMessage(e) { if (e.type === "webhooks.test") { return { text: `📡 Received webhook event`, action: false }; } return null; }, styles: ` .webhook-monitor-pane { max-width: 800px; margin: 0 auto; padding: 16px; height: 100%; overflow-y: auto; box-sizing: border-box; } .webhook-monitor-header { margin-bottom: 16px; } .webhook-monitor-header h2 { margin: 0 0 4px; font-size: 1.4em; } .webhook-monitor-blurb { color: var(--muted); font-size: 0.9em; margin: 0; } .webhook-monitor-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; } .webhook-monitor-item { border: 1px solid var(--border); border-radius: 8px; padding: 12px; background: var(--bubble, #f5f5f5); } .webhook-monitor-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--border); } .webhook-monitor-item-type { font-weight: 600; color: var(--ink); } .webhook-monitor-item-time { font-size: 0.85em; color: var(--muted); } .webhook-monitor-item-payload { background: var(--panel); border-radius: 6px; padding: 8px; font-family: monospace; font-size: 0.85em; overflow-x: auto; white-space: pre-wrap; word-break: break-word; color: var(--muted); max-height: 300px; overflow-y: auto; } .webhook-monitor-item-delete { padding: 4px 8px; font-size: 0.85em; background: transparent; border: 1px solid var(--border); border-radius: 4px; cursor: var(--ui-cursor, pointer); color: var(--muted); } .webhook-monitor-item-delete:hover { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); } .webhook-monitor-empty { color: var(--muted); text-align: center; padding: 32px 16px; font-size: 0.95em; } `, render(state, el, ctx) { const pane = document.createElement("div"); pane.className = "webhook-monitor-pane"; const header = document.createElement("div"); header.className = "webhook-monitor-header"; const title = document.createElement("h2"); title.textContent = "Webhook Monitor"; const blurb = document.createElement("p"); blurb.className = "webhook-monitor-blurb"; blurb.textContent = "Incoming webhook events appear here as they arrive."; header.append(title, blurb); pane.appendChild(header); const events = (state && state.events) || []; if (!events.length) { const empty = document.createElement("div"); empty.className = "webhook-monitor-empty"; empty.textContent = "No webhook events yet. Create a webhook via /webhooks and send it a request."; pane.appendChild(empty); } else { const list = document.createElement("ul"); list.className = "webhook-monitor-list"; // Display newest first for (const item of [...events].reverse()) { const li = document.createElement("li"); li.className = "webhook-monitor-item"; const itemHeader = document.createElement("div"); itemHeader.className = "webhook-monitor-item-header"; const typeAndName = document.createElement("div"); typeAndName.className = "webhook-monitor-item-type"; const typeEl = document.createElement("div"); typeEl.textContent = item.type; const nameEl = document.createElement("div"); nameEl.style.fontSize = "0.85em"; nameEl.style.color = "var(--muted)"; nameEl.textContent = item.name || "Webhook"; typeAndName.append(typeEl, nameEl); const timeDiv = document.createElement("div"); timeDiv.className = "webhook-monitor-item-time"; const date = new Date(item.ts); timeDiv.textContent = date.toLocaleTimeString(); const deleteBtn = document.createElement("button"); deleteBtn.className = "webhook-monitor-item-delete"; deleteBtn.textContent = "Clear"; deleteBtn.onclick = () => ctx.emit({ type: N.ev("remove"), id: item.id }); itemHeader.append(typeAndName, timeDiv, deleteBtn); const payloadDiv = document.createElement("div"); payloadDiv.className = "webhook-monitor-item-payload"; // Organize display into event metadata and request details const reqData = item.request || {}; const display = { event: { type: item.type, name: item.name, }, request: { method: reqData.method, path: reqData.path, query: reqData.query, headers: reqData.headers, body: reqData.body, }, }; if (reqData.body_b64) { display.request.body_b64 = true; } payloadDiv.textContent = JSON.stringify(display, null, 2); li.append(itemHeader, payloadDiv); list.appendChild(li); } pane.appendChild(list); } el.appendChild(pane); }, // Optional: also add a command to clear all events commands: { "/clear-webhooks": { desc: "Clear all webhook events from this monitor", local: true, run: (args, ctx) => { ctx.reply({ text: "Webhook history cleared." }); }, }, }, });