Files
clawdbot/extensions/googlechat/src/actions.ts
2026-01-24 23:30:45 +00:00

163 lines
5.6 KiB
TypeScript

import type {
ChannelMessageActionAdapter,
ChannelMessageActionName,
ClawdbotConfig,
} from "clawdbot/plugin-sdk";
import {
createActionGate,
jsonResult,
readNumberParam,
readReactionParams,
readStringParam,
} from "clawdbot/plugin-sdk";
import { listEnabledGoogleChatAccounts, resolveGoogleChatAccount } from "./accounts.js";
import {
createGoogleChatReaction,
deleteGoogleChatReaction,
listGoogleChatReactions,
sendGoogleChatMessage,
uploadGoogleChatAttachment,
} from "./api.js";
import { getGoogleChatRuntime } from "./runtime.js";
import { resolveGoogleChatOutboundSpace } from "./targets.js";
const providerId = "googlechat";
function listEnabledAccounts(cfg: ClawdbotConfig) {
return listEnabledGoogleChatAccounts(cfg).filter(
(account) => account.enabled && account.credentialSource !== "none",
);
}
function isReactionsEnabled(accounts: ReturnType<typeof listEnabledAccounts>, cfg: ClawdbotConfig) {
for (const account of accounts) {
const gate = createActionGate(
(account.config.actions ?? (cfg.channels?.["googlechat"] as { actions?: unknown })?.actions) as Record<
string,
boolean | undefined
>,
);
if (gate("reactions")) return true;
}
return false;
}
function resolveAppUserNames(account: { config: { botUser?: string | null } }) {
return new Set(["users/app", account.config.botUser?.trim()].filter(Boolean) as string[]);
}
export const googlechatMessageActions: ChannelMessageActionAdapter = {
listActions: ({ cfg }) => {
const accounts = listEnabledAccounts(cfg as ClawdbotConfig);
if (accounts.length === 0) return [];
const actions = new Set<ChannelMessageActionName>([]);
actions.add("send");
if (isReactionsEnabled(accounts, cfg as ClawdbotConfig)) {
actions.add("react");
actions.add("reactions");
}
return Array.from(actions);
},
extractToolSend: ({ args }) => {
const action = typeof args.action === "string" ? args.action.trim() : "";
if (action !== "sendMessage") return null;
const to = typeof args.to === "string" ? args.to : undefined;
if (!to) return null;
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
return { to, accountId };
},
handleAction: async ({ action, params, cfg, accountId }) => {
const account = resolveGoogleChatAccount({
cfg: cfg as ClawdbotConfig,
accountId,
});
if (account.credentialSource === "none") {
throw new Error("Google Chat credentials are missing.");
}
if (action === "send") {
const to = readStringParam(params, "to", { required: true });
const content = readStringParam(params, "message", {
required: true,
allowEmpty: true,
});
const mediaUrl = readStringParam(params, "media", { trim: false });
const threadId = readStringParam(params, "threadId") ?? readStringParam(params, "replyTo");
const space = await resolveGoogleChatOutboundSpace({ account, target: to });
if (mediaUrl) {
const core = getGoogleChatRuntime();
const maxBytes = (account.config.mediaMaxMb ?? 20) * 1024 * 1024;
const loaded = await core.channel.media.fetchRemoteMedia(mediaUrl, { maxBytes });
const upload = await uploadGoogleChatAttachment({
account,
space,
filename: loaded.filename ?? "attachment",
buffer: loaded.buffer,
contentType: loaded.contentType,
});
await sendGoogleChatMessage({
account,
space,
text: content,
thread: threadId ?? undefined,
attachments: upload.attachmentUploadToken
? [{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.filename }]
: undefined,
});
return jsonResult({ ok: true, to: space });
}
await sendGoogleChatMessage({
account,
space,
text: content,
thread: threadId ?? undefined,
});
return jsonResult({ ok: true, to: space });
}
if (action === "react") {
const messageName = readStringParam(params, "messageId", { required: true });
const { emoji, remove, isEmpty } = readReactionParams(params, {
removeErrorMessage: "Emoji is required to remove a Google Chat reaction.",
});
if (remove || isEmpty) {
const reactions = await listGoogleChatReactions({ account, messageName });
const appUsers = resolveAppUserNames(account);
const toRemove = reactions.filter((reaction) => {
const userName = reaction.user?.name?.trim();
if (appUsers.size > 0 && !appUsers.has(userName ?? "")) return false;
if (emoji) return reaction.emoji?.unicode === emoji;
return true;
});
for (const reaction of toRemove) {
if (!reaction.name) continue;
await deleteGoogleChatReaction({ account, reactionName: reaction.name });
}
return jsonResult({ ok: true, removed: toRemove.length });
}
const reaction = await createGoogleChatReaction({
account,
messageName,
emoji,
});
return jsonResult({ ok: true, reaction });
}
if (action === "reactions") {
const messageName = readStringParam(params, "messageId", { required: true });
const limit = readNumberParam(params, "limit", { integer: true });
const reactions = await listGoogleChatReactions({
account,
messageName,
limit: limit ?? undefined,
});
return jsonResult({ ok: true, reactions });
}
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
},
};