feat: add beta googlechat channel
This commit is contained in:
committed by
Peter Steinberger
parent
60661441b1
commit
b76cd6695d
207
extensions/googlechat/src/api.ts
Normal file
207
extensions/googlechat/src/api.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import type { ResolvedGoogleChatAccount } from "./accounts.js";
|
||||
import { getGoogleChatAccessToken } from "./auth.js";
|
||||
import type { GoogleChatReaction } from "./types.js";
|
||||
|
||||
const CHAT_API_BASE = "https://chat.googleapis.com/v1";
|
||||
const CHAT_UPLOAD_BASE = "https://chat.googleapis.com/upload/v1";
|
||||
|
||||
async function fetchJson<T>(
|
||||
account: ResolvedGoogleChatAccount,
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
): Promise<T> {
|
||||
const token = await getGoogleChatAccessToken(account);
|
||||
const res = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`Google Chat API ${res.status}: ${text || res.statusText}`);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
async function fetchOk(
|
||||
account: ResolvedGoogleChatAccount,
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
): Promise<void> {
|
||||
const token = await getGoogleChatAccessToken(account);
|
||||
const res = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`Google Chat API ${res.status}: ${text || res.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBuffer(
|
||||
account: ResolvedGoogleChatAccount,
|
||||
url: string,
|
||||
init?: RequestInit,
|
||||
): Promise<{ buffer: Buffer; contentType?: string }> {
|
||||
const token = await getGoogleChatAccessToken(account);
|
||||
const res = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`Google Chat API ${res.status}: ${text || res.statusText}`);
|
||||
}
|
||||
const buffer = Buffer.from(await res.arrayBuffer());
|
||||
const contentType = res.headers.get("content-type") ?? undefined;
|
||||
return { buffer, contentType };
|
||||
}
|
||||
|
||||
export async function sendGoogleChatMessage(params: {
|
||||
account: ResolvedGoogleChatAccount;
|
||||
space: string;
|
||||
text?: string;
|
||||
thread?: string;
|
||||
attachments?: Array<{ attachmentUploadToken: string; contentName?: string }>;
|
||||
}): Promise<{ messageName?: string } | null> {
|
||||
const { account, space, text, thread, attachments } = params;
|
||||
const body: Record<string, unknown> = {};
|
||||
if (text) body.text = text;
|
||||
if (thread) body.thread = { name: thread };
|
||||
if (attachments && attachments.length > 0) {
|
||||
body.attachment = attachments.map((item) => ({
|
||||
attachmentDataRef: { attachmentUploadToken: item.attachmentUploadToken },
|
||||
...(item.contentName ? { contentName: item.contentName } : {}),
|
||||
}));
|
||||
}
|
||||
const url = `${CHAT_API_BASE}/${space}/messages`;
|
||||
const result = await fetchJson<{ name?: string }>(account, url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return result ? { messageName: result.name } : null;
|
||||
}
|
||||
|
||||
export async function uploadGoogleChatAttachment(params: {
|
||||
account: ResolvedGoogleChatAccount;
|
||||
space: string;
|
||||
filename: string;
|
||||
buffer: Buffer;
|
||||
contentType?: string;
|
||||
}): Promise<{ attachmentUploadToken?: string }> {
|
||||
const { account, space, filename, buffer, contentType } = params;
|
||||
const boundary = `clawdbot-${crypto.randomUUID()}`;
|
||||
const metadata = JSON.stringify({ filename });
|
||||
const header = `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${metadata}\r\n`;
|
||||
const mediaHeader = `--${boundary}\r\nContent-Type: ${contentType ?? "application/octet-stream"}\r\n\r\n`;
|
||||
const footer = `\r\n--${boundary}--\r\n`;
|
||||
const body = Buffer.concat([
|
||||
Buffer.from(header, "utf8"),
|
||||
Buffer.from(mediaHeader, "utf8"),
|
||||
buffer,
|
||||
Buffer.from(footer, "utf8"),
|
||||
]);
|
||||
|
||||
const token = await getGoogleChatAccessToken(account);
|
||||
const url = `${CHAT_UPLOAD_BASE}/${space}/attachments:upload?uploadType=multipart`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": `multipart/related; boundary=${boundary}`,
|
||||
},
|
||||
body,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`Google Chat upload ${res.status}: ${text || res.statusText}`);
|
||||
}
|
||||
const payload = (await res.json()) as {
|
||||
attachmentDataRef?: { attachmentUploadToken?: string };
|
||||
};
|
||||
return { attachmentUploadToken: payload.attachmentDataRef?.attachmentUploadToken };
|
||||
}
|
||||
|
||||
export async function downloadGoogleChatMedia(params: {
|
||||
account: ResolvedGoogleChatAccount;
|
||||
resourceName: string;
|
||||
}): Promise<{ buffer: Buffer; contentType?: string }> {
|
||||
const { account, resourceName } = params;
|
||||
const url = `${CHAT_API_BASE}/media/${resourceName}?alt=media`;
|
||||
return await fetchBuffer(account, url);
|
||||
}
|
||||
|
||||
export async function createGoogleChatReaction(params: {
|
||||
account: ResolvedGoogleChatAccount;
|
||||
messageName: string;
|
||||
emoji: string;
|
||||
}): Promise<GoogleChatReaction> {
|
||||
const { account, messageName, emoji } = params;
|
||||
const url = `${CHAT_API_BASE}/${messageName}/reactions`;
|
||||
return await fetchJson<GoogleChatReaction>(account, url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ emoji: { unicode: emoji } }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listGoogleChatReactions(params: {
|
||||
account: ResolvedGoogleChatAccount;
|
||||
messageName: string;
|
||||
limit?: number;
|
||||
}): Promise<GoogleChatReaction[]> {
|
||||
const { account, messageName, limit } = params;
|
||||
const url = new URL(`${CHAT_API_BASE}/${messageName}/reactions`);
|
||||
if (limit && limit > 0) url.searchParams.set("pageSize", String(limit));
|
||||
const result = await fetchJson<{ reactions?: GoogleChatReaction[] }>(account, url.toString(), {
|
||||
method: "GET",
|
||||
});
|
||||
return result.reactions ?? [];
|
||||
}
|
||||
|
||||
export async function deleteGoogleChatReaction(params: {
|
||||
account: ResolvedGoogleChatAccount;
|
||||
reactionName: string;
|
||||
}): Promise<void> {
|
||||
const { account, reactionName } = params;
|
||||
const url = `${CHAT_API_BASE}/${reactionName}`;
|
||||
await fetchOk(account, url, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function findGoogleChatDirectMessage(params: {
|
||||
account: ResolvedGoogleChatAccount;
|
||||
userName: string;
|
||||
}): Promise<{ name?: string; displayName?: string } | null> {
|
||||
const { account, userName } = params;
|
||||
const url = new URL(`${CHAT_API_BASE}/spaces:findDirectMessage`);
|
||||
url.searchParams.set("name", userName);
|
||||
return await fetchJson<{ name?: string; displayName?: string }>(account, url.toString(), {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
export async function probeGoogleChat(account: ResolvedGoogleChatAccount): Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = new URL(`${CHAT_API_BASE}/spaces`);
|
||||
url.searchParams.set("pageSize", "1");
|
||||
await fetchJson<Record<string, unknown>>(account, url.toString(), { method: "GET" });
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user