mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 06:41:08 +00:00
fix(msteams): download DM inline images via Graph API (#52212)
Fix three bugs preventing inline image downloads in Teams 1:1 DM chats: wrong conversation ID format for Graph API, missing media URL extraction, and incorrect content type detection. Fixes #24797 Thanks @Ted-developer
This commit is contained in:
274
extensions/msteams/src/attachments/graph.test.ts
Normal file
274
extensions/msteams/src/attachments/graph.test.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock shared.js to avoid transitive runtime-api imports that pull in uninstalled packages.
|
||||
vi.mock("./shared.js", () => ({
|
||||
applyAuthorizationHeaderForUrl: vi.fn(),
|
||||
GRAPH_ROOT: "https://graph.microsoft.com/v1.0",
|
||||
inferPlaceholder: vi.fn(({ contentType }: { contentType?: string }) =>
|
||||
contentType?.startsWith("image/") ? "[image]" : "[file]",
|
||||
),
|
||||
isRecord: (v: unknown) => typeof v === "object" && v !== null && !Array.isArray(v),
|
||||
isUrlAllowed: vi.fn(() => true),
|
||||
normalizeContentType: vi.fn((ct: string | null | undefined) => ct ?? undefined),
|
||||
resolveMediaSsrfPolicy: vi.fn(() => undefined),
|
||||
resolveAttachmentFetchPolicy: vi.fn(() => ({ allowHosts: ["*"], authAllowHosts: ["*"] })),
|
||||
resolveRequestUrl: vi.fn((input: string) => input),
|
||||
safeFetchWithPolicy: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../runtime-api.js", () => ({
|
||||
fetchWithSsrFGuard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
getMSTeamsRuntime: vi.fn(() => ({
|
||||
media: {
|
||||
detectMime: vi.fn(async () => "image/png"),
|
||||
},
|
||||
channel: {
|
||||
media: {
|
||||
saveMediaBuffer: vi.fn(async (_buf: Buffer, ct: string) => ({
|
||||
path: "/tmp/saved.png",
|
||||
contentType: ct ?? "image/png",
|
||||
})),
|
||||
},
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./download.js", () => ({
|
||||
downloadMSTeamsAttachments: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
vi.mock("./remote-media.js", () => ({
|
||||
downloadAndStoreMSTeamsRemoteMedia: vi.fn(),
|
||||
}));
|
||||
|
||||
import { fetchWithSsrFGuard } from "../../runtime-api.js";
|
||||
import { downloadMSTeamsGraphMedia } from "./graph.js";
|
||||
|
||||
function mockFetchResponse(body: unknown, status = 200) {
|
||||
const bodyStr = typeof body === "string" ? body : JSON.stringify(body);
|
||||
return new Response(bodyStr, { status, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
|
||||
function mockBinaryResponse(data: Uint8Array, status = 200) {
|
||||
return new Response(Buffer.from(data) as BodyInit, { status });
|
||||
}
|
||||
|
||||
describe("downloadMSTeamsGraphMedia hosted content $value fallback", () => {
|
||||
it("fetches $value endpoint when contentBytes is null but item.id exists", async () => {
|
||||
const imageBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); // PNG magic bytes
|
||||
|
||||
const fetchCalls: string[] = [];
|
||||
|
||||
vi.mocked(fetchWithSsrFGuard).mockImplementation(async (params: { url: string }) => {
|
||||
fetchCalls.push(params.url);
|
||||
const url = params.url;
|
||||
|
||||
// Main message fetch
|
||||
if (url.endsWith("/messages/msg-1") && !url.includes("hostedContents")) {
|
||||
return {
|
||||
response: mockFetchResponse({ body: {}, attachments: [] }),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
// hostedContents collection
|
||||
if (url.endsWith("/hostedContents")) {
|
||||
return {
|
||||
response: mockFetchResponse({
|
||||
value: [{ id: "hosted-123", contentType: "image/png", contentBytes: null }],
|
||||
}),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
// $value endpoint (the fallback being tested)
|
||||
if (url.includes("/hostedContents/hosted-123/$value")) {
|
||||
return {
|
||||
response: mockBinaryResponse(imageBytes),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
// attachments collection
|
||||
if (url.endsWith("/attachments")) {
|
||||
return {
|
||||
response: mockFetchResponse({ value: [] }),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
return {
|
||||
response: mockFetchResponse({}, 404),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await downloadMSTeamsGraphMedia({
|
||||
messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-1",
|
||||
tokenProvider: { getAccessToken: vi.fn(async () => "test-token") },
|
||||
maxBytes: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
// Verify the $value endpoint was fetched
|
||||
const valueCall = fetchCalls.find((u) => u.includes("/hostedContents/hosted-123/$value"));
|
||||
expect(valueCall).toBeDefined();
|
||||
expect(result.media.length).toBeGreaterThan(0);
|
||||
expect(result.hostedCount).toBe(1);
|
||||
});
|
||||
|
||||
it("skips hosted content when contentBytes is null and id is missing", async () => {
|
||||
vi.mocked(fetchWithSsrFGuard).mockImplementation(async (params: { url: string }) => {
|
||||
const url = params.url;
|
||||
if (url.endsWith("/messages/msg-2") && !url.includes("hostedContents")) {
|
||||
return {
|
||||
response: mockFetchResponse({ body: {}, attachments: [] }),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
if (url.endsWith("/hostedContents")) {
|
||||
return {
|
||||
response: mockFetchResponse({
|
||||
value: [{ contentType: "image/png", contentBytes: null }],
|
||||
}),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
if (url.endsWith("/attachments")) {
|
||||
return {
|
||||
response: mockFetchResponse({ value: [] }),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
return {
|
||||
response: mockFetchResponse({}, 404),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await downloadMSTeamsGraphMedia({
|
||||
messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-2",
|
||||
tokenProvider: { getAccessToken: vi.fn(async () => "test-token") },
|
||||
maxBytes: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
// No media because there's no id to fetch $value from and no contentBytes
|
||||
expect(result.media).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("skips $value content when Content-Length exceeds maxBytes", async () => {
|
||||
const fetchCalls: string[] = [];
|
||||
|
||||
vi.mocked(fetchWithSsrFGuard).mockImplementation(async (params: { url: string }) => {
|
||||
fetchCalls.push(params.url);
|
||||
const url = params.url;
|
||||
if (url.endsWith("/messages/msg-cl") && !url.includes("hostedContents")) {
|
||||
return {
|
||||
response: mockFetchResponse({ body: {}, attachments: [] }),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
if (url.endsWith("/hostedContents")) {
|
||||
return {
|
||||
response: mockFetchResponse({
|
||||
value: [{ id: "hosted-big", contentType: "image/png", contentBytes: null }],
|
||||
}),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
if (url.includes("/hostedContents/hosted-big/$value")) {
|
||||
// Return a response whose Content-Length exceeds maxBytes
|
||||
const data = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
return {
|
||||
response: new Response(Buffer.from(data) as BodyInit, {
|
||||
status: 200,
|
||||
headers: { "content-length": "999999999" },
|
||||
}),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
if (url.endsWith("/attachments")) {
|
||||
return {
|
||||
response: mockFetchResponse({ value: [] }),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
return {
|
||||
response: mockFetchResponse({}, 404),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await downloadMSTeamsGraphMedia({
|
||||
messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-cl",
|
||||
tokenProvider: { getAccessToken: vi.fn(async () => "test-token") },
|
||||
maxBytes: 1024, // 1 KB limit
|
||||
});
|
||||
|
||||
// $value was fetched but skipped due to Content-Length exceeding maxBytes
|
||||
const valueCall = fetchCalls.find((u) => u.includes("/hostedContents/hosted-big/$value"));
|
||||
expect(valueCall).toBeDefined();
|
||||
expect(result.media).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("uses inline contentBytes when available instead of $value", async () => {
|
||||
const fetchCalls: string[] = [];
|
||||
const base64Png = Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString("base64");
|
||||
|
||||
vi.mocked(fetchWithSsrFGuard).mockImplementation(async (params: { url: string }) => {
|
||||
fetchCalls.push(params.url);
|
||||
const url = params.url;
|
||||
if (url.endsWith("/messages/msg-3") && !url.includes("hostedContents")) {
|
||||
return {
|
||||
response: mockFetchResponse({ body: {}, attachments: [] }),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
if (url.endsWith("/hostedContents")) {
|
||||
return {
|
||||
response: mockFetchResponse({
|
||||
value: [{ id: "hosted-456", contentType: "image/png", contentBytes: base64Png }],
|
||||
}),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
if (url.endsWith("/attachments")) {
|
||||
return {
|
||||
response: mockFetchResponse({ value: [] }),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
}
|
||||
return {
|
||||
response: mockFetchResponse({}, 404),
|
||||
release: async () => {},
|
||||
finalUrl: params.url,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await downloadMSTeamsGraphMedia({
|
||||
messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-3",
|
||||
tokenProvider: { getAccessToken: vi.fn(async () => "test-token") },
|
||||
maxBytes: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
// Should NOT have fetched $value since contentBytes was available
|
||||
const valueCall = fetchCalls.find((u) => u.includes("/$value"));
|
||||
expect(valueCall).toBeUndefined();
|
||||
expect(result.media.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -194,13 +194,44 @@ async function downloadGraphHostedContent(params: {
|
||||
const out: MSTeamsInboundMedia[] = [];
|
||||
for (const item of hosted.items) {
|
||||
const contentBytes = typeof item.contentBytes === "string" ? item.contentBytes : "";
|
||||
if (!contentBytes) {
|
||||
continue;
|
||||
}
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = Buffer.from(contentBytes, "base64");
|
||||
} catch {
|
||||
if (contentBytes) {
|
||||
try {
|
||||
buffer = Buffer.from(contentBytes, "base64");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
} else if (item.id) {
|
||||
// contentBytes not inline — fetch from the individual $value endpoint.
|
||||
try {
|
||||
const valueUrl = `${params.messageUrl}/hostedContents/${encodeURIComponent(item.id)}/$value`;
|
||||
const { response: valRes, release } = await fetchWithSsrFGuard({
|
||||
url: valueUrl,
|
||||
fetchImpl: params.fetchFn ?? fetch,
|
||||
init: {
|
||||
headers: { Authorization: `Bearer ${params.accessToken}` },
|
||||
},
|
||||
policy: params.ssrfPolicy,
|
||||
auditContext: "msteams.graph.hostedContent.value",
|
||||
});
|
||||
try {
|
||||
if (!valRes.ok) {
|
||||
continue;
|
||||
}
|
||||
// Check Content-Length before buffering to avoid RSS spikes on large files.
|
||||
const cl = valRes.headers.get("content-length");
|
||||
if (cl && Number(cl) > params.maxBytes) {
|
||||
continue;
|
||||
}
|
||||
const ab = await valRes.arrayBuffer();
|
||||
buffer = Buffer.from(ab);
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (buffer.byteLength > params.maxBytes) {
|
||||
|
||||
@@ -108,6 +108,30 @@ export function stripMSTeamsMentionTags(text: string): string {
|
||||
return text.replace(/<at[^>]*>.*?<\/at>/gi, "").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bot Framework uses 'a:xxx' conversation IDs for personal chats, but Graph API
|
||||
* requires the '19:{userId}_{botAppId}@unq.gbl.spaces' format.
|
||||
*
|
||||
* This is the documented Graph API format for 1:1 chat thread IDs between a user
|
||||
* and a bot/app. See Microsoft docs "Get chat between user and app":
|
||||
* https://learn.microsoft.com/en-us/graph/api/userscopeteamsappinstallation-get-chat
|
||||
*
|
||||
* The format is only synthesized when the Bot Framework conversation ID starts with
|
||||
* 'a:' (the opaque format used by BF but not recognized by Graph). If the ID already
|
||||
* has the '19:...' Graph format, it is passed through unchanged.
|
||||
*/
|
||||
export function translateMSTeamsDmConversationIdForGraph(params: {
|
||||
isDirectMessage: boolean;
|
||||
conversationId: string;
|
||||
aadObjectId?: string | null;
|
||||
appId?: string | null;
|
||||
}): string {
|
||||
const { isDirectMessage, conversationId, aadObjectId, appId } = params;
|
||||
return isDirectMessage && conversationId.startsWith("a:") && aadObjectId && appId
|
||||
? `19:${aadObjectId}_${appId}@unq.gbl.spaces`
|
||||
: conversationId;
|
||||
}
|
||||
|
||||
export function wasMSTeamsBotMentioned(activity: MentionableActivity): boolean {
|
||||
const botId = activity.recipient?.id;
|
||||
if (!botId) {
|
||||
|
||||
75
extensions/msteams/src/monitor-handler/inbound-media.test.ts
Normal file
75
extensions/msteams/src/monitor-handler/inbound-media.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../attachments.js", () => ({
|
||||
downloadMSTeamsAttachments: vi.fn(async () => []),
|
||||
downloadMSTeamsGraphMedia: vi.fn(async () => ({ media: [] })),
|
||||
buildMSTeamsGraphMessageUrls: vi.fn(() => [
|
||||
"https://graph.microsoft.com/v1.0/chats/c/messages/m",
|
||||
]),
|
||||
}));
|
||||
|
||||
import {
|
||||
downloadMSTeamsAttachments,
|
||||
downloadMSTeamsGraphMedia,
|
||||
buildMSTeamsGraphMessageUrls,
|
||||
} from "../attachments.js";
|
||||
import { resolveMSTeamsInboundMedia } from "./inbound-media.js";
|
||||
|
||||
const baseParams = {
|
||||
maxBytes: 1024 * 1024,
|
||||
tokenProvider: { getAccessToken: vi.fn(async () => "token") },
|
||||
conversationType: "personal",
|
||||
conversationId: "19:user_bot@unq.gbl.spaces",
|
||||
activity: { id: "msg-1", replyToId: undefined, channelData: {} },
|
||||
log: { debug: vi.fn() },
|
||||
};
|
||||
|
||||
describe("resolveMSTeamsInboundMedia graph fallback trigger", () => {
|
||||
it("triggers Graph fallback when some attachments are text/html (some() behavior)", async () => {
|
||||
vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]);
|
||||
vi.mocked(downloadMSTeamsGraphMedia).mockResolvedValue({
|
||||
media: [{ path: "/tmp/img.png", contentType: "image/png", placeholder: "[image]" }],
|
||||
});
|
||||
|
||||
await resolveMSTeamsInboundMedia({
|
||||
...baseParams,
|
||||
attachments: [
|
||||
{ contentType: "text/html", content: "<div><img src='x'/></div>" },
|
||||
{ contentType: "image/png", contentUrl: "https://example.com/img.png" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(buildMSTeamsGraphMessageUrls).toHaveBeenCalled();
|
||||
expect(downloadMSTeamsGraphMedia).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does NOT trigger Graph fallback when no attachments are text/html", async () => {
|
||||
vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]);
|
||||
vi.mocked(downloadMSTeamsGraphMedia).mockClear();
|
||||
vi.mocked(buildMSTeamsGraphMessageUrls).mockClear();
|
||||
|
||||
await resolveMSTeamsInboundMedia({
|
||||
...baseParams,
|
||||
attachments: [
|
||||
{ contentType: "image/png", contentUrl: "https://example.com/img.png" },
|
||||
{ contentType: "application/pdf", contentUrl: "https://example.com/doc.pdf" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does NOT trigger Graph fallback when direct download succeeds", async () => {
|
||||
vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([
|
||||
{ path: "/tmp/img.png", contentType: "image/png", placeholder: "[image]" },
|
||||
]);
|
||||
vi.mocked(downloadMSTeamsGraphMedia).mockClear();
|
||||
|
||||
await resolveMSTeamsInboundMedia({
|
||||
...baseParams,
|
||||
attachments: [{ contentType: "text/html", content: "<div><img src='x'/></div>" }],
|
||||
});
|
||||
|
||||
expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -52,11 +52,11 @@ export async function resolveMSTeamsInboundMedia(params: {
|
||||
});
|
||||
|
||||
if (mediaList.length === 0) {
|
||||
const onlyHtmlAttachments =
|
||||
const hasHtmlAttachment =
|
||||
attachments.length > 0 &&
|
||||
attachments.every((att) => String(att.contentType ?? "").startsWith("text/html"));
|
||||
attachments.some((att) => String(att.contentType ?? "").startsWith("text/html"));
|
||||
|
||||
if (onlyHtmlAttachments) {
|
||||
if (hasHtmlAttachment) {
|
||||
const messageUrls = buildMSTeamsGraphMessageUrls({
|
||||
conversationType,
|
||||
conversationId,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { translateMSTeamsDmConversationIdForGraph } from "../inbound.js";
|
||||
|
||||
describe("translateMSTeamsDmConversationIdForGraph", () => {
|
||||
it("translates a: conversation ID to Graph format for DMs", () => {
|
||||
const result = translateMSTeamsDmConversationIdForGraph({
|
||||
isDirectMessage: true,
|
||||
conversationId: "a:1abc2def3",
|
||||
aadObjectId: "user-aad-id",
|
||||
appId: "bot-app-id",
|
||||
});
|
||||
expect(result).toBe("19:user-aad-id_bot-app-id@unq.gbl.spaces");
|
||||
});
|
||||
|
||||
it("passes through non-a: conversation IDs unchanged", () => {
|
||||
const result = translateMSTeamsDmConversationIdForGraph({
|
||||
isDirectMessage: true,
|
||||
conversationId: "19:existing@unq.gbl.spaces",
|
||||
aadObjectId: "user-aad-id",
|
||||
appId: "bot-app-id",
|
||||
});
|
||||
expect(result).toBe("19:existing@unq.gbl.spaces");
|
||||
});
|
||||
|
||||
it("passes through when aadObjectId is missing", () => {
|
||||
const result = translateMSTeamsDmConversationIdForGraph({
|
||||
isDirectMessage: true,
|
||||
conversationId: "a:1abc2def3",
|
||||
aadObjectId: null,
|
||||
appId: "bot-app-id",
|
||||
});
|
||||
expect(result).toBe("a:1abc2def3");
|
||||
});
|
||||
|
||||
it("passes through when appId is missing", () => {
|
||||
const result = translateMSTeamsDmConversationIdForGraph({
|
||||
isDirectMessage: true,
|
||||
conversationId: "a:1abc2def3",
|
||||
aadObjectId: "user-aad-id",
|
||||
appId: null,
|
||||
});
|
||||
expect(result).toBe("a:1abc2def3");
|
||||
});
|
||||
|
||||
it("passes through for non-DM conversations even with a: prefix", () => {
|
||||
const result = translateMSTeamsDmConversationIdForGraph({
|
||||
isDirectMessage: false,
|
||||
conversationId: "a:1abc2def3",
|
||||
aadObjectId: "user-aad-id",
|
||||
appId: "bot-app-id",
|
||||
});
|
||||
expect(result).toBe("a:1abc2def3");
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
normalizeMSTeamsConversationId,
|
||||
parseMSTeamsActivityTimestamp,
|
||||
stripMSTeamsMentionTags,
|
||||
translateMSTeamsDmConversationIdForGraph,
|
||||
wasMSTeamsBotMentioned,
|
||||
} from "../inbound.js";
|
||||
import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js";
|
||||
@@ -435,6 +436,13 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const graphConversationId = translateMSTeamsDmConversationIdForGraph({
|
||||
isDirectMessage,
|
||||
conversationId,
|
||||
aadObjectId: from.aadObjectId,
|
||||
appId,
|
||||
});
|
||||
|
||||
const mediaList = await resolveMSTeamsInboundMedia({
|
||||
attachments,
|
||||
htmlSummary: htmlSummary ?? undefined,
|
||||
@@ -443,7 +451,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
allowHosts: msteamsCfg?.mediaAllowHosts,
|
||||
authAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
|
||||
conversationType,
|
||||
conversationId,
|
||||
conversationId: graphConversationId,
|
||||
conversationMessageId: conversationMessageId ?? undefined,
|
||||
activity: {
|
||||
id: activity.id,
|
||||
|
||||
Reference in New Issue
Block a user