Skip to content

fix(msteams): download DM inline images via Graph API#52212

Merged
BradGroux merged 1 commit intoopenclaw:mainfrom
Ted-developer:fix/msteams-dm-image-download
Apr 3, 2026
Merged

fix(msteams): download DM inline images via Graph API#52212
BradGroux merged 1 commit intoopenclaw:mainfrom
Ted-developer:fix/msteams-dm-image-download

Conversation

@Ted-developer
Copy link
Copy Markdown
Contributor

Summary

Fixes #24797 — Images sent in personal (1:1) bot DM chats are never downloaded due to three bugs in the media download pipeline.

Changes

Bug 1: Wrong conversation ID format for Graph API

File: extensions/msteams/src/monitor-handler/message-handler.ts

Bot Framework uses a:xxx conversation IDs for personal chats, but the Graph API requires 19:{userId}_{botAppId}@unq.gbl.spaces. Added translation before calling resolveMSTeamsInboundMedia.

Bug 2: Graph fallback blocked by non-HTML attachments

File: extensions/msteams/src/monitor-handler/inbound-media.ts

Changed every() to some() — trigger Graph fallback when any attachment is HTML, not only when all are. Image messages include non-HTML metadata alongside HTML content.

Bug 3: Hosted content bytes not returned inline

File: extensions/msteams/src/attachments/graph.ts

When contentBytes is null in the /hostedContents list response (common in bot DM chats), fall back to fetching binary data from the individual /hostedContents/{id}/$value endpoint.

Testing

  • Verified with debug logs that the Graph fallback path is now reached
  • Manual Graph API calls confirmed the conversation ID translation resolves the 404
  • All three fixes together resolve DM image download completely

@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: S labels Mar 22, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR fixes three compounding bugs that prevented inline images from being downloaded in MS Teams 1:1 bot DM chats. Together the fixes wire up a complete Graph API fallback path: the conversation ID is now translated to the format the Graph API expects, the Graph fallback is now triggered even when mixed HTML/non-HTML attachments are present, and missing contentBytes in the hosted-content list response is handled by fetching binary data from the individual $value endpoint.

  • message-handler.ts – Before calling resolveMSTeamsInboundMedia, a:xxx-format conversation IDs are translated to 19:{aadObjectId}_{appId}@unq.gbl.spaces for personal DMs; falls back to the original ID when aadObjectId or appId is unavailable.
  • inbound-media.tsevery()some() so that the Graph fallback is triggered when any attachment is text/html, not only when all attachments are HTML; the variable name onlyHtmlAttachments is now misleading and should be renamed (e.g. hasAnyHtmlAttachment).
  • graph.ts – When contentBytes is absent from the /hostedContents list response, a second request to /hostedContents/{id}/$value is made; resource (release) handling is correct in all error and continue paths.

Confidence Score: 5/5

  • This PR is safe to merge; it corrects three clearly identified bugs in an additive, well-guarded way without touching unrelated paths.
  • All three changes are narrowly scoped fixes. The Graph fallback is already guarded by mediaList.length === 0, so the some() change cannot regress non-DM message handling. The conversation-ID translation is guarded by multiple conditions and degrades gracefully. The $value fallback in graph.ts has correct try-finally resource management. The only non-blocking issue is the now-inaccurate variable name onlyHtmlAttachments.
  • No files require special attention beyond the minor rename suggestion in inbound-media.ts.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/inbound-media.ts
Line: 55-59

Comment:
**Misleading variable name after `every``some` change**

The variable `onlyHtmlAttachments` no longer reflects its meaning. After the change to `some()`, the variable is `true` whenever *any* attachment is HTML — not only when all attachments are HTML. A future reader seeing `onlyHtmlAttachments` will expect an `every()`-style guard and may re-introduce the original bug.

```suggestion
      attachments.some((att) => String(att.contentType ?? "").startsWith("text/html"));

    if (onlyHtmlAttachments) {
```

Rename the variable and the condition name:
```
    const hasAnyHtmlAttachment =
      attachments.length > 0 &&
      attachments.some((att) => String(att.contentType ?? "").startsWith("text/html"));

    if (hasAnyHtmlAttachment) {
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(msteams): download DM inline images ..." | Re-trigger Greptile

Comment on lines 55 to 59
const onlyHtmlAttachments =
attachments.length > 0 &&
attachments.every((att) => String(att.contentType ?? "").startsWith("text/html"));
attachments.some((att) => String(att.contentType ?? "").startsWith("text/html"));

if (onlyHtmlAttachments) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading variable name after everysome change

The variable onlyHtmlAttachments no longer reflects its meaning. After the change to some(), the variable is true whenever any attachment is HTML — not only when all attachments are HTML. A future reader seeing onlyHtmlAttachments will expect an every()-style guard and may re-introduce the original bug.

Suggested change
const onlyHtmlAttachments =
attachments.length > 0 &&
attachments.every((att) => String(att.contentType ?? "").startsWith("text/html"));
attachments.some((att) => String(att.contentType ?? "").startsWith("text/html"));
if (onlyHtmlAttachments) {
attachments.some((att) => String(att.contentType ?? "").startsWith("text/html"));
if (onlyHtmlAttachments) {

Rename the variable and the condition name:

    const hasAnyHtmlAttachment =
      attachments.length > 0 &&
      attachments.some((att) => String(att.contentType ?? "").startsWith("text/html"));

    if (hasAnyHtmlAttachment) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/inbound-media.ts
Line: 55-59

Comment:
**Misleading variable name after `every``some` change**

The variable `onlyHtmlAttachments` no longer reflects its meaning. After the change to `some()`, the variable is `true` whenever *any* attachment is HTML — not only when all attachments are HTML. A future reader seeing `onlyHtmlAttachments` will expect an `every()`-style guard and may re-introduce the original bug.

```suggestion
      attachments.some((att) => String(att.contentType ?? "").startsWith("text/html"));

    if (onlyHtmlAttachments) {
```

Rename the variable and the condition name:
```
    const hasAnyHtmlAttachment =
      attachments.length > 0 &&
      attachments.some((att) => String(att.contentType ?? "").startsWith("text/html"));

    if (hasAnyHtmlAttachment) {
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5466fb7c22

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +449 to +455
const graphConversationId =
isDirectMessage &&
conversationId.startsWith("a:") &&
from.aadObjectId &&
appId
? `19:${from.aadObjectId}_${appId}@unq.gbl.spaces`
: conversationId;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve DM Graph chat IDs instead of synthesizing them

For personal chats whose Graph ID is not exactly 19:{userAadId}_{appId}@unq.gbl.spaces, this branch still builds /chats/{chatId} URLs with the wrong ID, so inline-media fetches keep 404ing. We already treat these IDs as opaque elsewhere: extensions/msteams/src/graph-upload.ts resolves them via Graph because valid DM chat IDs can be @thread.tacv2 or other forms, and extensions/msteams/src/send.test.ts has fixtures for that. Reusing the existing resolver/cache here would avoid leaving those DMs broken.

Useful? React with 👍 / 👎.

Comment on lines +221 to +222
const ab = await valRes.arrayBuffer();
buffer = Buffer.from(ab);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce maxBytes before buffering hosted content values

This new $value path loads the entire hosted-content response into memory with arrayBuffer() and only rejects oversized files afterwards. In DM chats where Graph omits contentBytes for a large hosted file, the gateway now allocates the full payload before discarding it, which can spike RSS or crash the worker; the existing remote-media path in extensions/msteams/src/attachments/remote-media.ts avoids that by applying the size cap while reading.

Useful? React with 👍 / 👎.

@Ted-developer Ted-developer force-pushed the fix/msteams-dm-image-download branch from 5466fb7 to 4b54ca5 Compare March 22, 2026 11:44
@Ted-developer Ted-developer force-pushed the fix/msteams-dm-image-download branch from 4b54ca5 to f94392d Compare March 22, 2026 11:47
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f94392d329

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +60 to +62
return isDirectMessage && conversationId.startsWith("a:") && aadObjectId && appId
? `19:${aadObjectId}_${appId}@unq.gbl.spaces`
: conversationId;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve non-a: DM chat IDs before building Graph URLs

resolveMSTeamsInboundMedia() now feeds buildMSTeamsGraphMessageUrls() with the output of this helper, but this helper only rewrites Bot Framework IDs that start with a:. Our own Teams send path already treats DM conversation IDs as opaque: extensions/msteams/src/graph-upload.ts resolves them through Graph, and extensions/msteams/src/graph-upload.test.ts has an explicit 8:orgid:user-object-id fixture. For those DMs, this branch still builds /chats/8%3Aorgid.../messages/... URLs, so inline-media downloads remain broken. Fresh evidence: the repo already tests 8:orgid: as a valid personal-chat input in graph-upload.test.ts.

Useful? React with 👍 / 👎.

Three fixes for personal chat image downloads:
1. Translate Bot Framework conversation ID (a:xxx) to Graph format (19:[email protected])
2. Trigger Graph fallback when any (not all) attachments are HTML
3. Fetch hosted content bytes from $value endpoint when not inline

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@Ted-developer Ted-developer force-pushed the fix/msteams-dm-image-download branch from f94392d to e5dc963 Compare March 22, 2026 12:02
@BradGroux BradGroux merged commit dd080b6 into openclaw:main Apr 3, 2026
35 of 37 checks passed
ngutman pushed a commit that referenced this pull request Apr 3, 2026
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
steipete pushed a commit to duncanita/openclaw that referenced this pull request Apr 4, 2026
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 openclaw#24797

Thanks @Ted-developer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MSTeams: Image attachments not downloaded in bot DM chats (3 bugs)

2 participants