A bot hook (bot_hooks) is a secret URL, POST /api/hooks/<token>, whose calls queue the hook's brief plus the raw body as a task, per user and bot like a routine. Only the token's hash is kept (shown once, rotatable); an optional HMAC-SHA256 signing secret is stored encrypted. Calls are rate limited per address and per hook, recorded in bot_hook_events (pruned daily), deduplicated by delivery id, and a call while the last run still waits joins its brief instead of queueing another. The payload is fenced data and HookRunNote tells the bot never to take instructions from it; failed runs retry like routines. A hook can instead watch a computer's browser notifications: the app installs a small watcher that owns org.freedesktop.Notifications on the sandbox's session bus, so Chrome hands it every notification (it only looks for that service at start-up, so Chrome is restarted once, never mid-task), and it posts each one to its hooks, optionally filtered. Computers reach the app at COMPUTER_APP_URL or the app container's name. Users make hooks in the conversation's Hooks modal and manage them all on /admin/routines; bots never create or change hooks. Co-Authored-By: Claude Opus 5.5 <[email protected]>
41 lines
1.5 KiB
TypeScript
41 lines
1.5 KiB
TypeScript
import Row from "@moduletrace/twui/layout/Row";
|
|
import Span from "@moduletrace/twui/layout/Span";
|
|
import Stack from "@moduletrace/twui/layout/Stack";
|
|
import type { BUN_SQLITE_COBBLE_BOT_HOOK_EVENTS } from "@/db/types/db";
|
|
import { BotHookEventStatuses } from "@/src/dict/bot-hook-dict";
|
|
import relativeTime from "@/src/utils/relative-time";
|
|
|
|
type Props = {
|
|
event: BUN_SQLITE_COBBLE_BOT_HOOK_EVENTS;
|
|
};
|
|
|
|
/**
|
|
* One call to a hook: what became of it and when, where it came from, why
|
|
* it was refused, and the start of what it sent.
|
|
*/
|
|
export default function HookEventRow({ event }: Props) {
|
|
const status = BotHookEventStatuses.find((s) => s.value == event.status)?.title || event.status || "";
|
|
const meta = [
|
|
status,
|
|
event.created_at ? relativeTime({ at: Number(event.created_at) }) : "",
|
|
event.source_ip || "",
|
|
event.job_id ? `job #${event.job_id}` : "",
|
|
event.error || "",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" · ");
|
|
|
|
return (
|
|
<Stack className="w-full gap-1! items-stretch px-3 py-2">
|
|
<Row className="w-full gap-2! flex-nowrap">
|
|
<Span className="text-xs font-bold leading-4 text-ink truncate">{meta}</Span>
|
|
</Row>
|
|
{event.payload ? (
|
|
<Span className="block w-full max-h-24 overflow-hidden whitespace-pre-wrap break-all rounded-default-xs bg-sunken px-2 py-1 font-mono text-[12px] leading-4 text-ink-secondary">
|
|
{event.payload}
|
|
</Span>
|
|
) : null}
|
|
</Stack>
|
|
);
|
|
}
|