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,
|
RingCentralPost,
|
||||||
RingCentralUser,
|
RingCentralUser,
|
||||||
RingCentralAttachment,
|
RingCentralAttachment,
|
||||||
|
RingCentralAdaptiveCard,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
// Team Messaging API endpoints
|
// 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(
|
export async function probeRingCentral(
|
||||||
account: ResolvedRingCentralAccount,
|
account: ResolvedRingCentralAccount,
|
||||||
): Promise<{ ok: boolean; error?: string; elapsedMs: number }> {
|
): Promise<{ ok: boolean; error?: string; elapsedMs: number }> {
|
||||||
|
|||||||
@ -42,6 +42,7 @@ const RingCentralAccountSchemaBase = z
|
|||||||
allowBots: z.boolean().optional(),
|
allowBots: z.boolean().optional(),
|
||||||
botExtensionId: z.string().optional(),
|
botExtensionId: z.string().optional(),
|
||||||
selfOnly: z.boolean().optional(),
|
selfOnly: z.boolean().optional(),
|
||||||
|
useAdaptiveCards: z.boolean().optional(), // Use Adaptive Cards for code blocks (default: false)
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { toRingCentralMarkdown, needsMarkdownConversion } from "./markdown.js";
|
import { toRingCentralMarkdown, needsMarkdownConversion, hasCodeBlocks, markdownToAdaptiveCard } from "./markdown.js";
|
||||||
|
|
||||||
describe("toRingCentralMarkdown", () => {
|
describe("toRingCentralMarkdown", () => {
|
||||||
it("converts single underscore italic to asterisk", () => {
|
it("converts single underscore italic to asterisk", () => {
|
||||||
@ -119,3 +119,50 @@ describe("needsMarkdownConversion", () => {
|
|||||||
expect(needsMarkdownConversion("*italic* and **bold**")).toBe(false);
|
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)
|
/^#{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;
|
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 = {
|
export type RingCentralWebhookEvent = {
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
event?: string;
|
event?: string;
|
||||||
@ -113,6 +163,7 @@ export type RingCentralAccountConfig = {
|
|||||||
botExtensionId?: string;
|
botExtensionId?: string;
|
||||||
replyToMode?: "off" | "all";
|
replyToMode?: "off" | "all";
|
||||||
selfOnly?: boolean; // JWT mode: only accept messages from the JWT user in Personal chat (default: true)
|
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 & {
|
export type RingCentralConfig = RingCentralAccountConfig & {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user