server/app-event-catalog.ts
Rama main · réplica de lectura del repositorio original.
// The events a bot (or the person, for a bot) can watch: only connected apps that bot may use, each with its event
// types from the connected-apps service. Apps it may not use are left out entirely, never listed as refused.
import { botMayWatchApp, type AppEventCatalog, type AppEventType } from "../shared/app-triggers.ts";
import type { ConnectorToolGrant } from "../shared/wire.ts";
import type { AppTriggerBroker, BrokerTriggerType } from "./app-trigger-broker.ts";
/** Asked about most often; shown as "not connected yet" so the bot can offer to connect one. */
const POPULAR = ["gmail", "slack", "googlecalendar", "outlook", "github", "hubspot", "notion", "googlesheets"];
const MAX_APPS = 10;
const MAX_EVENTS = 25;
const DESCRIPTION_MAX = 200;
type Bot = { composio?: boolean; connectorTools?: Record<string, ConnectorToolGrant>; connectorScopes?: { apps: Record<string, "read" | "write"> } };
type Connected = Record<string, { connected: boolean; accounts: Array<{ id: string; alias?: string; status: string }> }>;
const label = (slug: string) => ({ googlecalendar: "Google Calendar", googlesheets: "Google Sheets", github: "GitHub", hubspot: "HubSpot" } as Record<string, string>)[slug]
?? slug.split(/[-_]+/).map((part) => part ? part[0]!.toUpperCase() + part.slice(1) : part).join(" ");
function eventType(type: BrokerTriggerType): AppEventType {
const properties = (type.config.properties ?? {}) as Record<string, { type?: unknown; description?: unknown; default?: unknown }>;
const required = new Set(Array.isArray(type.config.required) ? type.config.required : []);
const payload = (type.payload.properties ?? {}) as Record<string, { type?: unknown }>;
return {
event: type.slug,
name: type.name,
description: type.description.replace(/\s+/g, " ").trim().slice(0, DESCRIPTION_MAX),
delivery: type.type === "poll" ? "poll" : "instant",
configFields: Object.entries(properties).map(([name, field]) => ({
name,
type: typeof field.type === "string" ? field.type : "string",
required: required.has(name) && field.default === undefined,
...(typeof field.description === "string" ? { description: field.description.replace(/\s+/g, " ").slice(0, 160) } : {}),
...(field.default !== undefined ? { default: field.default } : {}),
})),
filterFields: Object.entries(payload).filter(([, field]) => field.type === "string" || field.type === "array").map(([name]) => name).slice(0, 20),
};
}
export async function appEventCatalog(input: {
broker: AppTriggerBroker;
connected: Connected;
bot: Bot;
app?: string;
search?: string;
}): Promise<AppEventCatalog> {
const usable = Object.entries(input.connected)
.filter(([slug, state]) => state.connected && state.accounts.some((account) => /^active$/i.test(account.status)) && botMayWatchApp(input.bot, slug))
.map(([slug]) => slug)
.sort();
const wanted = input.app?.trim().toLowerCase();
const apps = (wanted ? usable.filter((slug) => slug === wanted) : usable).slice(0, MAX_APPS);
const words = input.search?.toLowerCase().split(/\s+/).filter(Boolean) ?? [];
const types = apps.length ? await input.broker.triggerTypes(apps) : {};
return {
apps: apps.map((slug) => {
const loaded = types[slug];
const events = (loaded?.types ?? []).map(eventType)
.filter((event) => words.every((word) => `${event.name} ${event.description} ${event.event}`.toLowerCase().includes(word)))
.slice(0, MAX_EVENTS);
return {
app: slug,
name: label(slug),
accounts: input.connected[slug]!.accounts.filter((account) => /^active$/i.test(account.status)).map((account) => ({ id: account.id, ...(account.alias ? { alias: account.alias } : {}) })),
events,
...(loaded?.error ? { error: loaded.error } : {}),
};
}),
notConnected: POPULAR.filter((slug) => !input.connected[slug]?.connected && (!wanted || wanted === slug)).map((slug) => ({ app: slug, name: label(slug) })),
};
}
export function appLabel(slug: string): string {
return label(slug);
}