feat: finalize msteams polls + outbound parity

This commit is contained in:
Peter Steinberger
2026-01-09 09:56:36 +01:00
parent a2bba7ef51
commit e55358c65d
22 changed files with 913 additions and 81 deletions

View File

@@ -10,7 +10,12 @@ import {
formatMSTeamsSendErrorHint,
formatUnknownError,
} from "./errors.js";
import { type MSTeamsAdapter, sendMSTeamsMessages } from "./messenger.js";
import {
buildConversationReference,
type MSTeamsAdapter,
sendMSTeamsMessages,
} from "./messenger.js";
import { buildMSTeamsPollCard } from "./polls.js";
import { resolveMSTeamsCredentials } from "./token.js";
let _log: ReturnType<typeof getChildLoggerFn> | undefined;
@@ -37,6 +42,25 @@ export type SendMSTeamsMessageResult = {
conversationId: string;
};
export type SendMSTeamsPollParams = {
/** Full config (for credentials) */
cfg: ClawdbotConfig;
/** Conversation ID or user ID to send to */
to: string;
/** Poll question */
question: string;
/** Poll options */
options: string[];
/** Max selections (defaults to 1) */
maxSelections?: number;
};
export type SendMSTeamsPollResult = {
pollId: string;
messageId: string;
conversationId: string;
};
/**
* Parse the --to argument into a conversation reference lookup key.
* Supported formats:
@@ -85,6 +109,37 @@ async function findConversationReference(recipient: {
return { conversationId: found.conversationId, ref: found.reference };
}
function extractMessageId(response: unknown): string | null {
if (!response || typeof response !== "object") return null;
if (!("id" in response)) return null;
const { id } = response as { id?: unknown };
if (typeof id !== "string" || !id) return null;
return id;
}
async function sendMSTeamsActivity(params: {
adapter: MSTeamsAdapter;
appId: string;
conversationRef: StoredConversationReference;
activity: Record<string, unknown>;
}): Promise<string> {
const baseRef = buildConversationReference(params.conversationRef);
const proactiveRef = {
...baseRef,
activityId: undefined,
};
let messageId = "unknown";
await params.adapter.continueConversation(
params.appId,
proactiveRef,
async (ctx) => {
const response = await ctx.sendActivity(params.activity);
messageId = extractMessageId(response) ?? "unknown";
},
);
return messageId;
}
/**
* Send a message to a Teams conversation or user.
*
@@ -181,6 +236,99 @@ export async function sendMessageMSTeams(
};
}
/**
* Send a poll (Adaptive Card) to a Teams conversation or user.
*/
export async function sendPollMSTeams(
params: SendMSTeamsPollParams,
): Promise<SendMSTeamsPollResult> {
const { cfg, to, question, options, maxSelections } = params;
const msteamsCfg = cfg.msteams;
if (!msteamsCfg?.enabled) {
throw new Error("msteams provider is not enabled");
}
const creds = resolveMSTeamsCredentials(msteamsCfg);
if (!creds) {
throw new Error("msteams credentials not configured");
}
const store = createMSTeamsConversationStoreFs();
const recipient = parseRecipient(to);
const found = await findConversationReference({ ...recipient, store });
if (!found) {
throw new Error(
`No conversation reference found for ${recipient.type}:${recipient.id}. ` +
`The bot must receive a message from this conversation before it can send proactively.`,
);
}
const { conversationId, ref } = found;
const log = await getLog();
const pollCard = buildMSTeamsPollCard({
question,
options,
maxSelections,
});
log.debug("sending poll", {
conversationId,
pollId: pollCard.pollId,
optionCount: pollCard.options.length,
});
const agentsHosting = await import("@microsoft/agents-hosting");
const { CloudAdapter, getAuthConfigWithDefaults } = agentsHosting;
const authConfig = getAuthConfigWithDefaults({
clientId: creds.appId,
clientSecret: creds.appPassword,
tenantId: creds.tenantId,
});
const adapter = new CloudAdapter(authConfig);
const activity = {
type: "message",
text: pollCard.fallbackText,
attachments: [
{
contentType: "application/vnd.microsoft.card.adaptive",
content: pollCard.card,
},
],
};
let messageId: string;
try {
messageId = await sendMSTeamsActivity({
adapter: adapter as unknown as MSTeamsAdapter,
appId: creds.appId,
conversationRef: ref,
activity,
});
} catch (err) {
const classification = classifyMSTeamsSendError(err);
const hint = formatMSTeamsSendErrorHint(classification);
const status = classification.statusCode
? ` (HTTP ${classification.statusCode})`
: "";
throw new Error(
`msteams poll send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
);
}
log.info("sent poll", { conversationId, pollId: pollCard.pollId, messageId });
return {
pollId: pollCard.pollId,
messageId,
conversationId,
};
}
/**
* List all known conversation references (for debugging/CLI).
*/