feat: finalize msteams polls + outbound parity
This commit is contained in:
@@ -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).
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user