Skip to content

Commit 0f19271

Browse files
sudie-codesclaudeBradGroux
authored
msteams: add message actions — pin, unpin, read, react, reactions (#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 #53432 remaining review feedback * fix(msteams): route channel actions via teamId/channelId path (#53432) * msteams: add unpin pinnedMessageId test coverage (#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>
1 parent 6c574d7 commit 0f19271

13 files changed

Lines changed: 1211 additions & 83 deletions

File tree

extensions/msteams/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "OpenClaw Microsoft Teams channel plugin",
55
"type": "module",
66
"dependencies": {
7+
"@sinclair/typebox": "0.34.49",
78
"@microsoft/teams.api": "2.0.7",
89
"@microsoft/teams.apps": "2.0.7",
910
"express": "^5.2.1",

extensions/msteams/src/actions.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Type } from "@sinclair/typebox";
12
import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-actions";
23
import type {
34
ChannelMessageActionAdapter,
@@ -71,6 +72,14 @@ function resolveActionTarget(
7172
: (currentChannelId?.trim() ?? "");
7273
}
7374

75+
function resolveGraphActionTarget(
76+
params: Record<string, unknown>,
77+
currentChannelId?: string | null,
78+
currentGraphChannelId?: string | null,
79+
): string {
80+
return resolveActionTarget(params, currentGraphChannelId ?? currentChannelId);
81+
}
82+
7483
function resolveActionMessageId(params: Record<string, unknown>): string {
7584
return normalizeOptionalString(params.messageId) ?? "";
7685
}
@@ -113,8 +122,16 @@ function resolveRequiredActionTarget(params: {
113122
actionLabel: string;
114123
toolParams: Record<string, unknown>;
115124
currentChannelId?: string | null;
125+
currentGraphChannelId?: string | null;
126+
graphOnly?: boolean;
116127
}): string | ReturnType<typeof actionError> {
117-
const to = resolveActionTarget(params.toolParams, params.currentChannelId);
128+
const to = params.graphOnly
129+
? resolveGraphActionTarget(
130+
params.toolParams,
131+
params.currentChannelId,
132+
params.currentGraphChannelId,
133+
)
134+
: resolveActionTarget(params.toolParams, params.currentChannelId);
118135
if (!to) {
119136
return actionError(`${params.actionLabel} requires a target (to).`);
120137
}
@@ -125,8 +142,16 @@ function resolveRequiredActionMessageTarget(params: {
125142
actionLabel: string;
126143
toolParams: Record<string, unknown>;
127144
currentChannelId?: string | null;
145+
currentGraphChannelId?: string | null;
146+
graphOnly?: boolean;
128147
}): { to: string; messageId: string } | ReturnType<typeof actionError> {
129-
const to = resolveActionTarget(params.toolParams, params.currentChannelId);
148+
const to = params.graphOnly
149+
? resolveGraphActionTarget(
150+
params.toolParams,
151+
params.currentChannelId,
152+
params.currentGraphChannelId,
153+
)
154+
: resolveActionTarget(params.toolParams, params.currentChannelId);
130155
const messageId = resolveActionMessageId(params.toolParams);
131156
if (!to || !messageId) {
132157
return actionError(`${params.actionLabel} requires a target (to) and messageId.`);
@@ -138,8 +163,16 @@ function resolveRequiredActionPinnedMessageTarget(params: {
138163
actionLabel: string;
139164
toolParams: Record<string, unknown>;
140165
currentChannelId?: string | null;
166+
currentGraphChannelId?: string | null;
167+
graphOnly?: boolean;
141168
}): { to: string; pinnedMessageId: string } | ReturnType<typeof actionError> {
142-
const to = resolveActionTarget(params.toolParams, params.currentChannelId);
169+
const to = params.graphOnly
170+
? resolveGraphActionTarget(
171+
params.toolParams,
172+
params.currentChannelId,
173+
params.currentGraphChannelId,
174+
)
175+
: resolveActionTarget(params.toolParams, params.currentChannelId);
143176
const pinnedMessageId = resolveActionPinnedMessageId(params.toolParams);
144177
if (!to || !pinnedMessageId) {
145178
return actionError(`${params.actionLabel} requires a target (to) and pinnedMessageId.`);
@@ -151,12 +184,16 @@ async function runWithRequiredActionTarget<T>(params: {
151184
actionLabel: string;
152185
toolParams: Record<string, unknown>;
153186
currentChannelId?: string | null;
187+
currentGraphChannelId?: string | null;
188+
graphOnly?: boolean;
154189
run: (to: string) => Promise<T>;
155190
}): Promise<T | ReturnType<typeof actionError>> {
156191
const to = resolveRequiredActionTarget({
157192
actionLabel: params.actionLabel,
158193
toolParams: params.toolParams,
159194
currentChannelId: params.currentChannelId,
195+
currentGraphChannelId: params.currentGraphChannelId,
196+
graphOnly: params.graphOnly,
160197
});
161198
if (typeof to !== "string") {
162199
return to;
@@ -168,12 +205,16 @@ async function runWithRequiredActionMessageTarget<T>(params: {
168205
actionLabel: string;
169206
toolParams: Record<string, unknown>;
170207
currentChannelId?: string | null;
208+
currentGraphChannelId?: string | null;
209+
graphOnly?: boolean;
171210
run: (target: { to: string; messageId: string }) => Promise<T>;
172211
}): Promise<T | ReturnType<typeof actionError>> {
173212
const target = resolveRequiredActionMessageTarget({
174213
actionLabel: params.actionLabel,
175214
toolParams: params.toolParams,
176215
currentChannelId: params.currentChannelId,
216+
currentGraphChannelId: params.currentGraphChannelId,
217+
graphOnly: params.graphOnly,
177218
});
178219
if ("isError" in target) {
179220
return target;
@@ -185,12 +226,16 @@ async function runWithRequiredActionPinnedMessageTarget<T>(params: {
185226
actionLabel: string;
186227
toolParams: Record<string, unknown>;
187228
currentChannelId?: string | null;
229+
currentGraphChannelId?: string | null;
230+
graphOnly?: boolean;
188231
run: (target: { to: string; pinnedMessageId: string }) => Promise<T>;
189232
}): Promise<T | ReturnType<typeof actionError>> {
190233
const target = resolveRequiredActionPinnedMessageTarget({
191234
actionLabel: params.actionLabel,
192235
toolParams: params.toolParams,
193236
currentChannelId: params.currentChannelId,
237+
currentGraphChannelId: params.currentGraphChannelId,
238+
graphOnly: params.graphOnly,
194239
});
195240
if ("isError" in target) {
196241
return target;
@@ -230,6 +275,12 @@ export function describeMSTeamsMessageTool({
230275
? {
231276
properties: {
232277
card: createMessageToolCardSchema(),
278+
pinnedMessageId: Type.Optional(
279+
Type.String({
280+
description:
281+
"Pinned message resource ID for unpin (from pin or list-pins, not the chat message ID).",
282+
}),
283+
),
233284
},
234285
}
235286
: null,
@@ -348,6 +399,8 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
348399
actionLabel: "Read",
349400
toolParams: ctx.params,
350401
currentChannelId: ctx.toolContext?.currentChannelId,
402+
currentGraphChannelId: ctx.toolContext?.currentGraphChannelId,
403+
graphOnly: true,
351404
run: async (target) => {
352405
const { getMessageMSTeams } = await loadMSTeamsChannelRuntime();
353406
const message = await getMessageMSTeams({
@@ -365,6 +418,8 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
365418
actionLabel: "Pin",
366419
toolParams: ctx.params,
367420
currentChannelId: ctx.toolContext?.currentChannelId,
421+
currentGraphChannelId: ctx.toolContext?.currentGraphChannelId,
422+
graphOnly: true,
368423
run: async (target) => {
369424
const { pinMessageMSTeams } = await loadMSTeamsChannelRuntime();
370425
const result = await pinMessageMSTeams({
@@ -382,6 +437,8 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
382437
actionLabel: "Unpin",
383438
toolParams: ctx.params,
384439
currentChannelId: ctx.toolContext?.currentChannelId,
440+
currentGraphChannelId: ctx.toolContext?.currentGraphChannelId,
441+
graphOnly: true,
385442
run: async (target) => {
386443
const { unpinMessageMSTeams } = await loadMSTeamsChannelRuntime();
387444
const result = await unpinMessageMSTeams({
@@ -399,6 +456,8 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
399456
actionLabel: "List-pins",
400457
toolParams: ctx.params,
401458
currentChannelId: ctx.toolContext?.currentChannelId,
459+
currentGraphChannelId: ctx.toolContext?.currentGraphChannelId,
460+
graphOnly: true,
402461
run: async (to) => {
403462
const { listPinsMSTeams } = await loadMSTeamsChannelRuntime();
404463
const result = await listPinsMSTeams({ cfg: ctx.cfg, to });
@@ -412,6 +471,8 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
412471
actionLabel: "React",
413472
toolParams: ctx.params,
414473
currentChannelId: ctx.toolContext?.currentChannelId,
474+
currentGraphChannelId: ctx.toolContext?.currentGraphChannelId,
475+
graphOnly: true,
415476
run: async (target) => {
416477
const emoji = normalizeOptionalString(ctx.params.emoji) ?? "";
417478
const remove = typeof ctx.params.remove === "boolean" ? ctx.params.remove : false;
@@ -464,6 +525,8 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
464525
actionLabel: "Reactions",
465526
toolParams: ctx.params,
466527
currentChannelId: ctx.toolContext?.currentChannelId,
528+
currentGraphChannelId: ctx.toolContext?.currentGraphChannelId,
529+
graphOnly: true,
467530
run: async (target) => {
468531
const { listReactionsMSTeams } = await loadMSTeamsChannelRuntime();
469532
const result = await listReactionsMSTeams({
@@ -481,6 +544,8 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
481544
actionLabel: "Search",
482545
toolParams: ctx.params,
483546
currentChannelId: ctx.toolContext?.currentChannelId,
547+
currentGraphChannelId: ctx.toolContext?.currentGraphChannelId,
548+
graphOnly: true,
484549
run: async (to) => {
485550
const query = resolveActionQuery(ctx.params);
486551
if (!query) {

0 commit comments

Comments
 (0)