import * as z from "zod" import { defineAction } from "../../automation/actions" import { GRAPH_CHANNEL_SCHEMA, GRAPH_CHAT_SCHEMA, GRAPH_MEMBER_SCHEMA, GRAPH_TEAM_SCHEMA, TEAMS_CHANNEL_SCHEMA, TEAMS_CHAT_SCHEMA, TEAMS_MEMBER_SCHEMA, TEAMS_TEAM_SCHEMA, getTeamsGraphApi, listTeamsGraphCollection, } from "@automate.ax/integration-contracts/teams" import { TEAMS_CHANNEL_CREATE_REQUIREMENT, TEAMS_CHANNEL_DELETE_REQUIREMENT, TEAMS_CHANNEL_MEMBER_WRITE_REQUIREMENT, TEAMS_CHANNEL_REFERENCE_MEMBER_REQUIREMENT, TEAMS_CHANNEL_REFERENCE_MESSAGE_READ_REQUIREMENT, TEAMS_CHANNEL_REFERENCE_MESSAGE_SEND_REQUIREMENT, TEAMS_CHANNEL_WRITE_REQUIREMENT, TEAMS_CHAT_MEMBER_READ_REQUIREMENT, TEAMS_CHAT_MEMBER_WRITE_REQUIREMENT, TEAMS_CHAT_MESSAGE_READ_REQUIREMENT, TEAMS_CHAT_MESSAGE_SEND_REQUIREMENT, TEAMS_CHAT_WRITE_REQUIREMENT, TEAMS_TEAM_MEMBER_WRITE_REQUIREMENT, TEAMS_TEAM_READ_REQUIREMENT, TEAMS_TEAM_REFERENCE_MEMBER_REQUIREMENT, TEAMS_TEAM_REFERENCE_READ_REQUIREMENT, } from "./lib/scopes" const MAX_LIMIT = 10_000 const TEAM_INPUT_SCHEMA = z.object({ team: z.string().trim().min(1) }) const CHANNEL_INPUT_SCHEMA = TEAM_INPUT_SCHEMA.extend({ channel: z.string().trim().min(1), }) const CHAT_INPUT_SCHEMA = z.object({ chatId: z.string().min(1) }) const MEMBER_INPUT_SCHEMA = z.object({ memberId: z.string().min(1) }) const MEMBER_WRITE_SCHEMA = z.object({ roles: z.string().array().prefault([]), userId: z.string().min(1), }) const CHANNEL_MESSAGE_INPUT_SCHEMA = CHANNEL_INPUT_SCHEMA.extend({ messageId: z.string().min(1), replyToMessageId: z.string().min(1).optional(), }) const CHAT_MESSAGE_INPUT_SCHEMA = CHAT_INPUT_SCHEMA.extend({ messageId: z.string().min(1), }) const HOSTED_CONTENT_SCHEMA = z.object({ contentId: z.string(), contentType: z.string().optional(), }) const GRAPH_HOSTED_CONTENT_SCHEMA = z .looseObject({ contentBytes: z.string().nullish(), contentType: z.string().nullish(), id: z.string(), }) .transform(({ contentType, id }) => ({ contentId: id, contentType: contentType ?? undefined, })) /** Gets one joined Microsoft Team. */ export const getMicrosoftTeam = defineAction("Get Microsoft Team") .describe("Gets one joined Microsoft Team by display name or ID.") .account("microsoft", TEAMS_TEAM_READ_REQUIREMENT) .input(TEAM_INPUT_SCHEMA) .output(TEAMS_TEAM_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => GRAPH_TEAM_SCHEMA.parse( await getTeamsGraphApi(account.secret).request( `/teams/${encodeURIComponent(await resolveTeamId(account.secret, input.team))}`, ), ), ) /** Gets one Microsoft Teams channel. */ export const getMicrosoftTeamsChannel = defineAction( "Get Microsoft Teams channel", ) .describe("Gets one Microsoft Teams channel by display name or ID.") .account("microsoft", TEAMS_TEAM_REFERENCE_READ_REQUIREMENT) .input(CHANNEL_INPUT_SCHEMA) .output(TEAMS_CHANNEL_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { // Resolve once because both team and channel IDs form the request path. const reference = await resolveChannelReference(account.secret, input) return GRAPH_CHANNEL_SCHEMA.parse( await getTeamsGraphApi(account.secret).request(channelPath(reference)), ) }) /** Creates a Microsoft Teams channel. */ export const createMicrosoftTeamsChannel = defineAction( "Create Microsoft Teams channel", ) .describe("Creates a standard, private, or shared channel in a team.") .account("microsoft", TEAMS_CHANNEL_CREATE_REQUIREMENT) .input( TEAM_INPUT_SCHEMA.extend({ description: z.string().optional(), displayName: z.string().trim().min(1), membershipType: z .enum(["standard", "private", "shared"]) .prefault("standard"), }), ) .output(TEAMS_CHANNEL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { team, ...body } = input return GRAPH_CHANNEL_SCHEMA.parse( await getTeamsGraphApi(account.secret).request( `/teams/${encodeURIComponent(await resolveTeamId(account.secret, team))}/channels`, { body, method: "POST" }, ), ) }) /** Updates a Microsoft Teams channel. */ export const updateMicrosoftTeamsChannel = defineAction( "Update Microsoft Teams channel", ) .describe("Updates a Microsoft Teams channel name or description.") .account("microsoft", TEAMS_CHANNEL_WRITE_REQUIREMENT) .input( CHANNEL_INPUT_SCHEMA.extend({ description: z.string().optional(), displayName: z.string().trim().min(1).optional(), }), ) .output(TEAMS_CHANNEL_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { // Resolve once because both team and channel IDs form the request path. const reference = await resolveChannelReference(account.secret, input) const graphApi = getTeamsGraphApi(account.secret) const path = channelPath(reference) await graphApi.request(path, { body: { description: input.description, displayName: input.displayName, }, method: "PATCH", }) return GRAPH_CHANNEL_SCHEMA.parse(await graphApi.request(path)) }) /** Deletes a Microsoft Teams channel. */ export const deleteMicrosoftTeamsChannel = channelVoidAction( "Delete Microsoft Teams channel", "Deletes a Microsoft Teams channel.", "DELETE", ) /** Archives a Microsoft Teams channel. */ export const archiveMicrosoftTeamsChannel = channelVoidAction( "Archive Microsoft Teams channel", "Archives a Microsoft Teams channel.", "POST", "/archive", ) /** Unarchives a Microsoft Teams channel. */ export const unarchiveMicrosoftTeamsChannel = channelVoidAction( "Unarchive Microsoft Teams channel", "Restores an archived Microsoft Teams channel.", "POST", "/unarchive", ) /** Gets one direct Microsoft Team member. */ export const getMicrosoftTeamsTeamMember = memberReadAction( "Get Microsoft Team member", "Gets one direct Microsoft Team member.", "team", ) /** Adds one direct Microsoft Team member. */ export const addMicrosoftTeamsTeamMember = memberAddAction( "Add Microsoft Team member", "Adds a Microsoft Entra user to a team.", "team", ) /** Updates one direct Microsoft Team member. */ export const updateMicrosoftTeamsTeamMember = memberUpdateAction( "Update Microsoft Team member", "Updates a Microsoft Team member's roles.", "team", ) /** Removes one direct Microsoft Team member. */ export const removeMicrosoftTeamsTeamMember = memberRemoveAction( "Remove Microsoft Team member", "Removes a direct member from a team.", "team", ) /** Gets one Microsoft Teams channel member. */ export const getMicrosoftTeamsChannelMember = memberReadAction( "Get Microsoft Teams channel member", "Gets one direct private or shared channel member.", "channel", ) /** Adds one Microsoft Teams channel member. */ export const addMicrosoftTeamsChannelMember = memberAddAction( "Add Microsoft Teams channel member", "Adds a Microsoft Entra user to a private or shared channel.", "channel", ) /** Updates one Microsoft Teams channel member. */ export const updateMicrosoftTeamsChannelMember = memberUpdateAction( "Update Microsoft Teams channel member", "Updates a private or shared channel member's roles.", "channel", ) /** Removes one Microsoft Teams channel member. */ export const removeMicrosoftTeamsChannelMember = memberRemoveAction( "Remove Microsoft Teams channel member", "Removes a direct member from a private or shared channel.", "channel", ) /** Updates a Microsoft Teams group chat. */ export const updateMicrosoftTeamsChat = defineAction( "Update Microsoft Teams chat", ) .describe("Updates the topic of a Microsoft Teams group chat.") .account("microsoft", TEAMS_CHAT_WRITE_REQUIREMENT) .input(CHAT_INPUT_SCHEMA.extend({ topic: z.string() })) .output(TEAMS_CHAT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => GRAPH_CHAT_SCHEMA.parse( await getTeamsGraphApi(account.secret).request( `/chats/${encodeURIComponent(input.chatId)}`, { body: { topic: input.topic }, method: "PATCH" }, ), ), ) /** Gets one Microsoft Teams chat member. */ export const getMicrosoftTeamsChatMember = defineAction( "Get Microsoft Teams chat member", ) .describe("Gets one member of a Microsoft Teams chat.") .account("microsoft", TEAMS_CHAT_MEMBER_READ_REQUIREMENT) .input(CHAT_INPUT_SCHEMA.extend(MEMBER_INPUT_SCHEMA.shape)) .output(TEAMS_MEMBER_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => GRAPH_MEMBER_SCHEMA.parse( await getTeamsGraphApi(account.secret).request( `/chats/${encodeURIComponent(input.chatId)}/members/${encodeURIComponent(input.memberId)}`, ), ), ) /** Adds one Microsoft Teams chat member. */ export const addMicrosoftTeamsChatMember = defineAction( "Add Microsoft Teams chat member", ) .describe("Adds a Microsoft Entra user to a Microsoft Teams chat.") .account("microsoft", TEAMS_CHAT_MEMBER_WRITE_REQUIREMENT) .input( CHAT_INPUT_SCHEMA.extend(MEMBER_WRITE_SCHEMA.shape).extend({ visibleHistoryStartAt: z.date().optional(), }), ) .output(TEAMS_MEMBER_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const graphApi = getTeamsGraphApi(account.secret) const response = await graphApi.fetch( `/chats/${encodeURIComponent(input.chatId)}/members`, { body: JSON.stringify(memberPayload(input)), headers: { "Content-Type": "application/json" }, method: "POST", }, ) if (!response.ok) throw new Error( `Microsoft Graph chat-member creation failed with ${response.status} ${response.statusText}.`, ) const location = response.headers.get("Location") if (!location) throw new Error("Microsoft Graph omitted the created chat member URL.") return GRAPH_MEMBER_SCHEMA.parse( await graphApi.request(location.replace(/^\/?v1\.0\//, "")), ) }) /** Removes one Microsoft Teams chat member. */ export const removeMicrosoftTeamsChatMember = defineAction( "Remove Microsoft Teams chat member", ) .describe("Removes one member from a Microsoft Teams chat.") .account("microsoft", TEAMS_CHAT_MEMBER_WRITE_REQUIREMENT) .input(CHAT_INPUT_SCHEMA.extend(MEMBER_INPUT_SCHEMA.shape)) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getTeamsGraphApi(account.secret).request( `/chats/${encodeURIComponent(input.chatId)}/members/${encodeURIComponent(input.memberId)}`, { method: "DELETE" }, ) }) /** Sets a reaction on a Microsoft Teams channel message. */ export const setMicrosoftTeamsChannelMessageReaction = channelReactionAction( "Set Teams channel message reaction", "Sets the connected user's reaction on a Teams channel message.", "setReaction", ) /** Removes a reaction from a Microsoft Teams channel message. */ export const unsetMicrosoftTeamsChannelMessageReaction = channelReactionAction( "Unset Teams channel message reaction", "Removes the connected user's reaction from a Teams channel message.", "unsetReaction", ) /** Sets a reaction on a Microsoft Teams chat message. */ export const setMicrosoftTeamsChatMessageReaction = chatReactionAction( "Set Teams chat message reaction", "Sets the connected user's reaction on a Teams chat message.", "setReaction", ) /** Removes a reaction from a Microsoft Teams chat message. */ export const unsetMicrosoftTeamsChatMessageReaction = chatReactionAction( "Unset Teams chat message reaction", "Removes the connected user's reaction from a Teams chat message.", "unsetReaction", ) /** Lists hosted content attached to a Teams channel message. */ export const listMicrosoftTeamsChannelMessageHostedContent = channelHostedContentListAction() /** Downloads hosted content attached to a Teams channel message. */ export const getMicrosoftTeamsChannelMessageHostedContent = channelHostedContentGetAction() /** Lists hosted content attached to a Teams chat message. */ export const listMicrosoftTeamsChatMessageHostedContent = chatHostedContentListAction() /** Downloads hosted content attached to a Teams chat message. */ export const getMicrosoftTeamsChatMessageHostedContent = chatHostedContentGetAction() /** * Builds a channel action that returns no value. * * @param name - Public action name. * @param description - Public action description. * @param method - Graph request method. * @param suffix - Path appended to the channel resource. */ function channelVoidAction( name: string, description: string, method: "DELETE" | "POST", suffix = "", ) { return defineAction(name) .describe(description) .account( "microsoft", method === "DELETE" ? TEAMS_CHANNEL_DELETE_REQUIREMENT : TEAMS_CHANNEL_WRITE_REQUIREMENT, ) .input(CHANNEL_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { // Resolve once because both team and channel IDs form the request path. const reference = await resolveChannelReference(account.secret, input) const graphApi = getTeamsGraphApi(account.secret) const path = `${channelPath(reference)}${suffix}` if (method === "DELETE") { await graphApi.request(path, { method }) return } const response = await graphApi.fetch(path, { method }) if (!response.ok) throw await teamsResponseError("channel update", response) await waitForTeamsAsyncOperation(graphApi.fetch, response) }) } const TEAMS_ASYNC_OPERATION_SCHEMA = z.object({ error: z .object({ code: z.string().nullish(), message: z.string().nullish() }) .nullish(), status: z.enum([ "invalid", "notStarted", "inProgress", "succeeded", "failed", ]), }) /** * Waits for a Teams asynchronous operation to reach a terminal state. * * @param graphFetch - Authenticated raw Graph request function. * @param acceptedResponse - Initial 202 response containing the operation URL. */ async function waitForTeamsAsyncOperation( graphFetch: ReturnType["fetch"], acceptedResponse: Response, ) { const location = acceptedResponse.headers.get("Location") if (!location) { throw new Error("Microsoft Teams operation omitted its Location header.") } for (let attempt = 0; attempt < 8; attempt++) { if (attempt > 0) { await new Promise((resolve) => setTimeout(resolve, 31_000)) } const response = await graphFetch(location) if (!response.ok) throw await teamsResponseError("operation status", response) const operation = TEAMS_ASYNC_OPERATION_SCHEMA.parse(await response.json()) if (operation.status === "succeeded") return if (operation.status === "failed" || operation.status === "invalid") { throw new Error( `Microsoft Teams operation failed${operation.error?.code ? ` (${operation.error.code})` : ""}${operation.error?.message ? `: ${operation.error.message}` : "."}`, ) } } throw new Error("Microsoft Teams operation did not complete before timeout.") } /** * Builds a provider error without assuming Graph's error payload shape. * * @param operation - Operation that failed. * @param response - Unsuccessful Graph response. */ async function teamsResponseError(operation: string, response: Response) { const detail = await response.text() return new Error( `Microsoft Teams ${operation} failed with ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""}.`, ) } /** * Builds a team or channel member read action. * * @param name - Public action name. * @param description - Public action description. * @param kind - Member collection kind. */ function memberReadAction( name: string, description: string, kind: "team" | "channel", ) { // Preserve the selected collection's inferred input type for the handler. const inputSchema = kind === "team" ? TEAM_INPUT_SCHEMA.extend(MEMBER_INPUT_SCHEMA.shape) : CHANNEL_INPUT_SCHEMA.extend(MEMBER_INPUT_SCHEMA.shape) return defineAction(name) .describe(description) .account( "microsoft", kind === "team" ? TEAMS_TEAM_REFERENCE_MEMBER_REQUIREMENT : TEAMS_CHANNEL_REFERENCE_MEMBER_REQUIREMENT, ) .input(inputSchema) .output(TEAMS_MEMBER_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => GRAPH_MEMBER_SCHEMA.parse( await getTeamsGraphApi(account.secret).request( `${await memberCollectionPath(account.secret, input, kind)}/${encodeURIComponent(input.memberId)}`, ), ), ) } /** * Builds a team or channel member add action. * * @param name - Public action name. * @param description - Public action description. * @param kind - Member collection kind. */ function memberAddAction( name: string, description: string, kind: "team" | "channel", ) { // Preserve the selected collection's inferred input type for the handler. const inputSchema = kind === "team" ? TEAM_INPUT_SCHEMA.extend(MEMBER_WRITE_SCHEMA.shape) : CHANNEL_INPUT_SCHEMA.extend(MEMBER_WRITE_SCHEMA.shape) return defineAction(name) .describe(description) .account( "microsoft", kind === "team" ? TEAMS_TEAM_MEMBER_WRITE_REQUIREMENT : TEAMS_CHANNEL_MEMBER_WRITE_REQUIREMENT, ) .input(inputSchema) .output(TEAMS_MEMBER_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => GRAPH_MEMBER_SCHEMA.parse( await getTeamsGraphApi(account.secret).request( await memberCollectionPath(account.secret, input, kind), { body: memberPayload(input), method: "POST" }, ), ), ) } /** * Builds a team or channel member update action. * * @param name - Public action name. * @param description - Public action description. * @param kind - Member collection kind. */ function memberUpdateAction( name: string, description: string, kind: "team" | "channel", ) { // Preserve the selected collection's inferred input type for the handler. const inputSchema = ( kind === "team" ? TEAM_INPUT_SCHEMA : CHANNEL_INPUT_SCHEMA ) .extend(MEMBER_INPUT_SCHEMA.shape) .extend({ roles: z.string().array() }) return defineAction(name) .describe(description) .account( "microsoft", kind === "team" ? TEAMS_TEAM_MEMBER_WRITE_REQUIREMENT : TEAMS_CHANNEL_MEMBER_WRITE_REQUIREMENT, ) .input(inputSchema) .output(TEAMS_MEMBER_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => GRAPH_MEMBER_SCHEMA.parse( await getTeamsGraphApi(account.secret).request( `${await memberCollectionPath(account.secret, input, kind)}/${encodeURIComponent(input.memberId)}`, { body: { roles: input.roles }, method: "PATCH" }, ), ), ) } /** * Builds a team or channel member removal action. * * @param name - Public action name. * @param description - Public action description. * @param kind - Member collection kind. */ function memberRemoveAction( name: string, description: string, kind: "team" | "channel", ) { // Preserve the selected collection's inferred input type for the handler. const inputSchema = ( kind === "team" ? TEAM_INPUT_SCHEMA : CHANNEL_INPUT_SCHEMA ).extend(MEMBER_INPUT_SCHEMA.shape) return defineAction(name) .describe(description) .account( "microsoft", kind === "team" ? TEAMS_TEAM_MEMBER_WRITE_REQUIREMENT : TEAMS_CHANNEL_MEMBER_WRITE_REQUIREMENT, ) .input(inputSchema) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getTeamsGraphApi(account.secret).request( `${await memberCollectionPath(account.secret, input, kind)}/${encodeURIComponent(input.memberId)}`, { method: "DELETE" }, ) }) } /** * Builds a channel-message reaction action. * * @param name - Public action name. * @param description - Public action description. * @param operation - Graph reaction operation. */ function channelReactionAction( name: string, description: string, operation: "setReaction" | "unsetReaction", ) { return defineAction(name) .describe(description) .account("microsoft", TEAMS_CHANNEL_REFERENCE_MESSAGE_SEND_REQUIREMENT) .input( CHANNEL_MESSAGE_INPUT_SCHEMA.extend({ reactionType: z.string().min(1) }), ) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { // Resolve once because both team and channel IDs form the message root. const reference = await resolveChannelReference(account.secret, input) const root = `${channelPath(reference)}/messages` // Replies and root messages use different Graph resource paths. const messagePath = input.replyToMessageId ? `${root}/${encodeURIComponent(input.replyToMessageId)}/replies/${encodeURIComponent(input.messageId)}` : `${root}/${encodeURIComponent(input.messageId)}` await getTeamsGraphApi(account.secret).request( `${messagePath}/${operation}`, { body: { reactionType: input.reactionType }, method: "POST" }, ) }) } /** * Builds a chat-message reaction action. * * @param name - Public action name. * @param description - Public action description. * @param operation - Graph reaction operation. */ function chatReactionAction( name: string, description: string, operation: "setReaction" | "unsetReaction", ) { return defineAction(name) .describe(description) .account("microsoft", TEAMS_CHAT_MESSAGE_SEND_REQUIREMENT) .input( CHAT_MESSAGE_INPUT_SCHEMA.extend({ reactionType: z.string().min(1) }), ) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { await getTeamsGraphApi(account.secret).request( `/chats/${encodeURIComponent(input.chatId)}/messages/${encodeURIComponent(input.messageId)}/${operation}`, { body: { reactionType: input.reactionType }, method: "POST" }, ) }) } /** Builds the channel-message hosted-content list action. */ function channelHostedContentListAction() { return defineAction("List Teams channel message hosted content") .describe("Lists hosted content attached to a Teams channel message.") .account("microsoft", TEAMS_CHANNEL_REFERENCE_MESSAGE_READ_REQUIREMENT) .input(CHANNEL_MESSAGE_INPUT_SCHEMA) .output(HOSTED_CONTENT_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => listTeamsGraphCollection( getTeamsGraphApi(account.secret).request, `${await channelMessagePath(account.secret, input)}/hostedContents`, GRAPH_HOSTED_CONTENT_SCHEMA, MAX_LIMIT, ), ) } /** Builds the channel-message hosted-content download action. */ function channelHostedContentGetAction() { return defineAction("Get Teams channel message hosted content") .describe("Downloads hosted content attached to a Teams channel message.") .account("microsoft", TEAMS_CHANNEL_REFERENCE_MESSAGE_READ_REQUIREMENT) .input( CHANNEL_MESSAGE_INPUT_SCHEMA.extend({ contentId: z.string().min(1) }), ) .output(z.instanceof(File)) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => downloadHostedContent( account.secret, `${await channelMessagePath(account.secret, input)}/hostedContents/${encodeURIComponent(input.contentId)}/$value`, input.contentId, ), ) } /** Builds the chat-message hosted-content list action. */ function chatHostedContentListAction() { return defineAction("List Teams chat message hosted content") .describe("Lists hosted content attached to a Teams chat message.") .account("microsoft", TEAMS_CHAT_MESSAGE_READ_REQUIREMENT) .input(CHAT_MESSAGE_INPUT_SCHEMA) .output(HOSTED_CONTENT_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => listTeamsGraphCollection( getTeamsGraphApi(account.secret).request, `/chats/${encodeURIComponent(input.chatId)}/messages/${encodeURIComponent(input.messageId)}/hostedContents`, GRAPH_HOSTED_CONTENT_SCHEMA, MAX_LIMIT, ), ) } /** Builds the chat-message hosted-content download action. */ function chatHostedContentGetAction() { return defineAction("Get Teams chat message hosted content") .describe("Downloads hosted content attached to a Teams chat message.") .account("microsoft", TEAMS_CHAT_MESSAGE_READ_REQUIREMENT) .input(CHAT_MESSAGE_INPUT_SCHEMA.extend({ contentId: z.string().min(1) })) .output(z.instanceof(File)) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => downloadHostedContent( account.secret, `/chats/${encodeURIComponent(input.chatId)}/messages/${encodeURIComponent(input.messageId)}/hostedContents/${encodeURIComponent(input.contentId)}/$value`, input.contentId, ), ) } /** * Downloads Graph-hosted message content. * * @param secret - Microsoft account secret. * @param path - Graph hosted-content value path. * @param contentId - Filename for the returned content. */ async function downloadHostedContent( secret: Record, path: string, contentId: string, ) { const response = await getTeamsGraphApi(secret).fetch(path) if (!response.ok) throw new Error( `Microsoft Graph hosted-content download failed with ${response.status} ${response.statusText}.`, ) return new File([await response.blob()], contentId, { type: response.headers.get("content-type") ?? "application/octet-stream", }) } /** * Builds the Graph conversation-member payload. * * @param input - Member role and identity fields. * @param input.roles - Teams conversation roles. * @param input.userId - Microsoft user ID. * @param input.visibleHistoryStartAt - Earliest visible chat history. */ function memberPayload(input: { roles: string[] userId: string visibleHistoryStartAt?: Date }) { return { "@odata.type": "#microsoft.graph.aadUserConversationMember", roles: input.roles, "user@odata.bind": `https://graph.microsoft.com/v1.0/users('${input.userId}')`, ...(input.visibleHistoryStartAt && { visibleHistoryStartDateTime: input.visibleHistoryStartAt.toISOString(), }), } } /** * Resolves a team or channel member collection path. * * @param secret - Microsoft account secret. * @param input - Team and optional channel reference. * @param input.channel - Channel display name or ID. * @param input.team - Team display name or ID. * @param kind - Member collection kind. */ async function memberCollectionPath( secret: Record, input: { channel?: string; team: string }, kind: "team" | "channel", ) { const teamId = await resolveTeamId(secret, input.team) if (kind === "team") return `/teams/${encodeURIComponent(teamId)}/members` // Resolve both IDs before building the nested channel collection path. const reference = await resolveChannelReference(secret, { channel: input.channel!, team: input.team, }) return `${channelPath(reference)}/members` } /** * Resolves a channel message or reply path. * * @param secret - Microsoft account secret. * @param input - Channel message reference. */ async function channelMessagePath( secret: Record, input: z.output, ) { // Resolve both IDs before building the nested message path. const reference = await resolveChannelReference(secret, input) const root = `${channelPath(reference)}/messages` return input.replyToMessageId ? `${root}/${encodeURIComponent(input.replyToMessageId)}/replies/${encodeURIComponent(input.messageId)}` : `${root}/${encodeURIComponent(input.messageId)}` } /** * Builds a Graph channel resource path. * * @param reference - Resolved team and channel IDs. * @param reference.channelId - Channel Graph ID. * @param reference.teamId - Team Graph ID. */ function channelPath(reference: { channelId: string; teamId: string }) { return `/teams/${encodeURIComponent(reference.teamId)}/channels/${encodeURIComponent(reference.channelId)}` } /** * Resolves a team name or ID to its Graph ID. * * @param secret - Microsoft account secret. * @param reference - Team display name or ID. */ async function resolveTeamId( secret: Record, reference: string, ) { const teams = await listTeamsGraphCollection( getTeamsGraphApi(secret).request, "/me/joinedTeams", GRAPH_TEAM_SCHEMA, MAX_LIMIT, ) const idMatch = teams.find((team) => team.teamId === reference) if (idMatch) return idMatch.teamId const matches = teams.filter( (team) => team.displayName.trim().toLowerCase() === reference.trim().toLowerCase(), ) if (matches.length === 1) return matches[0]!.teamId if (matches.length > 1) throw new Error(`Microsoft team "${reference}" is ambiguous; use its ID.`) throw new Error(`Microsoft team "${reference}" was not found.`) } /** * Resolves team and channel names or IDs to Graph IDs. * * @param secret - Microsoft account secret. * @param input - Team and channel references. */ async function resolveChannelReference( secret: Record, input: z.output, ) { const teamId = await resolveTeamId(secret, input.team) const channels = await listTeamsGraphCollection( getTeamsGraphApi(secret).request, `/teams/${encodeURIComponent(teamId)}/channels`, GRAPH_CHANNEL_SCHEMA, MAX_LIMIT, ) const idMatch = channels.find( (channel) => channel.channelId === input.channel, ) if (idMatch) return { channelId: idMatch.channelId, teamId } const matches = channels.filter( (channel) => channel.displayName.trim().toLowerCase() === input.channel.trim().toLowerCase(), ) if (matches.length === 1) return { channelId: matches[0]!.channelId, teamId } if (matches.length > 1) throw new Error( `Microsoft Teams channel "${input.channel}" is ambiguous; use its ID.`, ) throw new Error( `Microsoft Teams channel "${input.channel}" was not found in team "${input.team}".`, ) }