// Send a message (plain text or Adaptive Card) to a Microsoft Teams conversation
// using the Bot Framework REST API. Use this for proactive sends or as the
// outbound leg of a custom Teams bot.
type AzureBot = {
app_id: string;
app_password: string;
// Set only when the Azure Bot is configured as Single Tenant. Leave empty
// for Multi Tenant bots so the multi-tenant Bot Framework token endpoint is
// used.
tenant_id?: string;
};
type ConversationReference = {
bot: { id: string; name?: string };
channelId: string;
conversation: {
id: string;
tenantId?: string;
isGroup?: boolean;
conversationType?: string;
};
serviceUrl: string;
user?: { id: string; name?: string };
};
const BF_RESOURCE = "https://api.botframework.com";
const BF_TOKEN_ENDPOINT_MULTITENANT =
"https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token";
export async function main(
azure_bot: AzureBot,
conversation_reference: ConversationReference,
text: string,
card?: object,
): Promise<{ ok: true; activity_id?: string }> {
const token = await getBotAccessToken(azure_bot);
const activity: Record<string, unknown> = {
type: "message",
from: conversation_reference.bot,
recipient: conversation_reference.user ?? { id: "" },
conversation: conversation_reference.conversation,
};
if (card) {
activity.attachments = [
{ contentType: "application/vnd.microsoft.card.adaptive", content: card },
];
} else {
activity.text = text;
}
const url = `${conversation_reference.serviceUrl.replace(/\/$/, "")}/v3/conversations/${encodeURIComponent(conversation_reference.conversation.id)}/activities`;
const res = await fetch(url, {
method: "POST",
headers: {
authorization: `Bearer ${token}`,
"content-type": "application/json",
},
body: JSON.stringify(activity),
});
if (!res.ok) {
throw new Error(
`Bot Framework send failed: ${res.status} ${await res.text().catch(() => "")}`,
);
}
const body = (await res.json().catch(() => ({}))) as { id?: string };
return { ok: true, activity_id: body.id };
}
async function getBotAccessToken(azure: AzureBot): Promise<string> {
const isSingleTenant = !!azure.tenant_id;
const tokenEndpoint = isSingleTenant
? `https://login.microsoftonline.com/${azure.tenant_id}/oauth2/token`
: BF_TOKEN_ENDPOINT_MULTITENANT;
// Single-tenant bots use the v1 endpoint with `resource`; multi-tenant bots
// use the v2 botframework.com virtual directory with `scope=.../.default`.
const params = isSingleTenant
? new URLSearchParams({
grant_type: "client_credentials",
client_id: azure.app_id,
client_secret: azure.app_password,
resource: BF_RESOURCE,
})
: new URLSearchParams({
grant_type: "client_credentials",
client_id: azure.app_id,
client_secret: azure.app_password,
scope: `${BF_RESOURCE}/.default`,
});
const res = await fetch(tokenEndpoint, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: params,
});
if (!res.ok) {
throw new Error(
`AAD token fetch failed: ${res.status} ${await res.text().catch(() => "")}`,
);
}
const json = (await res.json()) as { access_token: string };
return json.access_token;
}
Submitted by hugo989 7 days ago