// teamId/channelId or already a conversation ID (19:xxx) — use directly if (!isUserTarget) { return cleaned;
}
// user:<aadId> — look up the conversation store for the real chat ID const store = createMSTeamsConversationStoreFs(); const found = await store.findPreferredDmByUserId(cleaned); if (!found) { thrownew Error(
`No conversation found for user:${cleaned}. ` + "The bot must receive a message from this user before Graph API operations work.",
);
}
// Prefer the cached Graph-native chat ID (19:xxx format) over the Bot Framework // conversation ID, which may be in a non-Graph format (a:xxx / 8:orgid:xxx) for // personal DMs. send-context.ts resolves and caches this on first send. if (found.reference.graphChatId) { return found.reference.graphChatId;
} if (found.conversationId.startsWith("19:")) { return found.conversationId;
} thrownew Error(
`Conversation for user:${cleaned} uses a Bot Framework ID (${found.conversationId}) ` + "that Graph API does not accept. Send a message to this user first so the Graph chat ID is cached.",
);
}
export function resolveConversationPath(to: string): {
kind: "chat" | "channel";
basePath: string;
chatId?: string;
teamId?: string;
channelId?: string;
} { const cleaned = stripTargetPrefix(to); if (cleaned.includes("/")) { const [teamId, channelId] = cleaned.split("/", 2); return {
kind: "channel",
basePath: `/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}`,
teamId,
channelId,
};
} // Conversation IDs like 19:xxx@thread.tacv2 may represent either group chats // or channel threads. Without a teamId/channelId pair (format "teamId/channelId") // we route through /chats/{id} which works for group chats and 1:1 DMs. // Channel operations that require /teams/{teamId}/channels/{channelId} paths // must be called with the explicit teamId/channelId target format. return {
kind: "chat",
basePath: `/chats/${encodeURIComponent(cleaned)}`,
chatId: cleaned,
};
}
export type GetMessageMSTeamsParams = {
cfg: OpenClawConfig;
to: string;
messageId: string;
};
if (conv.kind === "channel") { // Graph v1.0 does not expose pinnedMessages on channels — only on chats. // Attempting this would 404. thrownew Error( "Pin/unpin is not supported for channel messages on Graph v1.0. " + "Only chat conversations support pinned messages.",
);
}
// Graph API expects message@odata.bind with the full message resource URI const body = { "message@odata.bind": `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(params.messageId)}`,
}; const result = await postGraphJson<{ id?: string }>({
token,
path: `${conv.basePath}/pinnedMessages`,
body,
}); return { ok: true, pinnedMessageId: result.id };
}
export type UnpinMessageMSTeamsParams = {
cfg: OpenClawConfig;
to: string; /** The pinned-message resource ID returned by pin or list-pins (not the message ID). */
pinnedMessageId: string;
};
if (conv.kind === "channel") { thrownew Error( "Listing pinned messages is not supported for channels on Graph v1.0. " + "Only chat conversations support pinned messages.",
);
}
export type ReactionSummary = {
reactionType: string; /** Display name for the reaction (matches reactionType for known types). */
name: string; /** Emoji representation when available. */
emoji?: string;
count: number;
users: Array<{ id: string; displayName?: string }>;
};
export type ListReactionsMSTeamsResult = {
reactions: ReactionSummary[];
};
/** *Normalizeareactiontypestring.GraphsetReaction/unsetReactionaccepts *thewell-knownlegacynames(like,heart,laugh,surprised,sad,angry) *aswellasUnicodeemojivalues—sowepassunknowntypesthroughrather *thanrejectingthem.
*/ function normalizeReactionType(raw: string): string { const normalized = raw.trim(); if (!normalized) { thrownew Error(`Reaction type is required. Common types: ${TEAMS_REACTION_TYPES.join(", ")}`);
} // Lowercase only the well-known names; Unicode emoji should pass through as-is const lowered = normalized.toLowerCase(); if (TEAMS_REACTION_TYPES.includes(lowered as TeamsReactionType)) { return lowered;
} return normalized;
}
const grouped = new Map<
string,
{ count: number; users: Array<{ id: string; displayName?: string }> }
>(); for (const reaction of msg.reactions ?? []) { const type = reaction.reactionType ?? "unknown"; if (!grouped.has(type)) {
grouped.set(type, { count: 0, users: [] });
} const group = grouped.get(type)!; // Count every reaction regardless of whether the user ID is present // (deleted accounts, guests, or anonymous users may lack a user ID)
group.count++; if (reaction.user?.id) {
group.users.push({
id: reaction.user.id,
displayName: reaction.user.displayName,
});
}
}
// Strip double quotes from the query to prevent OData $search injection const sanitizedQuery = params.query.replace(/"/g, "");
// Build query string manually (not URLSearchParams) to preserve literal $ // in OData parameter names, consistent with other Graph calls in this module. const parts = [`$search=${encodeURIComponent(`"${sanitizedQuery}"`)}`];
parts.push(`$top=${top}`); if (params.from) {
parts.push(
`$filter=${encodeURIComponent(`from/user/displayName eq '${escapeOData(params.from)}'`)}`,
);
}
const path = `${basePath}/messages?${parts.join("&")}`; // ConsistencyLevel: eventual is required by Graph API for $search queries const res = await fetchGraphJson<GraphResponse<GraphMessage>>({
token,
path,
headers: { ConsistencyLevel: "eventual" },
});
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.