Skip to content

Commit b3d4b6d

Browse files
committed
msteams: add member-info action via Graph API
1 parent 88716f0 commit b3d4b6d

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

extensions/msteams/src/channel.runtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
listMSTeamsDirectoryGroupsLive as listMSTeamsDirectoryGroupsLiveImpl,
33
listMSTeamsDirectoryPeersLive as listMSTeamsDirectoryPeersLiveImpl,
44
} from "./directory-live.js";
5+
import { getMemberInfoMSTeams as getMemberInfoMSTeamsImpl } from "./graph-members.js";
56
import {
67
getMessageMSTeams as getMessageMSTeamsImpl,
78
listPinsMSTeams as listPinsMSTeamsImpl,
@@ -23,6 +24,7 @@ import {
2324
export const msTeamsChannelRuntime = {
2425
deleteMessageMSTeams: deleteMessageMSTeamsImpl,
2526
editMessageMSTeams: editMessageMSTeamsImpl,
27+
getMemberInfoMSTeams: getMemberInfoMSTeamsImpl,
2628
getMessageMSTeams: getMessageMSTeamsImpl,
2729
listPinsMSTeams: listPinsMSTeamsImpl,
2830
listReactionsMSTeams: listReactionsMSTeamsImpl,

extensions/msteams/src/channel.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ function describeMSTeamsMessageTool({
329329
"react",
330330
"reactions",
331331
"search",
332+
"member-info",
332333
] satisfies ChannelMessageActionName[])
333334
: [],
334335
capabilities: enabled ? ["cards"] : [],
@@ -842,6 +843,16 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
842843
});
843844
}
844845

846+
if (ctx.action === "member-info") {
847+
const userId = typeof ctx.params.userId === "string" ? ctx.params.userId.trim() : "";
848+
if (!userId) {
849+
return actionError("member-info requires a userId.");
850+
}
851+
const { getMemberInfoMSTeams } = await loadMSTeamsChannelRuntime();
852+
const result = await getMemberInfoMSTeams({ cfg: ctx.cfg, userId });
853+
return jsonMSTeamsOkActionResult("member-info", result);
854+
}
855+
845856
// Return null to fall through to default handler
846857
return null as never;
847858
},
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import type { OpenClawConfig } from "../runtime-api.js";
3+
import { getMemberInfoMSTeams } from "./graph-members.js";
4+
5+
const mockState = vi.hoisted(() => ({
6+
resolveGraphToken: vi.fn(),
7+
fetchGraphJson: vi.fn(),
8+
}));
9+
10+
vi.mock("./graph.js", async (importOriginal) => {
11+
const actual = await importOriginal<typeof import("./graph.js")>();
12+
return {
13+
...actual,
14+
resolveGraphToken: mockState.resolveGraphToken,
15+
fetchGraphJson: mockState.fetchGraphJson,
16+
};
17+
});
18+
19+
const TOKEN = "test-graph-token";
20+
21+
describe("getMemberInfoMSTeams", () => {
22+
beforeEach(() => {
23+
vi.clearAllMocks();
24+
mockState.resolveGraphToken.mockResolvedValue(TOKEN);
25+
});
26+
27+
it("fetches user profile and maps all fields", async () => {
28+
mockState.fetchGraphJson.mockResolvedValue({
29+
id: "user-123",
30+
displayName: "Alice Smith",
31+
32+
jobTitle: "Engineer",
33+
userPrincipalName: "[email protected]",
34+
officeLocation: "Building 1",
35+
});
36+
37+
const result = await getMemberInfoMSTeams({
38+
cfg: {} as OpenClawConfig,
39+
userId: "user-123",
40+
});
41+
42+
expect(result).toEqual({
43+
user: {
44+
id: "user-123",
45+
displayName: "Alice Smith",
46+
47+
jobTitle: "Engineer",
48+
userPrincipalName: "[email protected]",
49+
officeLocation: "Building 1",
50+
},
51+
});
52+
expect(mockState.fetchGraphJson).toHaveBeenCalledWith({
53+
token: TOKEN,
54+
path: `/users/${encodeURIComponent("user-123")}?$select=id,displayName,mail,jobTitle,userPrincipalName,officeLocation`,
55+
});
56+
});
57+
58+
it("handles sparse data with some fields undefined", async () => {
59+
mockState.fetchGraphJson.mockResolvedValue({
60+
id: "user-456",
61+
displayName: "Bob",
62+
});
63+
64+
const result = await getMemberInfoMSTeams({
65+
cfg: {} as OpenClawConfig,
66+
userId: "user-456",
67+
});
68+
69+
expect(result).toEqual({
70+
user: {
71+
id: "user-456",
72+
displayName: "Bob",
73+
mail: undefined,
74+
jobTitle: undefined,
75+
userPrincipalName: undefined,
76+
officeLocation: undefined,
77+
},
78+
});
79+
});
80+
81+
it("propagates Graph API errors", async () => {
82+
mockState.fetchGraphJson.mockRejectedValue(new Error("Graph API 404: user not found"));
83+
84+
await expect(
85+
getMemberInfoMSTeams({
86+
cfg: {} as OpenClawConfig,
87+
userId: "nonexistent-user",
88+
}),
89+
).rejects.toThrow("Graph API 404: user not found");
90+
});
91+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { OpenClawConfig } from "../runtime-api.js";
2+
import { fetchGraphJson, resolveGraphToken } from "./graph.js";
3+
4+
type GraphUserProfile = {
5+
id?: string;
6+
displayName?: string;
7+
mail?: string;
8+
jobTitle?: string;
9+
userPrincipalName?: string;
10+
officeLocation?: string;
11+
};
12+
13+
export type GetMemberInfoMSTeamsParams = {
14+
cfg: OpenClawConfig;
15+
userId: string;
16+
};
17+
18+
export type GetMemberInfoMSTeamsResult = {
19+
user: {
20+
id: string | undefined;
21+
displayName: string | undefined;
22+
mail: string | undefined;
23+
jobTitle: string | undefined;
24+
userPrincipalName: string | undefined;
25+
officeLocation: string | undefined;
26+
};
27+
};
28+
29+
/**
30+
* Fetch a user profile from Microsoft Graph by user ID.
31+
*/
32+
export async function getMemberInfoMSTeams(
33+
params: GetMemberInfoMSTeamsParams,
34+
): Promise<GetMemberInfoMSTeamsResult> {
35+
const token = await resolveGraphToken(params.cfg);
36+
const path = `/users/${encodeURIComponent(params.userId)}?$select=id,displayName,mail,jobTitle,userPrincipalName,officeLocation`;
37+
const user = await fetchGraphJson<GraphUserProfile>({ token, path });
38+
return {
39+
user: {
40+
id: user.id,
41+
displayName: user.displayName,
42+
mail: user.mail,
43+
jobTitle: user.jobTitle,
44+
userPrincipalName: user.userPrincipalName,
45+
officeLocation: user.officeLocation,
46+
},
47+
};
48+
}

0 commit comments

Comments
 (0)