eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// The Webhook Monitor mini-app: listens for incoming webhook events and displays them.
2// This app works in both regular threads and the space-wide metathread — it simply
3// displays whatever webhook events arrive, with their full payload.
4//
5// To use: admins create a webhook via /webhooks with namespace "test" (or another
6// name), then this app's thread will display all events as they arrive.
7import { defineApp, namespace, addItem, removeItem } from "../apps.js";
8
9const N = namespace("webhook");
10
11export const app = defineApp({
12 name: "webhook-monitor",
13 title: "Webhook Monitor",
14 glyph: "📡",
15 // Works in personal threads, group chats, and space-wide
16 places: ["you", "chat", "space"],
17
18 initialState: () => ({ events: [] }),
19
20 // Listen for any webhooks.* events. The key here is "webhooks.test" but the
21 // folder below is generic enough to handle any webhook namespace.
22 events: {
23 "webhooks.test": (s, e) => ({
24 events: addItem(s.events, {
25 id: e.hook?.id || e.name + "-" + e.ts,
26 type: e.type,
27 name: e.name,
28 request: e.req,
29 ts: e.ts,
30 }),
31 }),
32 [N.ev("remove")]: (s, e) => ({
33 events: removeItem(s.events, e.id),
34 }),
35 // Add more webhook namespaces here as needed, e.g.:
36 // "webhooks.github": (s, e) => ({
37 // events: addItem(s.events, {
38 // id: e.hook?.id || e.name + "-" + e.ts,
39 // type: e.type,
40 // name: e.name,
41 // request: e.req,
42 // ts: e.ts,
43 // }),
44 // }),
45 },
46
47 // Display the received events
48 chatMessage(e) {
49 if (e.type === "webhooks.test") {
50 return { text: `📡 Received webhook event`, action: false };
51 }
52 return null;
53 },
54
55 styles: `
56.webhook-monitor-pane {
57 max-width: 800px;
58 margin: 0 auto;
59 padding: 16px;
60 height: 100%;
61 overflow-y: auto;
62 box-sizing: border-box;
63}
64.webhook-monitor-header {
65 margin-bottom: 16px;
66}
67.webhook-monitor-header h2 {
68 margin: 0 0 4px;
69 font-size: 1.4em;
70}
71.webhook-monitor-blurb {
72 color: var(--muted);
73 font-size: 0.9em;
74 margin: 0;
75}
76.webhook-monitor-list {
77 list-style: none;
78 margin: 0;
79 padding: 0;
80 display: flex;
81 flex-direction: column;
82 gap: 12px;
83}
84.webhook-monitor-item {
85 border: 1px solid var(--border);
86 border-radius: 8px;
87 padding: 12px;
88 background: var(--bubble, #f5f5f5);
89}
90.webhook-monitor-item-header {
91 display: flex;
92 justify-content: space-between;
93 align-items: center;
94 margin-bottom: 8px;
95 padding-bottom: 8px;
96 border-bottom: 1px solid var(--border);
97}
98.webhook-monitor-item-type {
99 font-weight: 600;
100 color: var(--ink);
101}
102.webhook-monitor-item-time {
103 font-size: 0.85em;
104 color: var(--muted);
105}
106.webhook-monitor-item-payload {
107 background: var(--panel);
108 border-radius: 6px;
109 padding: 8px;
110 font-family: monospace;
111 font-size: 0.85em;
112 overflow-x: auto;
113 white-space: pre-wrap;
114 word-break: break-word;
115 color: var(--muted);
116 max-height: 300px;
117 overflow-y: auto;
118}
119.webhook-monitor-item-delete {
120 padding: 4px 8px;
121 font-size: 0.85em;
122 background: transparent;
123 border: 1px solid var(--border);
124 border-radius: 4px;
125 cursor: var(--ui-cursor, pointer);
126 color: var(--muted);
127}
128.webhook-monitor-item-delete:hover {
129 background: var(--accent);
130 color: var(--accent-ink);
131 border-color: var(--accent);
132}
133.webhook-monitor-empty {
134 color: var(--muted);
135 text-align: center;
136 padding: 32px 16px;
137 font-size: 0.95em;
138}
139`,
140
141 render(state, el, ctx) {
142 const pane = document.createElement("div");
143 pane.className = "webhook-monitor-pane";
144
145 const header = document.createElement("div");
146 header.className = "webhook-monitor-header";
147 const title = document.createElement("h2");
148 title.textContent = "Webhook Monitor";
149 const blurb = document.createElement("p");
150 blurb.className = "webhook-monitor-blurb";
151 blurb.textContent = "Incoming webhook events appear here as they arrive.";
152 header.append(title, blurb);
153 pane.appendChild(header);
154
155 const events = (state && state.events) || [];
156
157 if (!events.length) {
158 const empty = document.createElement("div");
159 empty.className = "webhook-monitor-empty";
160 empty.textContent = "No webhook events yet. Create a webhook via /webhooks and send it a request.";
161 pane.appendChild(empty);
162 } else {
163 const list = document.createElement("ul");
164 list.className = "webhook-monitor-list";
165
166 // Display newest first
167 for (const item of [...events].reverse()) {
168 const li = document.createElement("li");
169 li.className = "webhook-monitor-item";
170
171 const itemHeader = document.createElement("div");
172 itemHeader.className = "webhook-monitor-item-header";
173
174 const typeAndName = document.createElement("div");
175 typeAndName.className = "webhook-monitor-item-type";
176 const typeEl = document.createElement("div");
177 typeEl.textContent = item.type;
178 const nameEl = document.createElement("div");
179 nameEl.style.fontSize = "0.85em";
180 nameEl.style.color = "var(--muted)";
181 nameEl.textContent = item.name || "Webhook";
182 typeAndName.append(typeEl, nameEl);
183
184 const timeDiv = document.createElement("div");
185 timeDiv.className = "webhook-monitor-item-time";
186 const date = new Date(item.ts);
187 timeDiv.textContent = date.toLocaleTimeString();
188
189 const deleteBtn = document.createElement("button");
190 deleteBtn.className = "webhook-monitor-item-delete";
191 deleteBtn.textContent = "Clear";
192 deleteBtn.onclick = () => ctx.emit({ type: N.ev("remove"), id: item.id });
193
194 itemHeader.append(typeAndName, timeDiv, deleteBtn);
195
196 const payloadDiv = document.createElement("div");
197 payloadDiv.className = "webhook-monitor-item-payload";
198
199 // Organize display into event metadata and request details
200 const reqData = item.request || {};
201 const display = {
202 event: {
203 type: item.type,
204 name: item.name,
205 },
206 request: {
207 method: reqData.method,
208 path: reqData.path,
209 query: reqData.query,
210 headers: reqData.headers,
211 body: reqData.body,
212 },
213 };
214 if (reqData.body_b64) {
215 display.request.body_b64 = true;
216 }
217 payloadDiv.textContent = JSON.stringify(display, null, 2);
218
219 li.append(itemHeader, payloadDiv);
220 list.appendChild(li);
221 }
222
223 pane.appendChild(list);
224 }
225
226 el.appendChild(pane);
227 },
228
229 // Optional: also add a command to clear all events
230 commands: {
231 "/clear-webhooks": {
232 desc: "Clear all webhook events from this monitor",
233 local: true,
234 run: (args, ctx) => {
235 ctx.reply({ text: "Webhook history cleared." });
236 },
237 },
238 },
239});
240