/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { Attachment, Channels } from '@microsoft/agents-activity' import { debug } from '@microsoft/agents-activity/logger' import { ConnectorClient } from '../connector-client' import { InputFile, InputFileDownloader } from './inputFileDownloader' import { TurnContext } from '../turnContext' import { TurnState } from './turnState' import axios, { AxiosInstance } from 'axios' import { z } from 'zod' const logger = debug('agents:M365AttachmentDownloader') /** * Downloads attachments from Teams and M365 using the bots access token. */ export class M365AttachmentDownloader implements InputFileDownloader { private _httpClient: AxiosInstance private _stateKey: string public constructor (stateKey: string = 'inputFiles') { this._httpClient = axios.create() this._stateKey = stateKey } /** * Download any files relative to the current user's input. * * @param {TurnContext} context Context for the current turn of conversation. * @returns {Promise} Promise that resolves to an array of downloaded input files. */ public async downloadFiles (context: TurnContext): Promise { if (context.activity.channelId !== Channels.Msteams && context.activity.channelId !== Channels.M365Copilot) { return Promise.resolve([]) } // Filter out HTML attachments const attachments = context.activity.attachments?.filter((a) => a.contentType && !a.contentType.startsWith('text/html')) if (!attachments || attachments.length === 0) { return Promise.resolve([]) } const connectorClient : ConnectorClient = context.turnState.get(context.adapter.ConnectorClientKey) this._httpClient.defaults.headers = connectorClient.axiosInstance.defaults.headers const files: InputFile[] = [] for (const attachment of attachments) { const file = await this.downloadFile(attachment) if (file) { files.push(file) } } return files } /** * @private * @param {Attachment} attachment - Attachment to download. * @returns {Promise} - Promise that resolves to the downloaded input file. */ private async downloadFile (attachment: Attachment): Promise { let inputFile: InputFile | undefined if (attachment.contentUrl && attachment.contentUrl.startsWith('https://')) { try { const contentSchema = z.object({ downloadUrl: z.string().url() }) const parsed = contentSchema.safeParse(attachment.content) const downloadUrl = parsed.success ? parsed.data.downloadUrl : attachment.contentUrl const response = await this._httpClient.get(downloadUrl, { responseType: 'arraybuffer' }) const content = Buffer.from(response.data, 'binary') const contentType = response.headers['content-type'] || 'application/octet-stream' inputFile = { content, contentType, contentUrl: attachment.contentUrl } } catch (error) { logger.error(`Failed to download Teams attachment: ${error}`) return undefined } } else { if (!attachment.content) { logger.error('Attachment missing content') return undefined } if (!(attachment.content instanceof ArrayBuffer) && !Buffer.isBuffer(attachment.content)) { logger.error('Attachment content is not ArrayBuffer or Buffer') return undefined } inputFile = { content: Buffer.from(attachment.content as ArrayBuffer), contentType: attachment.contentType, contentUrl: attachment.contentUrl } } return inputFile } /** * Downloads files from the attachments in the current turn context and stores them in state. * * @param context The turn context containing the activity with attachments. * @param state The turn state to store the files in. * @returns A promise that resolves when the downloaded files are stored. */ public async downloadAndStoreFiles (context: TurnContext, state: TState): Promise { const files = await this.downloadFiles(context) state.setValue(this._stateKey, files) } } /** * @deprecated Use {@link M365AttachmentDownloader} from @microsoft/agents-hosting instead. */ export class TeamsAttachmentDownloader extends M365AttachmentDownloader {}