export type MattermostClient = { baseUrl: string; apiBaseUrl: string; token: string; request: (path: string, init?: RequestInit) => Promise; }; export type MattermostUser = { id: string; username?: string | null; nickname?: string | null; first_name?: string | null; last_name?: string | null; }; export type MattermostChannel = { id: string; name?: string | null; display_name?: string | null; type?: string | null; team_id?: string | null; }; export type MattermostPost = { id: string; user_id?: string | null; channel_id?: string | null; message?: string | null; file_ids?: string[] | null; type?: string | null; root_id?: string | null; create_at?: number | null; props?: Record | null; }; export type MattermostFileInfo = { id: string; name?: string | null; mime_type?: string | null; size?: number | null; }; export function normalizeMattermostBaseUrl(raw?: string | null): string | undefined { const trimmed = raw?.trim(); if (!trimmed) return undefined; const withoutTrailing = trimmed.replace(/\/+$/, ""); return withoutTrailing.replace(/\/api\/v4$/i, ""); } function buildMattermostApiUrl(baseUrl: string, path: string): string { const normalized = normalizeMattermostBaseUrl(baseUrl); if (!normalized) throw new Error("Mattermost baseUrl is required"); const suffix = path.startsWith("/") ? path : `/${path}`; return `${normalized}/api/v4${suffix}`; } async function readMattermostError(res: Response): Promise { const contentType = res.headers.get("content-type") ?? ""; if (contentType.includes("application/json")) { const data = (await res.json()) as { message?: string } | undefined; if (data?.message) return data.message; return JSON.stringify(data); } return await res.text(); } export function createMattermostClient(params: { baseUrl: string; botToken: string; fetchImpl?: typeof fetch; }): MattermostClient { const baseUrl = normalizeMattermostBaseUrl(params.baseUrl); if (!baseUrl) throw new Error("Mattermost baseUrl is required"); const apiBaseUrl = `${baseUrl}/api/v4`; const token = params.botToken.trim(); const fetchImpl = params.fetchImpl ?? fetch; const request = async (path: string, init?: RequestInit): Promise => { const url = buildMattermostApiUrl(baseUrl, path); const headers = new Headers(init?.headers); headers.set("Authorization", `Bearer ${token}`); if (typeof init?.body === "string" && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const res = await fetchImpl(url, { ...init, headers }); if (!res.ok) { const detail = await readMattermostError(res); throw new Error( `Mattermost API ${res.status} ${res.statusText}: ${detail || "unknown error"}`, ); } return (await res.json()) as T; }; return { baseUrl, apiBaseUrl, token, request }; } export async function fetchMattermostMe(client: MattermostClient): Promise { return await client.request("/users/me"); } export async function fetchMattermostUser( client: MattermostClient, userId: string, ): Promise { return await client.request(`/users/${userId}`); } export async function fetchMattermostUserByUsername( client: MattermostClient, username: string, ): Promise { return await client.request(`/users/username/${encodeURIComponent(username)}`); } export async function fetchMattermostChannel( client: MattermostClient, channelId: string, ): Promise { return await client.request(`/channels/${channelId}`); } export async function sendMattermostTyping( client: MattermostClient, params: { channelId: string; parentId?: string }, ): Promise { const payload: Record = { channel_id: params.channelId, }; const parentId = params.parentId?.trim(); if (parentId) payload.parent_id = parentId; await client.request>("/users/me/typing", { method: "POST", body: JSON.stringify(payload), }); } export async function createMattermostDirectChannel( client: MattermostClient, userIds: string[], ): Promise { return await client.request("/channels/direct", { method: "POST", body: JSON.stringify(userIds), }); } export async function createMattermostPost( client: MattermostClient, params: { channelId: string; message: string; rootId?: string; fileIds?: string[]; }, ): Promise { const payload: Record = { channel_id: params.channelId, message: params.message, }; if (params.rootId) payload.root_id = params.rootId; if (params.fileIds?.length) { (payload as Record).file_ids = params.fileIds; } return await client.request("/posts", { method: "POST", body: JSON.stringify(payload), }); } export async function uploadMattermostFile( client: MattermostClient, params: { channelId: string; buffer: Buffer; fileName: string; contentType?: string; }, ): Promise { const form = new FormData(); const fileName = params.fileName?.trim() || "upload"; const bytes = Uint8Array.from(params.buffer); const blob = params.contentType ? new Blob([bytes], { type: params.contentType }) : new Blob([bytes]); form.append("files", blob, fileName); form.append("channel_id", params.channelId); const res = await fetch(`${client.apiBaseUrl}/files`, { method: "POST", headers: { Authorization: `Bearer ${client.token}`, }, body: form, }); if (!res.ok) { const detail = await readMattermostError(res); throw new Error(`Mattermost API ${res.status} ${res.statusText}: ${detail || "unknown error"}`); } const data = (await res.json()) as { file_infos?: MattermostFileInfo[] }; const info = data.file_infos?.[0]; if (!info?.id) { throw new Error("Mattermost file upload failed"); } return info; }