Skip to content

msteams: add message actions — pin, unpin, read, react, reactions#53432

Merged
BradGroux merged 14 commits intoopenclaw:mainfrom
sudie-codes:feat/msteams-message-edit-delete
Apr 11, 2026
Merged

msteams: add message actions — pin, unpin, read, react, reactions#53432
BradGroux merged 14 commits intoopenclaw:mainfrom
sudie-codes:feat/msteams-message-edit-delete

Conversation

@sudie-codes
Copy link
Copy Markdown
Contributor

@sudie-codes sudie-codes commented Mar 24, 2026

Summary

Brings the MS Teams plugin to feature parity with Slack for message actions. The bot can now manage pinned messages, read message content, and react to messages with emoji — all capabilities already supported by other channels (Discord, Slack, Telegram).

New capabilities

  • React — add or remove emoji reactions on any message (like, heart, laugh, surprised, sad, angry)
  • List reactions — see who reacted to a message and with what emoji
  • Pin / Unpin — pin or unpin messages in chats and channels
  • List pins — see all pinned messages in a conversation
  • Read message — retrieve a single message by ID

How it works

  • Reactions use the Microsoft Graph API beta endpoints for setting and unsetting reactions
  • Pin/unpin/read/list-pins use Graph API v1.0
  • All actions support chat conversations, channel conversations, and DM targets (user:<id>)

Test plan

  • pnpm test -- extensions/msteams/src/graph-messages.test.ts — 24/24 pass
  • pnpm format — clean
  • Manual: react to a message in a Teams chat and verify it appears
  • Manual: list reactions on a message with existing reactions

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added channel: msteams Channel integration: msteams size: L labels Mar 24, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 24, 2026

Greptile Summary

This PR wires up four message actions — read, pin, unpin, and list-pins — for the MS Teams channel, alongside edit and delete which were also missing from the runtime. The implementation follows the existing Graph API and Bot Framework proactive-send patterns cleanly, and the test coverage is solid (8 new graph-messages tests + edit/delete tests).

Key observations:

  • postGraphJson in graph.ts always calls res.json() on success and will throw a SyntaxError if the API ever returns 204 No Content — a realistic scenario for the channel-pin path. deleteGraphRequest already handles this correctly by returning void; postGraphJson should guard against an empty body similarly.
  • A dead path variable remains in the channel branch of pinMessageMSTeams (line 109 of graph-messages.ts) — it is computed but the postGraphJson call uses a different inline path literal.
  • The messageId parameter of UnpinMessageMSTeamsParams (and the corresponding action handler) actually expects the pinned-message resource ID returned by pin/list-pins, not the original message ID. The jsdoc mentions this, but the conflicting name will mislead callers; renaming to pinnedMessageId or pinId would make the distinction type-safe.

Confidence Score: 4/5

  • Safe to merge after addressing the postGraphJson 204-response issue; the other findings are clean-ups.
  • The core logic is correct and well-tested for the happy path. The postGraphJson issue is a real runtime failure mode (not just theoretical) if any Graph endpoint returns 204 on success, and channel-pin endpoints are a plausible candidate. The dead variable and parameter naming are polish issues. One targeted fix to postGraphJson brings this to merge-ready.
  • extensions/msteams/src/graph.ts — postGraphJson needs a 204/empty-body guard before the res.json() call.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/graph-messages.ts
Line: 109

Comment:
**Dead variable `path` in channel branch**

`path` is computed but never referenced — the `postGraphJson` call on line 114 uses an inline template literal (`${conv.basePath}/pinnedMessages`) instead. This leftover line is likely a refactoring artifact from an earlier draft that routed through the message endpoint.

```suggestion
    // Graph v1.0 doesn't have channel pin — use the pinnedMessages pattern on chat
```
(remove the unused `const path = ...` line entirely)

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

---

This is a comment left during a code review.
Path: extensions/msteams/src/graph.ts
Line: 90-95

Comment:
**`postGraphJson` throws on 204 No Content responses**

`res.json()` is called unconditionally after a successful (2xx) response. If Graph API ever returns `204 No Content` for a POST (which some Graph mutation endpoints do, and is plausible for the channel-pin path once it is GA), `res.json()` will throw a `SyntaxError` on the empty body even though the operation succeeded. The error surface to the caller will be a cryptic JSON parse failure rather than a clear success.

`deleteGraphRequest` (lines 97–110) correctly handles this by returning `void`. `postGraphJson` should do the same when there is no content:

```suggestion
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`Graph POST ${params.path} failed (${res.status}): ${text || "unknown error"}`);
  }
  if (res.status === 204 || res.headers.get("content-length") === "0") {
    return undefined as T;
  }
  return (await res.json()) as T;
```

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

---

This is a comment left during a code review.
Path: extensions/msteams/src/graph-messages.ts
Line: 128-136

Comment:
**`messageId` in `UnpinMessageMSTeamsParams` is semantically the pin-resource ID, not the message ID**

The jsdoc correctly notes this, but the field name `messageId` conflicts with the `messageId` meaning used in every other operation (the actual message's Graph ID). The `list-pins` / `pin` responses surface this value as `pinnedMessageId`, so callers will naturally pass the wrong ID. Renaming to `pinnedMessageId` (or at least `pinId`) here and in the corresponding action handler would make the distinction unambiguous at the type level.

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

Reviews (1): Last reviewed commit: "msteams: add pin/unpin, list-pins, and r..." | Re-trigger Greptile

if (conv.kind === "channel") {
// Channel pin: POST .../messages/{messageId}/setReaction is not available;
// use beta endpoint for channel pin
const path = `${conv.basePath}/messages/${encodeURIComponent(params.messageId)}`;
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 Dead variable path in channel branch

path is computed but never referenced — the postGraphJson call on line 114 uses an inline template literal (${conv.basePath}/pinnedMessages) instead. This leftover line is likely a refactoring artifact from an earlier draft that routed through the message endpoint.

Suggested change
const path = `${conv.basePath}/messages/${encodeURIComponent(params.messageId)}`;
// Graph v1.0 doesn't have channel pin — use the pinnedMessages pattern on chat

(remove the unused const path = ... line entirely)

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/graph-messages.ts
Line: 109

Comment:
**Dead variable `path` in channel branch**

`path` is computed but never referenced — the `postGraphJson` call on line 114 uses an inline template literal (`${conv.basePath}/pinnedMessages`) instead. This leftover line is likely a refactoring artifact from an earlier draft that routed through the message endpoint.

```suggestion
    // Graph v1.0 doesn't have channel pin — use the pinnedMessages pattern on chat
```
(remove the unused `const path = ...` line entirely)

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — removed the dead path variable in the channel pin branch. See 0e250de.

Comment thread extensions/msteams/src/graph.ts Outdated
Comment on lines +90 to +95
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Graph POST ${params.path} failed (${res.status}): ${text || "unknown error"}`);
}
return (await res.json()) as T;
}
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.

P1 postGraphJson throws on 204 No Content responses

res.json() is called unconditionally after a successful (2xx) response. If Graph API ever returns 204 No Content for a POST (which some Graph mutation endpoints do, and is plausible for the channel-pin path once it is GA), res.json() will throw a SyntaxError on the empty body even though the operation succeeded. The error surface to the caller will be a cryptic JSON parse failure rather than a clear success.

deleteGraphRequest (lines 97–110) correctly handles this by returning void. postGraphJson should do the same when there is no content:

Suggested change
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Graph POST ${params.path} failed (${res.status}): ${text || "unknown error"}`);
}
return (await res.json()) as T;
}
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Graph POST ${params.path} failed (${res.status}): ${text || "unknown error"}`);
}
if (res.status === 204 || res.headers.get("content-length") === "0") {
return undefined as T;
}
return (await res.json()) as T;
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/graph.ts
Line: 90-95

Comment:
**`postGraphJson` throws on 204 No Content responses**

`res.json()` is called unconditionally after a successful (2xx) response. If Graph API ever returns `204 No Content` for a POST (which some Graph mutation endpoints do, and is plausible for the channel-pin path once it is GA), `res.json()` will throw a `SyntaxError` on the empty body even though the operation succeeded. The error surface to the caller will be a cryptic JSON parse failure rather than a clear success.

`deleteGraphRequest` (lines 97–110) correctly handles this by returning `void`. `postGraphJson` should do the same when there is no content:

```suggestion
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`Graph POST ${params.path} failed (${res.status}): ${text || "unknown error"}`);
  }
  if (res.status === 204 || res.headers.get("content-length") === "0") {
    return undefined as T;
  }
  return (await res.json()) as T;
```

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — postGraphJson now checks for 204 No Content and empty content-length before calling res.json(), returning undefined as T for empty bodies. See 0e250de.

Comment on lines +128 to +136
export type UnpinMessageMSTeamsParams = {
cfg: OpenClawConfig;
to: string;
messageId: string;
};

/**
* Unpin a message in a chat conversation via Graph API.
* The `messageId` here is the pinned-message resource ID (from pin or list-pins).
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 messageId in UnpinMessageMSTeamsParams is semantically the pin-resource ID, not the message ID

The jsdoc correctly notes this, but the field name messageId conflicts with the messageId meaning used in every other operation (the actual message's Graph ID). The list-pins / pin responses surface this value as pinnedMessageId, so callers will naturally pass the wrong ID. Renaming to pinnedMessageId (or at least pinId) here and in the corresponding action handler would make the distinction unambiguous at the type level.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/graph-messages.ts
Line: 128-136

Comment:
**`messageId` in `UnpinMessageMSTeamsParams` is semantically the pin-resource ID, not the message ID**

The jsdoc correctly notes this, but the field name `messageId` conflicts with the `messageId` meaning used in every other operation (the actual message's Graph ID). The `list-pins` / `pin` responses surface this value as `pinnedMessageId`, so callers will naturally pass the wrong ID. Renaming to `pinnedMessageId` (or at least `pinId`) here and in the corresponding action handler would make the distinction unambiguous at the type level.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — renamed the param from messageId to pinnedMessageId in UnpinMessageMSTeamsParams and updated the JSDoc. The channel action handler accepts both pinnedMessageId (preferred) and messageId (fallback) for backward compatibility. See 0e250de.

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: b3f2145b50

ℹ️ 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".

}
return {
kind: "chat",
basePath: `/chats/${encodeURIComponent(trimmed)}`,
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 Normalize Teams action targets before building Graph paths

resolveConversationPath uses the raw to value as a Graph chat ID, but MSTeams targets are commonly normalized as conversation:<id> (and that format is advertised in this plugin). When callers pass that explicit target, the code builds paths like /chats/conversation%3A19..., which Graph rejects, so read, pin, unpin, and list-pins fail for valid user inputs unless they manually strip the prefix first.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — added stripTargetPrefix() that strips conversation: and user: prefixes before building Graph paths, so targets like conversation:19:...@thread.tacv2 now resolve correctly. See 0e250de.

): Promise<{ ok: true }> {
const token = await resolveGraphToken(params.cfg);
const conv = resolveConversationPath(params.to);
const path = `${conv.basePath}/pinnedMessages/${encodeURIComponent(params.messageId)}`;
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 Resolve pinned-resource IDs before unpin delete calls

This delete call assumes messageId is already the pinned-message resource ID, but the action surface and CLI expose messageId as the message identifier users normally have. In practice this makes unpin return 404 when users pass the original message ID (including the common pin→unpin flow) because Graph expects /pinnedMessages/{pinnedMessageId} here; the implementation should translate message IDs to pinned IDs (or enforce/return a consistent ID contract).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the same commit — the unpin param was renamed to pinnedMessageId to make the contract unambiguous. The action handler's error message and JSDoc now clearly state that the pinned-message resource ID (from pin or list-pins responses) is required, not the original message ID. See 0e250de.

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: 0e250de768

ℹ️ 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 +44 to +45
if (/^user:/i.test(trimmed)) {
return trimmed.slice("user:".length).trim();
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 user targets to chat IDs before Graph actions

user:<aadId> targets are stripped to raw user IDs here, and resolveConversationPath later uses that value as /chats/{id}. In direct-message flows, ctx.toolContext.currentChannelId is user:<senderId>, so read, pin, unpin, and list-pins against the current chat call Graph with a user object ID instead of a chat ID (19:...), which fails with 404/BadRequest. This path should resolve user: targets to an actual conversation/chat ID (for example via the conversation store or Graph lookup) before building message endpoints.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — user:<aadId> targets are now resolved to actual conversation IDs via the conversation store before building Graph paths. If no stored conversation exists for the user, a descriptive error is thrown. Added tests for both the happy path and the missing-conversation case. See 5b1c0fd.

@sudie-codes sudie-codes force-pushed the feat/msteams-message-edit-delete branch from 0e250de to 5b1c0fd Compare March 24, 2026 06:54
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: 5b1c0fddb6

ℹ️ 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".

"The bot must receive a message from this user before Graph API operations work.",
);
}
return found.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 targets to Graph chat IDs before chat actions

This returns found.conversationId directly, but personal Teams conversations are often stored as Bot Framework IDs (a:... / 8:orgid:...) that Graph /chats/{id} does not accept (the repo already handles this elsewhere via resolveGraphChatId). In direct-message action flows where to is user:<aadId>, read/pin/unpin/list-pins will therefore build chat endpoints with non-Graph IDs and fail with 404/BadRequest; use cached reference.graphChatId or resolve a Graph-native chat ID before returning.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — resolveGraphConversationId now prefers the cached reference.graphChatId (19:xxx format) over the Bot Framework conversation ID for user targets. If only a non-Graph ID is available, it throws a descriptive error. See 4f1131c.

Comment on lines +214 to +216
const pins = (res.value ?? []).map((pin) => ({
pinnedMessageId: pin.id ?? "",
messageId: pin.message?.id,
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 Expose pinned resource IDs in list-pins payload rows

Each pin row omits a standard id field and only provides pinnedMessageId plus messageId; the default message formatter picks id/ts/messageId, so users see the underlying message ID instead of the pinned resource ID. Since unpin expects the pinned-message resource ID, the common list-pinsunpin flow from non-JSON CLI output will pass the wrong identifier and fail; include the pinned ID as id (or otherwise surface it as the primary ID).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — each pin row now includes an id field (aliasing pinnedMessageId) so default formatters surface the pinned resource ID for the list-pins to unpin flow. See 4f1131c.

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: 4f1131c9ff

ℹ️ 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 +100 to +103
if (cleaned.includes("/")) {
const [teamId, channelId] = cleaned.split("/", 2);
return {
kind: "channel",
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 Route channel conversation IDs through channel Graph paths

Treating every non-/ target as a chat ID breaks channel actions in normal runtime flows: buildToolContext supplies context.To as conversation:<conversationId> for channel turns, so read/pin/unpin/list-pins default to values like conversation:19:... that hit this branch and become /chats/{id} calls. For channel messages, Graph requires /teams/{teamId}/channels/{channelId}/..., so these actions fail for the common “act on current channel” case unless users manually provide a different target format.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — added a clarifying comment in resolveConversationPath explaining that plain conversation IDs route through /chats/{id} and that channel operations requiring /teams/{teamId}/channels/{channelId} must use the explicit format. See 00268b42.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I dont think this one is fixed yet. The change in resolveConversationPath only documents the limitation, but the runtime still defaults current-channel actions to context.To (conversation:<conversationId>), and the action handlers still fall back to currentChannelId when target is omitted. That means normal current-channel flows still route non-teamId/channelId targets through /chats/{id}. For channel turns, react, unreact, and reactions still build chat endpoints with the channel conversation ID, and pin, unpin, and list-pins now fail with the new unsupported-channel error instead of working on the current channel. So the behavioral issue from this thread is still present; only the comment was added.

Comment on lines +168 to +171
await postGraphJson<unknown>({
token,
path: `${conv.basePath}/pinnedMessages`,
body: { message: { id: params.messageId } },
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 Stop using channel pinnedMessages path on Graph v1.0

This branch posts to /teams/{teamId}/channels/{channelId}/pinnedMessages, but Microsoft Graph v1.0 documents pinned-message APIs under the chat resource and does not expose pinnedMessages methods on the channel resource, so channel pin (and the matching unpin/list-pins paths built from the same base) will consistently 404/BadRequest for channel targets. The implementation should either gate these actions to chats or use a supported API surface.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — pin, unpin, and list-pins now throw a descriptive error for channel targets since Graph v1.0 does not expose pinnedMessages on the channel resource. This gates those actions to chat targets only. See 00268b42.

@sudie-codes sudie-codes changed the title msteams: add pin/unpin, list-pins, and read message actions msteams: add message actions — pin, unpin, read, react, reactions Mar 25, 2026
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: be14dca225

ℹ️ 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 +183 to +186
token,
path: `${conv.basePath}/pinnedMessages`,
body: { message: { id: params.messageId } },
});
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 Use message@odata.bind when pinning chat messages

The Graph pin API expects the request body to bind an existing message via message@odata.bind (for example, .../messages/{id}), but this code sends { "message": { "id": ... } }. In practice this causes pin to fail with a Graph 400 BadRequest for valid chat targets, so the new pin action is not functional against the documented API contract.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — pin request body now uses "message@odata.bind": "https://graph.microsoft.com/v1.0/chats/{chatId}/messages/{messageId}" instead of { message: { id } }, matching the documented Graph API contract. See 00268b42.

Comment on lines +291 to +294
if (!TEAMS_REACTION_TYPES.includes(normalized as TeamsReactionType)) {
throw new Error(
`Invalid reaction type "${raw}". Valid types: ${TEAMS_REACTION_TYPES.join(", ")}`,
);
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 Allow Unicode reaction types for react/unreact

This validator hard-restricts reactions to six legacy names, but Graph setReaction/unsetReaction accepts Unicode reaction values (and additional reaction types). As a result, common inputs like 👍 are rejected before the API call, so react/unreact fail for valid Teams reactions even when permissions and target IDs are correct.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — replaced the strict six-value validation with a normalizeReactionType helper that maps known aliases (including emoji like 👍) to Graph names and passes unknown types through to the API. See 00268b42.

BradGroux

This comment was marked as duplicate.

Copy link
Copy Markdown
Member

@BradGroux BradGroux left a comment

Choose a reason for hiding this comment

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

Review: Pin/Unpin/Read/React/Reactions Actions

Summary

Adds five message actions to the Teams extension: pin, unpin, getMessage, react, and listReactions. Substantial addition that rounds out the Teams channel capabilities.

Issue 1: Reaction count undercount in listReactionsMSTeams

listReactionsMSTeams builds reaction groups and sets count: users.length. However, the users array only includes entries where reaction.user?.id is truthy. Reactions from deleted accounts, guests, or anonymous users will have a reactionType entry (creating a group) but no corresponding user push. This means count will undercount the actual number of reactions for that type.

Fix: Track count independently of the users array, or count all reactions of that type regardless of whether the user ID is present.

Issue 2: unpinMessageMSTeams ID fallback is a footgun

The function accepts messageId as a fallback when pinnedMessageId is not provided. In the Graph API, pinned message resources have their own ID that is distinct from the chat message ID. Passing a messageId to the unpin endpoint will 404 because it is not a valid pinned-message resource identifier.

The error message does hint at the correct field name, but the silent fallback to messageId means callers might not realize they are passing the wrong identifier until they get a runtime failure.

Fix: Either require pinnedMessageId explicitly (throw if missing), or add a lookup step that resolves a messageId to its pinnedMessageId via the list-pins endpoint.

What works well

  • pinMessageMSTeams implementation is clean and follows Graph API patterns
  • getMessageMSTeams with includeReactions option is a nice touch
  • Search implementation with KQL sanitization handles common edge cases
  • Test coverage for the new functions is solid

Verdict

Request Changes. Both issues are correctness bugs that will surface at runtime. The reaction count will silently report wrong numbers, and the unpin fallback will 404 on valid-looking input.

@sudie-codes
Copy link
Copy Markdown
Contributor Author

@BradGroux Thanks for the review — both issues addressed in ad495d3d62:

Issue 1 (reaction count undercount): Changed listReactionsMSTeams to track count independently of the users array. Every reaction increments count regardless of whether reaction.user?.id is present; only reactions with a valid user ID are pushed into users. Added a test case verifying the count is correct when most reactions lack a user ID.

Issue 2 (unpin ID fallback): Removed the messageId fallback from the unpin action handler. It now requires pinnedMessageId explicitly and returns a clear error explaining the distinction between pinned-message resource IDs and chat message IDs.

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: ad495d3d62

ℹ️ 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 thread extensions/msteams/src/channel.ts Outdated
Comment on lines +583 to +586
const pinnedMessageId =
typeof ctx.params.pinnedMessageId === "string"
? ctx.params.pinnedMessageId.trim()
: "";
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 Accept messageId for Teams unpin action

The new unpin handler only reads pinnedMessageId, but the shared message unpin CLI and tool schema currently provide messageId (src/cli/program/message/register.pins.ts and src/agents/tools/message-tool.ts). In normal flows, users can only pass --message-id, so this branch always treats the ID as missing and returns the validation error instead of calling Graph, making unpin unusable for MSTeams from the standard interfaces.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — the unpin handler now falls back to ctx.params.messageId when ctx.params.pinnedMessageId is not provided, so the standard --message-id CLI flag works for MSTeams unpin. See 00268b42.

Comment on lines +364 to +366
reactionType: type,
count: group.count,
users: group.users,
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 Emit reaction name field in Teams reactions payload

The reactions action now returns rows keyed only by reactionType, but the CLI formatter reads emoji/name fields when rendering the reactions table (src/commands/message-format.ts). This mismatch causes MSTeams reaction lists to render with blank emoji labels, so users cannot distinguish which counts/users belong to which reaction type in non-JSON output.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — added a REACTION_TYPE_EMOJI map and extended ReactionSummary with name and emoji fields so the CLI formatter renders reaction labels correctly for MSTeams. See 00268b42.

Copy link
Copy Markdown
Member

@BradGroux BradGroux left a comment

Choose a reason for hiding this comment

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

Review: msteams pin/unpin/react/reactions

Commit reviewed: ad495d3


Fixes from prior review

Reaction count undercount: ✅ Fixed correctly. grouped now stores { count, users } and increments count independently of user.id presence. The new test covering deleted/guest/anonymous users is a solid addition.

Unpin pinnedMessageId: ✅ Logic is correct. The messageId fallback was removed and pinnedMessageId is now required with a clear error message explaining it is the pinned-message resource ID, not the chat message ID.


New findings

1. Unpin call surface mismatch (High)
The runtime now correctly requires pinnedMessageId for unpin operations. However, the CLI wiring (register.pins.ts) and the generic message tool schema still only expose --message-id. Common call paths (CLI, message tool) cannot provide the pinnedMessageId field, which means unpin will fail at runtime with "requires pinnedMessageId" for any caller that does not manually construct the parameter.

This is not a code bug in the PR itself, but it means the feature is unreachable through normal user-facing surfaces. Either the CLI/tool schema needs updating in this PR, or a follow-up PR should be linked that wires the parameter through.

2. Channel pin endpoint uncertainty (Medium)
The code uses /teams/{teamId}/channels/{channelId}/pinnedMessages for channel targets via resolveConversationPath + postGraphJson. The PR comments acknowledge that channel pinning may require different handling or beta API access. If Graph only supports pinned messages through chat resources, channel pin/list/unpin will consistently fail outside of mocks.

Suggestion: Add explicit documentation or a feature flag for channel pinning that notes the endpoint status. If it is beta-only, gate it behind a config option or at minimum log a warning when channel pin operations are attempted.

3. Pagination gap in listPinsMSTeams (Low)
@odata.nextLink is not handled, so large pin sets may return incomplete results. This is unlikely to matter in practice (most chats have few pins), but worth noting for completeness.


Test coverage

  • ✅ Reaction count fix has dedicated test for anonymous/deleted users
  • ✅ Unpin requires pinnedMessageId with clear error
  • ⚠️ No integration test proving unpin works from CLI or message tool surface with actual parameter names
  • ⚠️ No test for channel pin endpoint behavior against real Graph constraints
  • ⚠️ No pagination test for listPinsMSTeams

Summary

The two flagged issues from the prior review are both fixed correctly. The reaction count logic is solid and the unpin ID handling is clean. The new concern is the call surface mismatch: the runtime requires pinnedMessageId but no user-facing tool provides it yet. This needs either a schema update in this PR or a linked follow-up before the feature is usable end-to-end.

@openclaw-barnacle openclaw-barnacle Bot added the cli CLI command changes label Mar 28, 2026
@sudie-codes
Copy link
Copy Markdown
Contributor Author

@BradGroux Thanks for the follow-up review — all 3 findings addressed in 0f9b856c17:

Finding 1 (High) — Unpin call surface mismatch: Updated the CLI unpin command in register.pins.ts to accept --pinned-message-id instead of --message-id, with a description clarifying it expects the pinned-message resource ID. Also added pinnedMessageId as an Optional(String) property to the message tool schema in channel.ts so the AI agent surface includes it.

Finding 2 (Medium) — Channel pin endpoint uncertainty: Added JSDoc comments on pinMessageMSTeams, unpinMessageMSTeams, and listPinsMSTeams documenting that the channel pin endpoint may require beta API access or tenant-level permissions. Added console.warn log warnings when channel pin operations are attempted.

Finding 3 (Low) — Pagination gap in listPinsMSTeams: Added fetchGraphAbsoluteUrl helper for @odata.nextLink pagination URLs. listPinsMSTeams now follows pagination with a safety cap of 10 pages. Added tests for both single-page pagination and the page cap.

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: 0f9b856c17

ℹ️ 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 +18 to +20
.requiredOption(
"--pinned-message-id <id>",
"Pinned message resource id (from pin or list-pins, not the chat message id)",
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 Keep --message-id for cross-channel unpin flows

The message unpin CLI flag was changed to --pinned-message-id globally, but this command is registered once for all channels (src/cli/program/register.message.ts) and most channel unpin handlers still require messageId (for example, Slack reads messageId as required in extensions/slack/src/action-runtime.ts). This means non-MSTeams unpin workflows now break: --message-id is rejected by the CLI, and supplying --pinned-message-id leaves messageId unset in channel handlers. Please preserve --message-id (or map it to messageId) for generic unpin and keep MSTeams-specific translation local to that plugin.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — restored --message-id as the primary flag for cross-channel unpin and kept --pinned-message-id as an optional MSTeams-friendly alternative, so non-MSTeams channel handlers are unaffected. See 00268b42.

@sudie-codes sudie-codes force-pushed the feat/msteams-message-edit-delete branch from 00268b4 to 9c44f3e Compare April 7, 2026 04:06
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: 9c44f3e7ed

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/msteams/src/graph.ts Outdated
return readOptionalGraphJson<T>(res);
}

export async function postGraphBetaJson<T>(params: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 Badge Remove duplicate postGraphBetaJson declaration

Defining postGraphBetaJson twice in the same ES module causes module evaluation to fail with SyntaxError: Identifier 'postGraphBetaJson' has already been declared as soon as graph.ts is imported. That blocks all MSTeams message actions that depend on this module from running at all, not just the new reaction paths.

Useful? React with 👍 / 👎.

Comment on lines +33 to +35
createMSTeamsConversationStoreFs: () => ({
findByUserId: mockState.findByUserId,
}),
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 Mock findPreferredDmByUserId in conversation-store tests

The production code resolves user: targets via createMSTeamsConversationStoreFs().findPreferredDmByUserId(...), but this test mock only provides findByUserId. In any test exercising user-target resolution, this makes the call site hit undefined and fail before assertions, so the new suite does not validate the intended behavior.

Useful? React with 👍 / 👎.

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: 7aba2981dc

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/msteams/src/channel.ts Outdated
Comment on lines +1008 to +1010
const currentChannelId = hasChannelRoute
? nativeChannelId
: context.To?.trim() || undefined;
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 Keep Bot Framework fallback target for non-Graph actions

threading.buildToolContext now rewrites channel fallbacks to teamId/channelId, but non-Graph actions still route through sendMessageMSTeams/editMessageMSTeams/deleteMessageMSTeams, which call resolveMSTeamsSendContext(parseRecipient) and look up conversation-store entries by exact conversation ID. The store is keyed by plain conversationId (for example 19:...), so a fallback like team-1/19:...@thread.tacv2 cannot be resolved and these existing channel flows fail with “No conversation reference found” whenever target is omitted. This regression is introduced by the new global fallback and affects upload/edit/delete (and similar Bot Framework-backed actions) in normal channel turns.

Useful? React with 👍 / 👎.

@sudie-codes
Copy link
Copy Markdown
Contributor Author

@BradGroux All outstanding items addressed:

  1. Channel routing gap (your Apr 4 blocker) — buildToolContext now prefers NativeChannelId (compound teamId/channelId) for channel turns, so actions route via /teams/{teamId}/channels/{channelId} instead of /chats/{id}. This is a real runtime fix, not just documentation. 7 new tests covering channel routing, DM fallback, and safety cases.

  2. pinnedMessageId call surface mismatch (your Mar 27 high) — Already wired in tool schema (Type.Optional(Type.String(...))). Added 3 tests: explicit pinnedMessageId preferred over messageId, clear error when missing, schema exposes the field.

  3. Incidental fixes: removed duplicate postGraphBetaJson export, updated stale test mocks, fixed pre-existing test failures that were blocking CI on this branch.

All 596 msteams tests pass. Ready for re-review.

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: e73af4130d

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/msteams/src/channel.ts Outdated
@@ -1,3 +1,4 @@
import { Type } from "@sinclair/typebox";
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 Declare @sinclair/typebox as msteams runtime dependency

channel.ts now imports @sinclair/typebox, but extensions/msteams/package.json does not list that package in dependencies. In the plugin’s documented install mode (npm install --omit=dev in the extension directory), this causes a runtime module-resolution failure when loading the plugin (ERR_MODULE_NOT_FOUND), so MSTeams channel startup/actions can fail outside the monorepo hoisted dev environment.

Useful? React with 👍 / 👎.

BradGroux pushed a commit to BradGroux/openclaw that referenced this pull request Apr 10, 2026
BradGroux pushed a commit to BradGroux/openclaw that referenced this pull request Apr 10, 2026
BradGroux pushed a commit to BradGroux/openclaw that referenced this pull request Apr 10, 2026
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: e99e8ec47a

ℹ️ 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 thread extensions/msteams/src/channel.ts Outdated
Comment on lines +182 to +186
params: Record<string, unknown>,
currentChannelId?: string | null,
currentGraphChannelId?: string | null,
): string {
return resolveActionTarget(params, currentGraphChannelId ?? currentChannelId);
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 Ignore auto-filled target when resolving Graph-only fallbacks

Graph-only actions still pick the wrong route in normal runtime flows because resolveGraphActionTarget delegates to resolveActionTarget, which prefers params.to/params.target before currentGraphChannelId. Fresh evidence: runMessageAction normalizes required-target actions by auto-filling target from toolContext.currentChannelId (and then to), so channel turns end up locked to conversation:... and Graph calls are built as /chats/{id} instead of /teams/{teamId}/channels/{channelId} for read, pin, unpin, list-pins, react, and reactions when users omit an explicit target.

Useful? React with 👍 / 👎.

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: 4e6fc80e66

ℹ️ 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".

// can route through `/teams/{teamId}/channels/{channelId}` via Graph API.
// The bare conversation id (`19:...@thread.tacv2`) is insufficient on its
// own because channel Graph endpoints require the owning team id too.
const nativeChannelId = isChannel && teamId ? `${teamId}/${conversationId}` : undefined;
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 Graph team ID before composing NativeChannelId

NativeChannelId is built from raw teamId (activity.channelData.team.id) plus conversationId, then used as the Graph fallback target for channel actions. In this plugin, teamId is not reliably a Graph /teams/{id} GUID (see the existing resolveTeamGroupId mapping path and the resolve-allowlist notes/tests that runtime team IDs are often Bot Framework conversation keys), so channel read/react/reactions can still call Graph with an invalid team ID and fail with 404/BadRequest even when users act on the current channel. This should store/propagate a Graph-resolved team ID (or resolve on demand) before constructing the teamId/channelId Graph target.

Useful? React with 👍 / 👎.

@sudie-codes sudie-codes force-pushed the feat/msteams-message-edit-delete branch from 67982b8 to 7738c83 Compare April 10, 2026 21:24
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: 7738c8346a

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

currentChannelId?: string | null,
currentGraphChannelId?: string | null,
): string {
return resolveActionTarget(params, currentGraphChannelId ?? currentChannelId);
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 Prefer Graph channel fallback over prefilled target fields

This resolver still delegates to resolveActionTarget, which prioritizes params.to/params.target before fallback values, so Graph-only actions can ignore currentGraphChannelId. In the real runner flow, normalizeMessageActionInput auto-fills target from toolContext.currentChannelId for required-target actions, then maps it to to; as a result, channel turns that omit an explicit target still get routed with conversation:... and end up on /chats/{id} instead of /teams/{teamId}/channels/{channelId} for Graph actions like read/react/search.

Useful? React with 👍 / 👎.

// can route through `/teams/{teamId}/channels/{channelId}` via Graph API.
// The bare conversation id (`19:...@thread.tacv2`) is insufficient on its
// own because channel Graph endpoints require the owning team id too.
const nativeChannelId = isChannel && teamId ? `${teamId}/${conversationId}` : undefined;
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 Store Graph-resolved team IDs in NativeChannelId

This writes NativeChannelId using raw activity.channelData.team.id, but downstream Graph actions treat that first segment as /teams/{teamId}. In the same handler, channel thread fetches already call resolveTeamGroupId(...) before Graph requests, which indicates the raw Teams runtime ID is not reliably Graph-native; propagating the unresolved value here can produce invalid /teams/{id}/channels/{id} paths and 404/BadRequest for channel message actions.

Useful? React with 👍 / 👎.

@sudie-codes sudie-codes force-pushed the feat/msteams-message-edit-delete branch from 7738c83 to e864e19 Compare April 10, 2026 21:35
@sudie-codes
Copy link
Copy Markdown
Contributor Author

@BradGroux apologies — my earlier "ready for re-review" ping was premature. CI was actually red on check-additional (the lint:tmp:no-raw-channel-fetch guard caught a raw fetch() in the new fetchGraphAbsoluteUrl pagination helper). I should have verified CI was green before pinging.

Now properly fixed: fetchGraphAbsoluteUrl routes through fetchWithSsrFGuard like the rest of the extension. Latest commit: e864e198c1. CI is fully green — 33 passed, 0 failed.

Sorry for the noise. Ready for re-review.

@BradGroux BradGroux merged commit 0f19271 into openclaw:main Apr 11, 2026
41 checks passed
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…enclaw#53432)

* msteams: add pin/unpin, list-pins, and read message actions

Wire up Graph API endpoints for message read, pin, unpin, and list-pins
in the MS Teams extension, following the same patterns as edit/delete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: address PR review comments for pin/unpin/read actions

- Handle 204 No Content in postGraphJson (Graph mutations may return empty body)
- Strip conversation:/user: prefixes in resolveConversationPath to avoid Graph 404s
- Remove dead variable in channel pin branch
- Rename unpin param from messageId to pinnedMessageId for semantic clarity
- Accept both pinnedMessageId and messageId in unpin action handler for compat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve user targets + add User-Agent to Graph helpers

- Resolve user:<aadId> targets to actual conversation IDs via conversation
  store before Graph API calls (fixes 404 for DM-context actions)
- Add User-Agent header to postGraphJson/deleteGraphRequest for consistency
  with fetchGraphJson after rebase onto main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve DM targets to Graph chat IDs + expose pin IDs

- Prefer cached graphChatId over Bot Framework conversation IDs for user
  targets; throw descriptive error when no Graph-compatible ID is available
- Add `id` field to list-pins rows so default formatters surface the pinned
  resource ID needed for the unpin flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: add react and reactions (list) message actions

* msteams: fix reaction count undercount and remove unpin messageId fallback

* msteams: wire pinnedMessageId through CLI/tool schema, add channel pin beta warnings, add list-pins pagination

* msteams: address PR openclaw#53432 remaining review feedback

* fix(msteams): route channel actions via teamId/channelId path (openclaw#53432)

* msteams: add unpin pinnedMessageId test coverage (openclaw#53432)

* fix(msteams): keep graph routing scoped to graph actions

* fix(msteams): align graph routing context types

* msteams: route fetchGraphAbsoluteUrl through fetchWithSsrFGuard

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…enclaw#53432)

* msteams: add pin/unpin, list-pins, and read message actions

Wire up Graph API endpoints for message read, pin, unpin, and list-pins
in the MS Teams extension, following the same patterns as edit/delete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: address PR review comments for pin/unpin/read actions

- Handle 204 No Content in postGraphJson (Graph mutations may return empty body)
- Strip conversation:/user: prefixes in resolveConversationPath to avoid Graph 404s
- Remove dead variable in channel pin branch
- Rename unpin param from messageId to pinnedMessageId for semantic clarity
- Accept both pinnedMessageId and messageId in unpin action handler for compat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve user targets + add User-Agent to Graph helpers

- Resolve user:<aadId> targets to actual conversation IDs via conversation
  store before Graph API calls (fixes 404 for DM-context actions)
- Add User-Agent header to postGraphJson/deleteGraphRequest for consistency
  with fetchGraphJson after rebase onto main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve DM targets to Graph chat IDs + expose pin IDs

- Prefer cached graphChatId over Bot Framework conversation IDs for user
  targets; throw descriptive error when no Graph-compatible ID is available
- Add `id` field to list-pins rows so default formatters surface the pinned
  resource ID needed for the unpin flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: add react and reactions (list) message actions

* msteams: fix reaction count undercount and remove unpin messageId fallback

* msteams: wire pinnedMessageId through CLI/tool schema, add channel pin beta warnings, add list-pins pagination

* msteams: address PR openclaw#53432 remaining review feedback

* fix(msteams): route channel actions via teamId/channelId path (openclaw#53432)

* msteams: add unpin pinnedMessageId test coverage (openclaw#53432)

* fix(msteams): keep graph routing scoped to graph actions

* fix(msteams): align graph routing context types

* msteams: route fetchGraphAbsoluteUrl through fetchWithSsrFGuard

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…enclaw#53432)

* msteams: add pin/unpin, list-pins, and read message actions

Wire up Graph API endpoints for message read, pin, unpin, and list-pins
in the MS Teams extension, following the same patterns as edit/delete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: address PR review comments for pin/unpin/read actions

- Handle 204 No Content in postGraphJson (Graph mutations may return empty body)
- Strip conversation:/user: prefixes in resolveConversationPath to avoid Graph 404s
- Remove dead variable in channel pin branch
- Rename unpin param from messageId to pinnedMessageId for semantic clarity
- Accept both pinnedMessageId and messageId in unpin action handler for compat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve user targets + add User-Agent to Graph helpers

- Resolve user:<aadId> targets to actual conversation IDs via conversation
  store before Graph API calls (fixes 404 for DM-context actions)
- Add User-Agent header to postGraphJson/deleteGraphRequest for consistency
  with fetchGraphJson after rebase onto main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve DM targets to Graph chat IDs + expose pin IDs

- Prefer cached graphChatId over Bot Framework conversation IDs for user
  targets; throw descriptive error when no Graph-compatible ID is available
- Add `id` field to list-pins rows so default formatters surface the pinned
  resource ID needed for the unpin flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: add react and reactions (list) message actions

* msteams: fix reaction count undercount and remove unpin messageId fallback

* msteams: wire pinnedMessageId through CLI/tool schema, add channel pin beta warnings, add list-pins pagination

* msteams: address PR openclaw#53432 remaining review feedback

* fix(msteams): route channel actions via teamId/channelId path (openclaw#53432)

* msteams: add unpin pinnedMessageId test coverage (openclaw#53432)

* fix(msteams): keep graph routing scoped to graph actions

* fix(msteams): align graph routing context types

* msteams: route fetchGraphAbsoluteUrl through fetchWithSsrFGuard

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams cli CLI command changes size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants