| import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams"; |
| import type { MSTeamsConversationStore } from "./conversation-store.js"; |
| import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js"; |
| import { normalizeMSTeamsConversationId } from "./inbound.js"; |
| import type { MSTeamsAdapter } from "./messenger.js"; |
| import { createMSTeamsMessageHandler } from "./monitor-handler/message-handler.js"; |
| import type { MSTeamsMonitorLogger } from "./monitor-types.js"; |
| import { getPendingUpload, removePendingUpload } from "./pending-uploads.js"; |
| import type { MSTeamsPollStore } from "./polls.js"; |
| import { withRevokedProxyFallback } from "./revoked-context.js"; |
| import type { MSTeamsTurnContext } from "./sdk-types.js"; |
|
|
| export type MSTeamsAccessTokenProvider = { |
| getAccessToken: (scope: string) => Promise<string>; |
| }; |
|
|
| export type MSTeamsActivityHandler = { |
| onMessage: ( |
| handler: (context: unknown, next: () => Promise<void>) => Promise<void>, |
| ) => MSTeamsActivityHandler; |
| onMembersAdded: ( |
| handler: (context: unknown, next: () => Promise<void>) => Promise<void>, |
| ) => MSTeamsActivityHandler; |
| run?: (context: unknown) => Promise<void>; |
| }; |
|
|
| export type MSTeamsMessageHandlerDeps = { |
| cfg: OpenClawConfig; |
| runtime: RuntimeEnv; |
| appId: string; |
| adapter: MSTeamsAdapter; |
| tokenProvider: MSTeamsAccessTokenProvider; |
| textLimit: number; |
| mediaMaxBytes: number; |
| conversationStore: MSTeamsConversationStore; |
| pollStore: MSTeamsPollStore; |
| log: MSTeamsMonitorLogger; |
| }; |
|
|
| |
| |
| |
| async function handleFileConsentInvoke( |
| context: MSTeamsTurnContext, |
| log: MSTeamsMonitorLogger, |
| ): Promise<boolean> { |
| const expiredUploadMessage = |
| "The file upload request has expired. Please try sending the file again."; |
| const activity = context.activity; |
| if (activity.type !== "invoke" || activity.name !== "fileConsent/invoke") { |
| return false; |
| } |
|
|
| const consentResponse = parseFileConsentInvoke(activity); |
| if (!consentResponse) { |
| log.debug?.("invalid file consent invoke", { value: activity.value }); |
| return false; |
| } |
|
|
| const uploadId = |
| typeof consentResponse.context?.uploadId === "string" |
| ? consentResponse.context.uploadId |
| : undefined; |
| const pendingFile = getPendingUpload(uploadId); |
| if (pendingFile) { |
| const pendingConversationId = normalizeMSTeamsConversationId(pendingFile.conversationId); |
| const invokeConversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? ""); |
| if (!invokeConversationId || pendingConversationId !== invokeConversationId) { |
| log.info("file consent conversation mismatch", { |
| uploadId, |
| expectedConversationId: pendingConversationId, |
| receivedConversationId: invokeConversationId || undefined, |
| }); |
| if (consentResponse.action === "accept") { |
| await context.sendActivity(expiredUploadMessage); |
| } |
| return true; |
| } |
| } |
|
|
| if (consentResponse.action === "accept" && consentResponse.uploadInfo) { |
| if (pendingFile) { |
| log.debug?.("user accepted file consent, uploading", { |
| uploadId, |
| filename: pendingFile.filename, |
| size: pendingFile.buffer.length, |
| }); |
|
|
| try { |
| |
| await uploadToConsentUrl({ |
| url: consentResponse.uploadInfo.uploadUrl, |
| buffer: pendingFile.buffer, |
| contentType: pendingFile.contentType, |
| }); |
|
|
| |
| const fileInfoCard = buildFileInfoCard({ |
| filename: consentResponse.uploadInfo.name, |
| contentUrl: consentResponse.uploadInfo.contentUrl, |
| uniqueId: consentResponse.uploadInfo.uniqueId, |
| fileType: consentResponse.uploadInfo.fileType, |
| }); |
|
|
| await context.sendActivity({ |
| type: "message", |
| attachments: [fileInfoCard], |
| }); |
|
|
| log.info("file upload complete", { |
| uploadId, |
| filename: consentResponse.uploadInfo.name, |
| uniqueId: consentResponse.uploadInfo.uniqueId, |
| }); |
| } catch (err) { |
| log.debug?.("file upload failed", { uploadId, error: String(err) }); |
| await context.sendActivity(`File upload failed: ${String(err)}`); |
| } finally { |
| removePendingUpload(uploadId); |
| } |
| } else { |
| log.debug?.("pending file not found for consent", { uploadId }); |
| await context.sendActivity(expiredUploadMessage); |
| } |
| } else { |
| |
| log.debug?.("user declined file consent", { uploadId }); |
| removePendingUpload(uploadId); |
| } |
|
|
| return true; |
| } |
|
|
| export function registerMSTeamsHandlers<T extends MSTeamsActivityHandler>( |
| handler: T, |
| deps: MSTeamsMessageHandlerDeps, |
| ): T { |
| const handleTeamsMessage = createMSTeamsMessageHandler(deps); |
|
|
| |
| const originalRun = handler.run; |
| if (originalRun) { |
| handler.run = async (context: unknown) => { |
| const ctx = context as MSTeamsTurnContext; |
| |
| if (ctx.activity?.type === "invoke" && ctx.activity?.name === "fileConsent/invoke") { |
| |
| await ctx.sendActivity({ type: "invokeResponse", value: { status: 200 } }); |
|
|
| try { |
| await withRevokedProxyFallback({ |
| run: async () => await handleFileConsentInvoke(ctx, deps.log), |
| onRevoked: async () => true, |
| onRevokedLog: () => { |
| deps.log.debug?.( |
| "turn context revoked during file consent invoke; skipping delayed response", |
| ); |
| }, |
| }); |
| } catch (err) { |
| deps.log.debug?.("file consent handler error", { error: String(err) }); |
| } |
| return; |
| } |
| return originalRun.call(handler, context); |
| }; |
| } |
|
|
| handler.onMessage(async (context, next) => { |
| try { |
| await handleTeamsMessage(context as MSTeamsTurnContext); |
| } catch (err) { |
| deps.runtime.error?.(`msteams handler failed: ${String(err)}`); |
| } |
| await next(); |
| }); |
|
|
| handler.onMembersAdded(async (context, next) => { |
| const membersAdded = (context as MSTeamsTurnContext).activity?.membersAdded ?? []; |
| for (const member of membersAdded) { |
| if (member.id !== (context as MSTeamsTurnContext).activity?.recipient?.id) { |
| deps.log.debug?.("member added", { member: member.id }); |
| |
| } |
| } |
| await next(); |
| }); |
|
|
| return handler; |
| } |
|
|