msteams: add member-info action via Graph API (#57528)

This commit is contained in:
sudie-codes
2026-03-31 00:24:33 -07:00
committed by GitHub
parent 5ec362fe0b
commit 4e67e7c02c
4 changed files with 152 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ import {
listMSTeamsDirectoryGroupsLive as listMSTeamsDirectoryGroupsLiveImpl,
listMSTeamsDirectoryPeersLive as listMSTeamsDirectoryPeersLiveImpl,
} from "./directory-live.js";
import { getMemberInfoMSTeams as getMemberInfoMSTeamsImpl } from "./graph-members.js";
import {
getMessageMSTeams as getMessageMSTeamsImpl,
listPinsMSTeams as listPinsMSTeamsImpl,
@@ -23,6 +24,7 @@ import {
export const msTeamsChannelRuntime = {
deleteMessageMSTeams: deleteMessageMSTeamsImpl,
editMessageMSTeams: editMessageMSTeamsImpl,
getMemberInfoMSTeams: getMemberInfoMSTeamsImpl,
getMessageMSTeams: getMessageMSTeamsImpl,
listPinsMSTeams: listPinsMSTeamsImpl,
listReactionsMSTeams: listReactionsMSTeamsImpl,

View File

@@ -329,6 +329,7 @@ function describeMSTeamsMessageTool({
"react",
"reactions",
"search",
"member-info",
] satisfies ChannelMessageActionName[])
: [],
capabilities: enabled ? ["cards"] : [],
@@ -842,6 +843,16 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
});
}
if (ctx.action === "member-info") {
const userId = typeof ctx.params.userId === "string" ? ctx.params.userId.trim() : "";
if (!userId) {
return actionError("member-info requires a userId.");
}
const { getMemberInfoMSTeams } = await loadMSTeamsChannelRuntime();
const result = await getMemberInfoMSTeams({ cfg: ctx.cfg, userId });
return jsonMSTeamsOkActionResult("member-info", result);
}
// Return null to fall through to default handler
return null as never;
},

View File

@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../runtime-api.js";
import { getMemberInfoMSTeams } from "./graph-members.js";
const mockState = vi.hoisted(() => ({
resolveGraphToken: vi.fn(),
fetchGraphJson: vi.fn(),
}));
vi.mock("./graph.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./graph.js")>();
return {
...actual,
resolveGraphToken: mockState.resolveGraphToken,
fetchGraphJson: mockState.fetchGraphJson,
};
});
const TOKEN = "test-graph-token";
describe("getMemberInfoMSTeams", () => {
beforeEach(() => {
vi.clearAllMocks();
mockState.resolveGraphToken.mockResolvedValue(TOKEN);
});
it("fetches user profile and maps all fields", async () => {
mockState.fetchGraphJson.mockResolvedValue({
id: "user-123",
displayName: "Alice Smith",
mail: "alice@contoso.com",
jobTitle: "Engineer",
userPrincipalName: "alice@contoso.com",
officeLocation: "Building 1",
});
const result = await getMemberInfoMSTeams({
cfg: {} as OpenClawConfig,
userId: "user-123",
});
expect(result).toEqual({
user: {
id: "user-123",
displayName: "Alice Smith",
mail: "alice@contoso.com",
jobTitle: "Engineer",
userPrincipalName: "alice@contoso.com",
officeLocation: "Building 1",
},
});
expect(mockState.fetchGraphJson).toHaveBeenCalledWith({
token: TOKEN,
path: `/users/${encodeURIComponent("user-123")}?$select=id,displayName,mail,jobTitle,userPrincipalName,officeLocation`,
});
});
it("handles sparse data with some fields undefined", async () => {
mockState.fetchGraphJson.mockResolvedValue({
id: "user-456",
displayName: "Bob",
});
const result = await getMemberInfoMSTeams({
cfg: {} as OpenClawConfig,
userId: "user-456",
});
expect(result).toEqual({
user: {
id: "user-456",
displayName: "Bob",
mail: undefined,
jobTitle: undefined,
userPrincipalName: undefined,
officeLocation: undefined,
},
});
});
it("propagates Graph API errors", async () => {
mockState.fetchGraphJson.mockRejectedValue(new Error("Graph API 404: user not found"));
await expect(
getMemberInfoMSTeams({
cfg: {} as OpenClawConfig,
userId: "nonexistent-user",
}),
).rejects.toThrow("Graph API 404: user not found");
});
});

View File

@@ -0,0 +1,48 @@
import type { OpenClawConfig } from "../runtime-api.js";
import { fetchGraphJson, resolveGraphToken } from "./graph.js";
type GraphUserProfile = {
id?: string;
displayName?: string;
mail?: string;
jobTitle?: string;
userPrincipalName?: string;
officeLocation?: string;
};
export type GetMemberInfoMSTeamsParams = {
cfg: OpenClawConfig;
userId: string;
};
export type GetMemberInfoMSTeamsResult = {
user: {
id: string | undefined;
displayName: string | undefined;
mail: string | undefined;
jobTitle: string | undefined;
userPrincipalName: string | undefined;
officeLocation: string | undefined;
};
};
/**
* Fetch a user profile from Microsoft Graph by user ID.
*/
export async function getMemberInfoMSTeams(
params: GetMemberInfoMSTeamsParams,
): Promise<GetMemberInfoMSTeamsResult> {
const token = await resolveGraphToken(params.cfg);
const path = `/users/${encodeURIComponent(params.userId)}?$select=id,displayName,mail,jobTitle,userPrincipalName,officeLocation`;
const user = await fetchGraphJson<GraphUserProfile>({ token, path });
return {
user: {
id: user.id,
displayName: user.displayName,
mail: user.mail,
jobTitle: user.jobTitle,
userPrincipalName: user.userPrincipalName,
officeLocation: user.officeLocation,
},
};
}