feat(ringcentral): add Adaptive Cards support
- Add sendRingCentralAdaptiveCard, updateRingCentralAdaptiveCard, deleteRingCentralAdaptiveCard APIs - Add RingCentralAdaptiveCard, RingCentralAdaptiveCardElement, RingCentralAdaptiveCardAction types - Add hasCodeBlocks() and markdownToAdaptiveCard() helpers for code block formatting - Add useAdaptiveCards config option (default: false) - Add 6 new tests for Adaptive Cards functionality
This commit is contained in:
parent
c84cd60b6d
commit
de00b3e335
@ -6,6 +6,7 @@ import type {
|
||||
RingCentralPost,
|
||||
RingCentralUser,
|
||||
RingCentralAttachment,
|
||||
RingCentralAdaptiveCard,
|
||||
} from "./types.js";
|
||||
|
||||
// Team Messaging API endpoints
|
||||
@ -164,6 +165,60 @@ export async function downloadRingCentralAttachment(params: {
|
||||
};
|
||||
}
|
||||
|
||||
// Adaptive Cards API
|
||||
export async function sendRingCentralAdaptiveCard(params: {
|
||||
account: ResolvedRingCentralAccount;
|
||||
chatId: string;
|
||||
card: RingCentralAdaptiveCard;
|
||||
fallbackText?: string;
|
||||
}): Promise<{ cardId?: string } | null> {
|
||||
const { account, chatId, card, fallbackText } = params;
|
||||
const platform = await getRingCentralPlatform(account);
|
||||
|
||||
const body = {
|
||||
...card,
|
||||
type: "AdaptiveCard",
|
||||
$schema: card.$schema ?? "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: card.version ?? "1.3",
|
||||
...(fallbackText ? { fallbackText } : {}),
|
||||
};
|
||||
|
||||
const response = await platform.post(`${TM_API_BASE}/chats/${chatId}/adaptive-cards`, body);
|
||||
const result = (await response.json()) as { id?: string };
|
||||
return result ? { cardId: result.id } : null;
|
||||
}
|
||||
|
||||
export async function updateRingCentralAdaptiveCard(params: {
|
||||
account: ResolvedRingCentralAccount;
|
||||
cardId: string;
|
||||
card: RingCentralAdaptiveCard;
|
||||
fallbackText?: string;
|
||||
}): Promise<{ cardId?: string }> {
|
||||
const { account, cardId, card, fallbackText } = params;
|
||||
const platform = await getRingCentralPlatform(account);
|
||||
|
||||
const body = {
|
||||
...card,
|
||||
type: "AdaptiveCard",
|
||||
$schema: card.$schema ?? "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: card.version ?? "1.3",
|
||||
...(fallbackText ? { fallbackText } : {}),
|
||||
};
|
||||
|
||||
const response = await platform.put(`${TM_API_BASE}/adaptive-cards/${cardId}`, body);
|
||||
const result = (await response.json()) as { id?: string };
|
||||
return { cardId: result.id };
|
||||
}
|
||||
|
||||
export async function deleteRingCentralAdaptiveCard(params: {
|
||||
account: ResolvedRingCentralAccount;
|
||||
cardId: string;
|
||||
}): Promise<void> {
|
||||
const { account, cardId } = params;
|
||||
const platform = await getRingCentralPlatform(account);
|
||||
await platform.delete(`${TM_API_BASE}/adaptive-cards/${cardId}`);
|
||||
}
|
||||
|
||||
export async function probeRingCentral(
|
||||
account: ResolvedRingCentralAccount,
|
||||
): Promise<{ ok: boolean; error?: string; elapsedMs: number }> {
|
||||
|
||||
@ -42,6 +42,7 @@ const RingCentralAccountSchemaBase = z
|
||||
allowBots: z.boolean().optional(),
|
||||
botExtensionId: z.string().optional(),
|
||||
selfOnly: z.boolean().optional(),
|
||||
useAdaptiveCards: z.boolean().optional(), // Use Adaptive Cards for code blocks (default: false)
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { toRingCentralMarkdown, needsMarkdownConversion } from "./markdown.js";
|
||||
import { toRingCentralMarkdown, needsMarkdownConversion, hasCodeBlocks, markdownToAdaptiveCard } from "./markdown.js";
|
||||
|
||||
describe("toRingCentralMarkdown", () => {
|
||||
it("converts single underscore italic to asterisk", () => {
|
||||
@ -119,3 +119,50 @@ describe("needsMarkdownConversion", () => {
|
||||
expect(needsMarkdownConversion("*italic* and **bold**")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasCodeBlocks", () => {
|
||||
it("returns true for text with code blocks", () => {
|
||||
expect(hasCodeBlocks("```\ncode\n```")).toBe(true);
|
||||
expect(hasCodeBlocks("```js\nconst x = 1;\n```")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for text without code blocks", () => {
|
||||
expect(hasCodeBlocks("plain text")).toBe(false);
|
||||
expect(hasCodeBlocks("`inline code`")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("markdownToAdaptiveCard", () => {
|
||||
it("converts simple text to adaptive card", () => {
|
||||
const result = markdownToAdaptiveCard("Hello world");
|
||||
expect(result.type).toBe("AdaptiveCard");
|
||||
expect(result.version).toBe("1.3");
|
||||
expect(result.body).toHaveLength(1);
|
||||
expect(result.body[0].type).toBe("TextBlock");
|
||||
expect(result.body[0].text).toBe("Hello world");
|
||||
});
|
||||
|
||||
it("converts code blocks with monospace font", () => {
|
||||
const result = markdownToAdaptiveCard("```js\nconst x = 1;\n```");
|
||||
expect(result.body).toHaveLength(1);
|
||||
expect(result.body[0].fontType).toBe("Monospace");
|
||||
expect(result.body[0].text).toBe("const x = 1;");
|
||||
});
|
||||
|
||||
it("handles mixed text and code blocks", () => {
|
||||
const result = markdownToAdaptiveCard("Some text\n\n```\ncode\n```\n\nMore text");
|
||||
expect(result.body).toHaveLength(3);
|
||||
expect(result.body[0].text).toBe("Some text");
|
||||
expect(result.body[1].fontType).toBe("Monospace");
|
||||
expect(result.body[1].text).toBe("code");
|
||||
expect(result.body[2].text).toBe("More text");
|
||||
});
|
||||
|
||||
it("converts headings with bold styling", () => {
|
||||
const result = markdownToAdaptiveCard("# Title");
|
||||
expect(result.body).toHaveLength(1);
|
||||
expect(result.body[0].weight).toBe("Bolder");
|
||||
expect(result.body[0].size).toBe("Medium");
|
||||
expect(result.body[0].text).toBe("Title");
|
||||
});
|
||||
});
|
||||
|
||||
@ -85,3 +85,74 @@ export function needsMarkdownConversion(text: string): boolean {
|
||||
/^#{1,6}\s+.+$/m.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if text contains code blocks that would benefit from Adaptive Card formatting
|
||||
*/
|
||||
export function hasCodeBlocks(text: string): boolean {
|
||||
return /```[\s\S]*?```/.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown text to an Adaptive Card structure.
|
||||
* This preserves code blocks with proper formatting.
|
||||
*/
|
||||
export function markdownToAdaptiveCard(text: string): {
|
||||
type: "AdaptiveCard";
|
||||
$schema: string;
|
||||
version: string;
|
||||
body: Array<{ type: string; text?: string; wrap?: boolean; fontType?: string; size?: string; weight?: string }>;
|
||||
} {
|
||||
const body: Array<{ type: string; text?: string; wrap?: boolean; fontType?: string; size?: string; weight?: string }> = [];
|
||||
|
||||
// Split by code blocks
|
||||
const parts = text.split(/(```[\s\S]*?```)/g);
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part.trim()) continue;
|
||||
|
||||
if (part.startsWith("```")) {
|
||||
// Extract language and code
|
||||
const match = part.match(/```(\w*)\n?([\s\S]*?)\n?```/);
|
||||
if (match) {
|
||||
const [, , code] = match;
|
||||
// Add code block with monospace font
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: code.trim(),
|
||||
wrap: true,
|
||||
fontType: "Monospace",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Regular text - convert markdown
|
||||
const converted = toRingCentralMarkdown(part.trim());
|
||||
if (converted) {
|
||||
// Check if it's a heading (starts with **)
|
||||
const headingMatch = converted.match(/^\*\*(.+)\*\*$/);
|
||||
if (headingMatch && !converted.includes("\n")) {
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: headingMatch[1],
|
||||
wrap: true,
|
||||
size: "Medium",
|
||||
weight: "Bolder",
|
||||
});
|
||||
} else {
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: converted,
|
||||
wrap: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "AdaptiveCard",
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.3",
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
@ -53,6 +53,56 @@ export type RingCentralMention = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
// Adaptive Cards types (Microsoft Adaptive Card schema v1.3)
|
||||
export type RingCentralAdaptiveCardElement = {
|
||||
type: string;
|
||||
text?: string;
|
||||
size?: "Small" | "Default" | "Medium" | "Large" | "ExtraLarge";
|
||||
weight?: "Lighter" | "Default" | "Bolder";
|
||||
color?: "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention";
|
||||
wrap?: boolean;
|
||||
url?: string;
|
||||
altText?: string;
|
||||
columns?: RingCentralAdaptiveCardElement[];
|
||||
items?: RingCentralAdaptiveCardElement[];
|
||||
width?: string | number;
|
||||
style?: string;
|
||||
isVisible?: boolean;
|
||||
id?: string;
|
||||
// Input elements
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
isRequired?: boolean;
|
||||
// Action elements
|
||||
title?: string;
|
||||
data?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type RingCentralAdaptiveCardAction = {
|
||||
type: "Action.Submit" | "Action.OpenUrl" | "Action.ShowCard" | "Action.ToggleVisibility";
|
||||
title?: string;
|
||||
url?: string;
|
||||
data?: Record<string, unknown>;
|
||||
card?: RingCentralAdaptiveCard;
|
||||
targetElements?: Array<string | { elementId: string; isVisible?: boolean }>;
|
||||
};
|
||||
|
||||
export type RingCentralAdaptiveCard = {
|
||||
type?: "AdaptiveCard";
|
||||
$schema?: string;
|
||||
version?: string;
|
||||
body?: RingCentralAdaptiveCardElement[];
|
||||
actions?: RingCentralAdaptiveCardAction[];
|
||||
fallbackText?: string;
|
||||
speak?: string;
|
||||
lang?: string;
|
||||
verticalContentAlignment?: "Top" | "Center" | "Bottom";
|
||||
backgroundImage?: string | { url: string; fillMode?: string };
|
||||
minHeight?: string;
|
||||
};
|
||||
|
||||
export type RingCentralWebhookEvent = {
|
||||
uuid?: string;
|
||||
event?: string;
|
||||
@ -113,6 +163,7 @@ export type RingCentralAccountConfig = {
|
||||
botExtensionId?: string;
|
||||
replyToMode?: "off" | "all";
|
||||
selfOnly?: boolean; // JWT mode: only accept messages from the JWT user in Personal chat (default: true)
|
||||
useAdaptiveCards?: boolean; // Use Adaptive Cards for messages with code blocks (default: false)
|
||||
};
|
||||
|
||||
export type RingCentralConfig = RingCentralAccountConfig & {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user