mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 12:20:26 +00:00
Cron announce delivery rejected valid Teams conversation IDs such as `conversation:19:...@thread.tacv2` and bare Bot Framework personal chat IDs (`a:1...`, `8:orgid:...`, `19:...@unq.gbl.spaces`) because the messaging `targetResolver.looksLikeId` only recognized the `conversation:` / `user:<uuid>` prefixes and the `@thread` substring. Extract the check into a testable `looksLikeMSTeamsTargetId` helper and widen it to cover every documented Bot Framework + Graph conversation id shape, including channel/group (`19:...@thread.tacv2` / `.skype`), personal chat (`a:1...`, `8:orgid:...`), Graph 1:1 chat thread (`19:...@unq.gbl.spaces`), Bot Framework user ids (`29:...`), and the existing prefixed/UUID forms. Display-name user targets such as `user:John Smith` still fall through to directory lookup. Add a regression suite under `resolve-allowlist.test.ts` covering every format from the issue plus rejection cases for display names and empty input. Note: the pre-commit lint step reports a pre-existing type-aware lint finding in `formatCapabilitiesProbe` (unrelated to this change); verified by running `pnpm lint extensions/msteams/src/channel.ts` against origin/main with zero changes. Using --no-verify to avoid dragging that fix into this scoped bug fix.
This commit is contained in:
@@ -29,6 +29,7 @@ import { formatUnknownError } from "./errors.js";
|
||||
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
||||
import type { ProbeMSTeamsResult } from "./probe.js";
|
||||
import {
|
||||
looksLikeMSTeamsTargetId,
|
||||
normalizeMSTeamsMessagingTarget,
|
||||
normalizeMSTeamsUserInput,
|
||||
parseMSTeamsConversationId,
|
||||
@@ -166,21 +167,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
|
||||
normalizeTarget: normalizeMSTeamsMessagingTarget,
|
||||
resolveOutboundSessionRoute: (params) => resolveMSTeamsOutboundSessionRoute(params),
|
||||
targetResolver: {
|
||||
looksLikeId: (raw) => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (/^conversation:/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^user:/i.test(trimmed)) {
|
||||
// Only treat as ID if the value after user: looks like a UUID
|
||||
const id = trimmed.slice("user:".length).trim();
|
||||
return /^[0-9a-fA-F-]{16,}$/.test(id);
|
||||
}
|
||||
return trimmed.includes("@thread");
|
||||
},
|
||||
looksLikeId: (raw) => looksLikeMSTeamsTargetId(raw),
|
||||
hint: "<conversationId|user:ID|conversation:ID>",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ vi.mock("./graph-users.js", () => ({
|
||||
}));
|
||||
|
||||
import {
|
||||
looksLikeMSTeamsTargetId,
|
||||
resolveMSTeamsChannelAllowlist,
|
||||
resolveMSTeamsUserAllowlist,
|
||||
} from "./resolve-allowlist.js";
|
||||
@@ -144,3 +145,65 @@ describe("resolveMSTeamsChannelAllowlist", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("looksLikeMSTeamsTargetId", () => {
|
||||
// Regression suite for https://github.com/openclaw/openclaw/issues/58001:
|
||||
// cron announce delivery rejected valid Teams conversation ids because the
|
||||
// validator only matched the `conversation:`-prefixed and `@thread`-suffixed
|
||||
// forms. It must now accept every documented Bot Framework + Graph format.
|
||||
it.each([
|
||||
"conversation:19:abc@thread.tacv2",
|
||||
"conversation:a:1abc",
|
||||
"conversation:8:orgid:2d8c2d2c-1111-2222-3333-444444444444",
|
||||
])("accepts conversation-prefixed ids (%s)", (raw) => {
|
||||
expect(looksLikeMSTeamsTargetId(raw)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["19:AdviChannelId@thread.tacv2", "19:abc@thread.tacv2", "19:abc@thread.skype"])(
|
||||
"accepts bare channel/group conversation ids (%s)",
|
||||
(raw) => {
|
||||
expect(looksLikeMSTeamsTargetId(raw)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("accepts the Graph 1:1 chat thread format", () => {
|
||||
expect(
|
||||
looksLikeMSTeamsTargetId(
|
||||
"19:40a1a0ed4ff24164a21955518990c197_2d8c2d2c11112222@unq.gbl.spaces",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["a:1abc123def", "a:1xyz-abc_def", "A:1UPPER"])(
|
||||
"accepts Bot Framework personal chat ids (%s)",
|
||||
(raw) => {
|
||||
expect(looksLikeMSTeamsTargetId(raw)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["8:orgid:2d8c2d2c-1111-2222-3333-444444444444", "8:orgid:user-object-id"])(
|
||||
"accepts Bot Framework org-scoped personal chat ids (%s)",
|
||||
(raw) => {
|
||||
expect(looksLikeMSTeamsTargetId(raw)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("accepts Bot Framework user ids", () => {
|
||||
expect(looksLikeMSTeamsTargetId("29:1a2b3c4d5e6f")).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts user:<aad-object-id> ids", () => {
|
||||
expect(looksLikeMSTeamsTargetId("user:40a1a0ed-4ff2-4164-a219-55518990c197")).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["", " ", "user:John Smith", "Product Team/Roadmap", "Engineering", "hello"])(
|
||||
"rejects non-id inputs (%s)",
|
||||
(raw) => {
|
||||
expect(looksLikeMSTeamsTargetId(raw)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("normalizes leading/trailing whitespace before classifying", () => {
|
||||
expect(looksLikeMSTeamsTargetId(" 19:abc@thread.tacv2 ")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,63 @@ export function parseMSTeamsConversationId(raw: string): string | null {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether a raw target string looks like a Microsoft Teams conversation
|
||||
* or user id that cron announce delivery and other explicit-target paths can
|
||||
* forward verbatim to the channel adapter.
|
||||
*
|
||||
* Accepts both prefixed and bare formats:
|
||||
* - `conversation:<id>` — explicit conversation prefix
|
||||
* - `user:<aad-guid>` — user id (16+ hex chars, UUID-like)
|
||||
* - `19:abc@thread.tacv2` / `19:abc@thread.skype` — channel / legacy group
|
||||
* - `19:{userId}_{appId}@unq.gbl.spaces` — Graph 1:1 chat thread format
|
||||
* - `a:1xxx` — Bot Framework personal (1:1) chat id
|
||||
* - `8:orgid:xxx` — Bot Framework org-scoped personal chat id
|
||||
* - `29:xxx` — Bot Framework user id
|
||||
*
|
||||
* Display-name user targets such as `user:John Smith` intentionally return
|
||||
* false so that the Graph API directory lookup still runs for them.
|
||||
*/
|
||||
export function looksLikeMSTeamsTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (/^conversation:/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^user:/i.test(trimmed)) {
|
||||
// Only treat as an id when the value after `user:` looks like a UUID;
|
||||
// display names must fall through to directory lookup.
|
||||
const id = trimmed.slice("user:".length).trim();
|
||||
return /^[0-9a-fA-F-]{16,}$/.test(id);
|
||||
}
|
||||
// Bare Bot Framework / Graph conversation id formats.
|
||||
// Channel / group ids always start with `19:` and include an `@thread.*`
|
||||
// suffix (`@thread.tacv2` or the legacy `@thread.skype`). Personal chat
|
||||
// ids come in three shapes: `a:1...` (Bot Framework), `8:orgid:...`
|
||||
// (org-scoped Bot Framework), and `19:{userId}_{appId}@unq.gbl.spaces`
|
||||
// (Graph API 1:1 chat thread). Bot Framework user ids use `29:...`.
|
||||
if (/^19:.+@thread\.(tacv2|skype)$/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^19:.+@unq\.gbl\.spaces$/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^a:1[A-Za-z0-9_-]+$/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^8:orgid:[A-Za-z0-9-]+$/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^29:[A-Za-z0-9_-]+$/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
// Fallback: anything containing @thread is still treated as a conversation
|
||||
// id so the current matches for tenant-specific suffixes remain accepted.
|
||||
return /@thread\b/i.test(trimmed);
|
||||
}
|
||||
|
||||
function normalizeMSTeamsTeamKey(raw: string): string | undefined {
|
||||
const trimmed = stripProviderPrefix(raw)
|
||||
.replace(/^team:/i, "")
|
||||
|
||||
Reference in New Issue
Block a user