refactor(msteams): share Graph helpers

This commit is contained in:
Peter Steinberger
2026-02-15 00:20:58 +00:00
parent a8e4ab3ebe
commit c872a43146
3 changed files with 115 additions and 184 deletions
+13 -92
View File
@@ -1,95 +1,16 @@
import type { ChannelDirectoryEntry, MSTeamsConfig } from "openclaw/plugin-sdk"; import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk";
import { GRAPH_ROOT } from "./attachments/shared.js"; import {
import { loadMSTeamsSdkWithAuth } from "./sdk.js"; escapeOData,
import { resolveMSTeamsCredentials } from "./token.js"; fetchGraphJson,
type GraphChannel,
type GraphUser = { type GraphGroup,
id?: string; type GraphResponse,
displayName?: string; type GraphUser,
userPrincipalName?: string; listChannelsForTeam,
mail?: string; listTeamsByName,
}; normalizeQuery,
resolveGraphToken,
type GraphGroup = { } from "./graph.js";
id?: string;
displayName?: string;
};
type GraphChannel = {
id?: string;
displayName?: string;
};
type GraphResponse<T> = { value?: T[] };
function readAccessToken(value: unknown): string | null {
if (typeof value === "string") {
return value;
}
if (value && typeof value === "object") {
const token =
(value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
return typeof token === "string" ? token : null;
}
return null;
}
function normalizeQuery(value?: string | null): string {
return value?.trim() ?? "";
}
function escapeOData(value: string): string {
return value.replace(/'/g, "''");
}
async function fetchGraphJson<T>(params: {
token: string;
path: string;
headers?: Record<string, string>;
}): Promise<T> {
const res = await fetch(`${GRAPH_ROOT}${params.path}`, {
headers: {
Authorization: `Bearer ${params.token}`,
...params.headers,
},
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Graph ${params.path} failed (${res.status}): ${text || "unknown error"}`);
}
return (await res.json()) as T;
}
async function resolveGraphToken(cfg: unknown): Promise<string> {
const creds = resolveMSTeamsCredentials(
(cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined,
);
if (!creds) {
throw new Error("MS Teams credentials missing");
}
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
const token = await tokenProvider.getAccessToken("https://graph.microsoft.com");
const accessToken = readAccessToken(token);
if (!accessToken) {
throw new Error("MS Teams graph token unavailable");
}
return accessToken;
}
async function listTeamsByName(token: string, query: string): Promise<GraphGroup[]> {
const escaped = escapeOData(query);
const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
const path = `/groups?$filter=${encodeURIComponent(filter)}&$select=id,displayName`;
const res = await fetchGraphJson<GraphResponse<GraphGroup>>({ token, path });
return res.value ?? [];
}
async function listChannelsForTeam(token: string, teamId: string): Promise<GraphChannel[]> {
const path = `/teams/${encodeURIComponent(teamId)}/channels?$select=id,displayName`;
const res = await fetchGraphJson<GraphResponse<GraphChannel>>({ token, path });
return res.value ?? [];
}
export async function listMSTeamsDirectoryPeersLive(params: { export async function listMSTeamsDirectoryPeersLive(params: {
cfg: unknown; cfg: unknown;
+92
View File
@@ -0,0 +1,92 @@
import type { MSTeamsConfig } from "openclaw/plugin-sdk";
import { GRAPH_ROOT } from "./attachments/shared.js";
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
import { resolveMSTeamsCredentials } from "./token.js";
export type GraphUser = {
id?: string;
displayName?: string;
userPrincipalName?: string;
mail?: string;
};
export type GraphGroup = {
id?: string;
displayName?: string;
};
export type GraphChannel = {
id?: string;
displayName?: string;
};
export type GraphResponse<T> = { value?: T[] };
function readAccessToken(value: unknown): string | null {
if (typeof value === "string") {
return value;
}
if (value && typeof value === "object") {
const token =
(value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
return typeof token === "string" ? token : null;
}
return null;
}
export function normalizeQuery(value?: string | null): string {
return value?.trim() ?? "";
}
export function escapeOData(value: string): string {
return value.replace(/'/g, "''");
}
export async function fetchGraphJson<T>(params: {
token: string;
path: string;
headers?: Record<string, string>;
}): Promise<T> {
const res = await fetch(`${GRAPH_ROOT}${params.path}`, {
headers: {
Authorization: `Bearer ${params.token}`,
...params.headers,
},
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Graph ${params.path} failed (${res.status}): ${text || "unknown error"}`);
}
return (await res.json()) as T;
}
export async function resolveGraphToken(cfg: unknown): Promise<string> {
const creds = resolveMSTeamsCredentials(
(cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined,
);
if (!creds) {
throw new Error("MS Teams credentials missing");
}
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
const token = await tokenProvider.getAccessToken("https://graph.microsoft.com");
const accessToken = readAccessToken(token);
if (!accessToken) {
throw new Error("MS Teams graph token unavailable");
}
return accessToken;
}
export async function listTeamsByName(token: string, query: string): Promise<GraphGroup[]> {
const escaped = escapeOData(query);
const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
const path = `/groups?$filter=${encodeURIComponent(filter)}&$select=id,displayName`;
const res = await fetchGraphJson<GraphResponse<GraphGroup>>({ token, path });
return res.value ?? [];
}
export async function listChannelsForTeam(token: string, teamId: string): Promise<GraphChannel[]> {
const path = `/teams/${encodeURIComponent(teamId)}/channels?$select=id,displayName`;
const res = await fetchGraphJson<GraphResponse<GraphChannel>>({ token, path });
return res.value ?? [];
}
+10 -92
View File
@@ -1,26 +1,13 @@
import type { MSTeamsConfig } from "openclaw/plugin-sdk"; import {
import { GRAPH_ROOT } from "./attachments/shared.js"; escapeOData,
import { loadMSTeamsSdkWithAuth } from "./sdk.js"; fetchGraphJson,
import { resolveMSTeamsCredentials } from "./token.js"; type GraphResponse,
type GraphUser,
type GraphUser = { listChannelsForTeam,
id?: string; listTeamsByName,
displayName?: string; normalizeQuery,
userPrincipalName?: string; resolveGraphToken,
mail?: string; } from "./graph.js";
};
type GraphGroup = {
id?: string;
displayName?: string;
};
type GraphChannel = {
id?: string;
displayName?: string;
};
type GraphResponse<T> = { value?: T[] };
export type MSTeamsChannelResolution = { export type MSTeamsChannelResolution = {
input: string; input: string;
@@ -40,18 +27,6 @@ export type MSTeamsUserResolution = {
note?: string; note?: string;
}; };
function readAccessToken(value: unknown): string | null {
if (typeof value === "string") {
return value;
}
if (value && typeof value === "object") {
const token =
(value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
return typeof token === "string" ? token : null;
}
return null;
}
function stripProviderPrefix(raw: string): string { function stripProviderPrefix(raw: string): string {
return raw.replace(/^(msteams|teams):/i, ""); return raw.replace(/^(msteams|teams):/i, "");
} }
@@ -128,63 +103,6 @@ export function parseMSTeamsTeamEntry(
}; };
} }
function normalizeQuery(value?: string | null): string {
return value?.trim() ?? "";
}
function escapeOData(value: string): string {
return value.replace(/'/g, "''");
}
async function fetchGraphJson<T>(params: {
token: string;
path: string;
headers?: Record<string, string>;
}): Promise<T> {
const res = await fetch(`${GRAPH_ROOT}${params.path}`, {
headers: {
Authorization: `Bearer ${params.token}`,
...params.headers,
},
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Graph ${params.path} failed (${res.status}): ${text || "unknown error"}`);
}
return (await res.json()) as T;
}
async function resolveGraphToken(cfg: unknown): Promise<string> {
const creds = resolveMSTeamsCredentials(
(cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined,
);
if (!creds) {
throw new Error("MS Teams credentials missing");
}
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
const token = await tokenProvider.getAccessToken("https://graph.microsoft.com");
const accessToken = readAccessToken(token);
if (!accessToken) {
throw new Error("MS Teams graph token unavailable");
}
return accessToken;
}
async function listTeamsByName(token: string, query: string): Promise<GraphGroup[]> {
const escaped = escapeOData(query);
const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
const path = `/groups?$filter=${encodeURIComponent(filter)}&$select=id,displayName`;
const res = await fetchGraphJson<GraphResponse<GraphGroup>>({ token, path });
return res.value ?? [];
}
async function listChannelsForTeam(token: string, teamId: string): Promise<GraphChannel[]> {
const path = `/teams/${encodeURIComponent(teamId)}/channels?$select=id,displayName`;
const res = await fetchGraphJson<GraphResponse<GraphChannel>>({ token, path });
return res.value ?? [];
}
export async function resolveMSTeamsChannelAllowlist(params: { export async function resolveMSTeamsChannelAllowlist(params: {
cfg: unknown; cfg: unknown;
entries: string[]; entries: string[];