@chat-adapter/teams 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +180 -0
- package/dist/index.js +1004 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { CardElement, BaseFormatConverter, PostableMessage, Root, Adapter, ChatInstance, WebhookOptions, RawMessage, EmojiValue, FetchOptions, Message, ThreadInfo, FormattedContent } from 'chat';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Teams Adaptive Card converter for cross-platform cards.
|
|
5
|
+
*
|
|
6
|
+
* Converts CardElement to Microsoft Adaptive Cards format.
|
|
7
|
+
* @see https://adaptivecards.io/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface AdaptiveCard {
|
|
11
|
+
type: "AdaptiveCard";
|
|
12
|
+
$schema: string;
|
|
13
|
+
version: string;
|
|
14
|
+
body: AdaptiveCardElement[];
|
|
15
|
+
actions?: AdaptiveCardAction[];
|
|
16
|
+
}
|
|
17
|
+
interface AdaptiveCardElement {
|
|
18
|
+
type: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
interface AdaptiveCardAction {
|
|
22
|
+
type: string;
|
|
23
|
+
title: string;
|
|
24
|
+
data?: Record<string, unknown>;
|
|
25
|
+
style?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Convert a CardElement to a Teams Adaptive Card.
|
|
29
|
+
*/
|
|
30
|
+
declare function cardToAdaptiveCard(card: CardElement): AdaptiveCard;
|
|
31
|
+
/**
|
|
32
|
+
* Generate fallback text from a card element.
|
|
33
|
+
* Used when adaptive cards aren't supported.
|
|
34
|
+
*/
|
|
35
|
+
declare function cardToFallbackText(card: CardElement): string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Teams-specific format conversion using AST-based parsing.
|
|
39
|
+
*
|
|
40
|
+
* Teams supports a subset of HTML for formatting:
|
|
41
|
+
* - Bold: <b> or <strong>
|
|
42
|
+
* - Italic: <i> or <em>
|
|
43
|
+
* - Strikethrough: <s> or <strike>
|
|
44
|
+
* - Links: <a href="url">text</a>
|
|
45
|
+
* - Code: <pre> and <code>
|
|
46
|
+
*
|
|
47
|
+
* Teams also accepts standard markdown in most cases.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
declare class TeamsFormatConverter extends BaseFormatConverter {
|
|
51
|
+
/**
|
|
52
|
+
* Convert @mentions to Teams format in plain text.
|
|
53
|
+
* @name → <at>name</at>
|
|
54
|
+
*/
|
|
55
|
+
private convertMentionsToTeams;
|
|
56
|
+
/**
|
|
57
|
+
* Override renderPostable to convert @mentions in plain strings.
|
|
58
|
+
*/
|
|
59
|
+
renderPostable(message: PostableMessage): string;
|
|
60
|
+
/**
|
|
61
|
+
* Render an AST to Teams format.
|
|
62
|
+
* Teams accepts standard markdown, so we just stringify cleanly.
|
|
63
|
+
*/
|
|
64
|
+
fromAst(ast: Root): string;
|
|
65
|
+
/**
|
|
66
|
+
* Parse Teams message into an AST.
|
|
67
|
+
* Converts Teams HTML/mentions to standard markdown format.
|
|
68
|
+
*/
|
|
69
|
+
toAst(teamsText: string): Root;
|
|
70
|
+
private nodeToTeams;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface TeamsAdapterConfig {
|
|
74
|
+
/** Microsoft App ID */
|
|
75
|
+
appId: string;
|
|
76
|
+
/** Microsoft App Password */
|
|
77
|
+
appPassword: string;
|
|
78
|
+
/** Microsoft App Type */
|
|
79
|
+
appType?: "MultiTenant" | "SingleTenant";
|
|
80
|
+
/** Microsoft App Tenant ID */
|
|
81
|
+
appTenantId?: string;
|
|
82
|
+
/** Override bot username (optional) */
|
|
83
|
+
userName?: string;
|
|
84
|
+
}
|
|
85
|
+
/** Teams-specific thread ID data */
|
|
86
|
+
interface TeamsThreadId {
|
|
87
|
+
conversationId: string;
|
|
88
|
+
serviceUrl: string;
|
|
89
|
+
replyToId?: string;
|
|
90
|
+
}
|
|
91
|
+
declare class TeamsAdapter implements Adapter<TeamsThreadId, unknown> {
|
|
92
|
+
readonly name = "teams";
|
|
93
|
+
readonly userName: string;
|
|
94
|
+
readonly botUserId?: string;
|
|
95
|
+
private botAdapter;
|
|
96
|
+
private chat;
|
|
97
|
+
private logger;
|
|
98
|
+
private formatConverter;
|
|
99
|
+
private config;
|
|
100
|
+
constructor(config: TeamsAdapterConfig);
|
|
101
|
+
initialize(chat: ChatInstance): Promise<void>;
|
|
102
|
+
handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
|
|
103
|
+
private handleTurn;
|
|
104
|
+
/**
|
|
105
|
+
* Handle Action.Submit button clicks sent as message activities.
|
|
106
|
+
* Teams sends these with type "message" and value.actionId.
|
|
107
|
+
*/
|
|
108
|
+
private handleMessageAction;
|
|
109
|
+
/**
|
|
110
|
+
* Handle invoke activities (adaptive card actions, etc.).
|
|
111
|
+
*/
|
|
112
|
+
private handleInvokeActivity;
|
|
113
|
+
/**
|
|
114
|
+
* Handle adaptive card button clicks.
|
|
115
|
+
* The action data is in activity.value with our { actionId, value } structure.
|
|
116
|
+
*/
|
|
117
|
+
private handleAdaptiveCardAction;
|
|
118
|
+
/**
|
|
119
|
+
* Handle Teams reaction events (reactionsAdded/reactionsRemoved).
|
|
120
|
+
*/
|
|
121
|
+
private handleReactionActivity;
|
|
122
|
+
private parseTeamsMessage;
|
|
123
|
+
/**
|
|
124
|
+
* Create an Attachment object from a Teams attachment.
|
|
125
|
+
*/
|
|
126
|
+
private createAttachment;
|
|
127
|
+
private normalizeMentions;
|
|
128
|
+
postMessage(threadId: string, message: PostableMessage): Promise<RawMessage<unknown>>;
|
|
129
|
+
/**
|
|
130
|
+
* Extract card element from a PostableMessage if present.
|
|
131
|
+
*/
|
|
132
|
+
private extractCard;
|
|
133
|
+
/**
|
|
134
|
+
* Extract files from a PostableMessage if present.
|
|
135
|
+
*/
|
|
136
|
+
private extractFiles;
|
|
137
|
+
/**
|
|
138
|
+
* Convert files to Teams attachments.
|
|
139
|
+
* Uses inline data URIs for small files.
|
|
140
|
+
*/
|
|
141
|
+
private filesToAttachments;
|
|
142
|
+
editMessage(threadId: string, messageId: string, message: PostableMessage): Promise<RawMessage<unknown>>;
|
|
143
|
+
deleteMessage(threadId: string, messageId: string): Promise<void>;
|
|
144
|
+
addReaction(_threadId: string, _messageId: string, _emoji: EmojiValue | string): Promise<void>;
|
|
145
|
+
removeReaction(_threadId: string, _messageId: string, _emoji: EmojiValue | string): Promise<void>;
|
|
146
|
+
startTyping(threadId: string): Promise<void>;
|
|
147
|
+
/**
|
|
148
|
+
* Open a direct message conversation with a user.
|
|
149
|
+
* Returns a thread ID that can be used to post messages.
|
|
150
|
+
*
|
|
151
|
+
* The serviceUrl and tenantId are automatically resolved from cached user interactions.
|
|
152
|
+
* If no cached values are found, defaults are used (which may not work for all tenants).
|
|
153
|
+
*/
|
|
154
|
+
openDM(userId: string): Promise<string>;
|
|
155
|
+
fetchMessages(_threadId: string, _options?: FetchOptions): Promise<Message<unknown>[]>;
|
|
156
|
+
fetchThread(threadId: string): Promise<ThreadInfo>;
|
|
157
|
+
encodeThreadId(platformData: TeamsThreadId): string;
|
|
158
|
+
/**
|
|
159
|
+
* Check if a thread is a direct message conversation.
|
|
160
|
+
* Teams DMs have conversation IDs that don't start with "19:" (which is for groups/channels).
|
|
161
|
+
*/
|
|
162
|
+
isDM(threadId: string): boolean;
|
|
163
|
+
decodeThreadId(threadId: string): TeamsThreadId;
|
|
164
|
+
parseMessage(raw: unknown): Message<unknown>;
|
|
165
|
+
/**
|
|
166
|
+
* Check if a Teams activity is from this bot.
|
|
167
|
+
*
|
|
168
|
+
* Teams bot IDs can appear in different formats:
|
|
169
|
+
* - Just the app ID: "abc123-def456-..."
|
|
170
|
+
* - With prefix: "28:abc123-def456-..."
|
|
171
|
+
*
|
|
172
|
+
* We check both exact match and suffix match (after colon delimiter)
|
|
173
|
+
* to handle all formats safely.
|
|
174
|
+
*/
|
|
175
|
+
private isMessageFromSelf;
|
|
176
|
+
renderFormatted(content: FormattedContent): string;
|
|
177
|
+
}
|
|
178
|
+
declare function createTeamsAdapter(config: TeamsAdapterConfig): TeamsAdapter;
|
|
179
|
+
|
|
180
|
+
export { TeamsAdapter, type TeamsAdapterConfig, TeamsFormatConverter, type TeamsThreadId, cardToAdaptiveCard, cardToFallbackText, createTeamsAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
ActivityTypes,
|
|
4
|
+
CloudAdapter,
|
|
5
|
+
ConfigurationBotFrameworkAuthentication
|
|
6
|
+
} from "botbuilder";
|
|
7
|
+
import {
|
|
8
|
+
convertEmojiPlaceholders as convertEmojiPlaceholders2,
|
|
9
|
+
defaultEmojiResolver,
|
|
10
|
+
isCardElement,
|
|
11
|
+
NotImplementedError
|
|
12
|
+
} from "chat";
|
|
13
|
+
|
|
14
|
+
// src/cards.ts
|
|
15
|
+
import {
|
|
16
|
+
convertEmojiPlaceholders
|
|
17
|
+
} from "chat";
|
|
18
|
+
function convertEmoji(text) {
|
|
19
|
+
return convertEmojiPlaceholders(text, "teams");
|
|
20
|
+
}
|
|
21
|
+
var ADAPTIVE_CARD_SCHEMA = "http://adaptivecards.io/schemas/adaptive-card.json";
|
|
22
|
+
var ADAPTIVE_CARD_VERSION = "1.4";
|
|
23
|
+
function cardToAdaptiveCard(card) {
|
|
24
|
+
const body = [];
|
|
25
|
+
const actions = [];
|
|
26
|
+
if (card.title) {
|
|
27
|
+
body.push({
|
|
28
|
+
type: "TextBlock",
|
|
29
|
+
text: convertEmoji(card.title),
|
|
30
|
+
weight: "bolder",
|
|
31
|
+
size: "large",
|
|
32
|
+
wrap: true
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (card.subtitle) {
|
|
36
|
+
body.push({
|
|
37
|
+
type: "TextBlock",
|
|
38
|
+
text: convertEmoji(card.subtitle),
|
|
39
|
+
isSubtle: true,
|
|
40
|
+
wrap: true
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (card.imageUrl) {
|
|
44
|
+
body.push({
|
|
45
|
+
type: "Image",
|
|
46
|
+
url: card.imageUrl,
|
|
47
|
+
size: "stretch"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
for (const child of card.children) {
|
|
51
|
+
const result = convertChildToAdaptive(child);
|
|
52
|
+
body.push(...result.elements);
|
|
53
|
+
actions.push(...result.actions);
|
|
54
|
+
}
|
|
55
|
+
const adaptiveCard = {
|
|
56
|
+
type: "AdaptiveCard",
|
|
57
|
+
$schema: ADAPTIVE_CARD_SCHEMA,
|
|
58
|
+
version: ADAPTIVE_CARD_VERSION,
|
|
59
|
+
body
|
|
60
|
+
};
|
|
61
|
+
if (actions.length > 0) {
|
|
62
|
+
adaptiveCard.actions = actions;
|
|
63
|
+
}
|
|
64
|
+
return adaptiveCard;
|
|
65
|
+
}
|
|
66
|
+
function convertChildToAdaptive(child) {
|
|
67
|
+
switch (child.type) {
|
|
68
|
+
case "text":
|
|
69
|
+
return { elements: [convertTextToElement(child)], actions: [] };
|
|
70
|
+
case "image":
|
|
71
|
+
return { elements: [convertImageToElement(child)], actions: [] };
|
|
72
|
+
case "divider":
|
|
73
|
+
return { elements: [convertDividerToElement(child)], actions: [] };
|
|
74
|
+
case "actions":
|
|
75
|
+
return convertActionsToElements(child);
|
|
76
|
+
case "section":
|
|
77
|
+
return convertSectionToElements(child);
|
|
78
|
+
case "fields":
|
|
79
|
+
return { elements: [convertFieldsToElement(child)], actions: [] };
|
|
80
|
+
default:
|
|
81
|
+
return { elements: [], actions: [] };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function convertTextToElement(element) {
|
|
85
|
+
const textBlock = {
|
|
86
|
+
type: "TextBlock",
|
|
87
|
+
text: convertEmoji(element.content),
|
|
88
|
+
wrap: true
|
|
89
|
+
};
|
|
90
|
+
if (element.style === "bold") {
|
|
91
|
+
textBlock.weight = "bolder";
|
|
92
|
+
} else if (element.style === "muted") {
|
|
93
|
+
textBlock.isSubtle = true;
|
|
94
|
+
}
|
|
95
|
+
return textBlock;
|
|
96
|
+
}
|
|
97
|
+
function convertImageToElement(element) {
|
|
98
|
+
return {
|
|
99
|
+
type: "Image",
|
|
100
|
+
url: element.url,
|
|
101
|
+
altText: element.alt || "Image",
|
|
102
|
+
size: "auto"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function convertDividerToElement(_element) {
|
|
106
|
+
return {
|
|
107
|
+
type: "Container",
|
|
108
|
+
separator: true,
|
|
109
|
+
items: []
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function convertActionsToElements(element) {
|
|
113
|
+
const actions = element.children.map(
|
|
114
|
+
(button) => convertButtonToAction(button)
|
|
115
|
+
);
|
|
116
|
+
return { elements: [], actions };
|
|
117
|
+
}
|
|
118
|
+
function convertButtonToAction(button) {
|
|
119
|
+
const action = {
|
|
120
|
+
type: "Action.Submit",
|
|
121
|
+
title: convertEmoji(button.label),
|
|
122
|
+
data: {
|
|
123
|
+
actionId: button.id,
|
|
124
|
+
value: button.value
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
if (button.style === "primary") {
|
|
128
|
+
action.style = "positive";
|
|
129
|
+
} else if (button.style === "danger") {
|
|
130
|
+
action.style = "destructive";
|
|
131
|
+
}
|
|
132
|
+
return action;
|
|
133
|
+
}
|
|
134
|
+
function convertSectionToElements(element) {
|
|
135
|
+
const elements = [];
|
|
136
|
+
const actions = [];
|
|
137
|
+
const containerItems = [];
|
|
138
|
+
for (const child of element.children) {
|
|
139
|
+
const result = convertChildToAdaptive(child);
|
|
140
|
+
containerItems.push(...result.elements);
|
|
141
|
+
actions.push(...result.actions);
|
|
142
|
+
}
|
|
143
|
+
if (containerItems.length > 0) {
|
|
144
|
+
elements.push({
|
|
145
|
+
type: "Container",
|
|
146
|
+
items: containerItems
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return { elements, actions };
|
|
150
|
+
}
|
|
151
|
+
function convertFieldsToElement(element) {
|
|
152
|
+
const facts = element.children.map((field) => ({
|
|
153
|
+
title: convertEmoji(field.label),
|
|
154
|
+
value: convertEmoji(field.value)
|
|
155
|
+
}));
|
|
156
|
+
return {
|
|
157
|
+
type: "FactSet",
|
|
158
|
+
facts
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function cardToFallbackText(card) {
|
|
162
|
+
const parts = [];
|
|
163
|
+
if (card.title) {
|
|
164
|
+
parts.push(`**${convertEmoji(card.title)}**`);
|
|
165
|
+
}
|
|
166
|
+
if (card.subtitle) {
|
|
167
|
+
parts.push(convertEmoji(card.subtitle));
|
|
168
|
+
}
|
|
169
|
+
for (const child of card.children) {
|
|
170
|
+
const text = childToFallbackText(child);
|
|
171
|
+
if (text) {
|
|
172
|
+
parts.push(text);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return parts.join("\n\n");
|
|
176
|
+
}
|
|
177
|
+
function childToFallbackText(child) {
|
|
178
|
+
switch (child.type) {
|
|
179
|
+
case "text":
|
|
180
|
+
return convertEmoji(child.content);
|
|
181
|
+
case "fields":
|
|
182
|
+
return child.children.map((f) => `**${convertEmoji(f.label)}**: ${convertEmoji(f.value)}`).join("\n");
|
|
183
|
+
case "actions":
|
|
184
|
+
return `[${child.children.map((b) => convertEmoji(b.label)).join("] [")}]`;
|
|
185
|
+
case "section":
|
|
186
|
+
return child.children.map((c) => childToFallbackText(c)).filter(Boolean).join("\n");
|
|
187
|
+
default:
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/markdown.ts
|
|
193
|
+
import {
|
|
194
|
+
BaseFormatConverter,
|
|
195
|
+
parseMarkdown
|
|
196
|
+
} from "chat";
|
|
197
|
+
var TeamsFormatConverter = class extends BaseFormatConverter {
|
|
198
|
+
/**
|
|
199
|
+
* Convert @mentions to Teams format in plain text.
|
|
200
|
+
* @name → <at>name</at>
|
|
201
|
+
*/
|
|
202
|
+
convertMentionsToTeams(text) {
|
|
203
|
+
return text.replace(/@(\w+)/g, "<at>$1</at>");
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Override renderPostable to convert @mentions in plain strings.
|
|
207
|
+
*/
|
|
208
|
+
renderPostable(message) {
|
|
209
|
+
if (typeof message === "string") {
|
|
210
|
+
return this.convertMentionsToTeams(message);
|
|
211
|
+
}
|
|
212
|
+
if ("raw" in message) {
|
|
213
|
+
return this.convertMentionsToTeams(message.raw);
|
|
214
|
+
}
|
|
215
|
+
if ("markdown" in message) {
|
|
216
|
+
return this.fromAst(parseMarkdown(message.markdown));
|
|
217
|
+
}
|
|
218
|
+
if ("ast" in message) {
|
|
219
|
+
return this.fromAst(message.ast);
|
|
220
|
+
}
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Render an AST to Teams format.
|
|
225
|
+
* Teams accepts standard markdown, so we just stringify cleanly.
|
|
226
|
+
*/
|
|
227
|
+
fromAst(ast) {
|
|
228
|
+
const parts = [];
|
|
229
|
+
for (const node of ast.children) {
|
|
230
|
+
parts.push(this.nodeToTeams(node));
|
|
231
|
+
}
|
|
232
|
+
return parts.join("\n\n");
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Parse Teams message into an AST.
|
|
236
|
+
* Converts Teams HTML/mentions to standard markdown format.
|
|
237
|
+
*/
|
|
238
|
+
toAst(teamsText) {
|
|
239
|
+
let markdown = teamsText;
|
|
240
|
+
markdown = markdown.replace(/<at>([^<]+)<\/at>/gi, "@$1");
|
|
241
|
+
markdown = markdown.replace(
|
|
242
|
+
/<(b|strong)>([^<]+)<\/(b|strong)>/gi,
|
|
243
|
+
"**$2**"
|
|
244
|
+
);
|
|
245
|
+
markdown = markdown.replace(/<(i|em)>([^<]+)<\/(i|em)>/gi, "_$2_");
|
|
246
|
+
markdown = markdown.replace(
|
|
247
|
+
/<(s|strike)>([^<]+)<\/(s|strike)>/gi,
|
|
248
|
+
"~~$2~~"
|
|
249
|
+
);
|
|
250
|
+
markdown = markdown.replace(
|
|
251
|
+
/<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi,
|
|
252
|
+
"[$2]($1)"
|
|
253
|
+
);
|
|
254
|
+
markdown = markdown.replace(/<code>([^<]+)<\/code>/gi, "`$1`");
|
|
255
|
+
markdown = markdown.replace(/<pre>([^<]+)<\/pre>/gi, "```\n$1\n```");
|
|
256
|
+
markdown = markdown.replace(/<[^>]+>/g, "");
|
|
257
|
+
markdown = markdown.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
258
|
+
return parseMarkdown(markdown);
|
|
259
|
+
}
|
|
260
|
+
nodeToTeams(node) {
|
|
261
|
+
switch (node.type) {
|
|
262
|
+
case "paragraph":
|
|
263
|
+
return node.children.map((child) => this.nodeToTeams(child)).join("");
|
|
264
|
+
case "text": {
|
|
265
|
+
const textValue = node.value;
|
|
266
|
+
return textValue.replace(/@(\w+)/g, "<at>$1</at>");
|
|
267
|
+
}
|
|
268
|
+
case "strong":
|
|
269
|
+
return `**${node.children.map((child) => this.nodeToTeams(child)).join("")}**`;
|
|
270
|
+
case "emphasis":
|
|
271
|
+
return `_${node.children.map((child) => this.nodeToTeams(child)).join("")}_`;
|
|
272
|
+
case "delete":
|
|
273
|
+
return `~~${node.children.map((child) => this.nodeToTeams(child)).join("")}~~`;
|
|
274
|
+
case "inlineCode":
|
|
275
|
+
return `\`${node.value}\``;
|
|
276
|
+
case "code": {
|
|
277
|
+
const codeNode = node;
|
|
278
|
+
return `\`\`\`${codeNode.lang || ""}
|
|
279
|
+
${codeNode.value}
|
|
280
|
+
\`\`\``;
|
|
281
|
+
}
|
|
282
|
+
case "link": {
|
|
283
|
+
const linkNode = node;
|
|
284
|
+
const linkText = linkNode.children.map((child) => this.nodeToTeams(child)).join("");
|
|
285
|
+
return `[${linkText}](${linkNode.url})`;
|
|
286
|
+
}
|
|
287
|
+
case "blockquote":
|
|
288
|
+
return node.children.map((child) => `> ${this.nodeToTeams(child)}`).join("\n");
|
|
289
|
+
case "list":
|
|
290
|
+
return node.children.map((item, i) => {
|
|
291
|
+
const prefix = node.ordered ? `${i + 1}.` : "-";
|
|
292
|
+
const content = item.children.map((child) => this.nodeToTeams(child)).join("");
|
|
293
|
+
return `${prefix} ${content}`;
|
|
294
|
+
}).join("\n");
|
|
295
|
+
case "listItem":
|
|
296
|
+
return node.children.map((child) => this.nodeToTeams(child)).join("");
|
|
297
|
+
case "break":
|
|
298
|
+
return "\n";
|
|
299
|
+
case "thematicBreak":
|
|
300
|
+
return "---";
|
|
301
|
+
default:
|
|
302
|
+
if ("children" in node && Array.isArray(node.children)) {
|
|
303
|
+
return node.children.map((child) => this.nodeToTeams(child)).join("");
|
|
304
|
+
}
|
|
305
|
+
if ("value" in node) {
|
|
306
|
+
return String(node.value);
|
|
307
|
+
}
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/index.ts
|
|
314
|
+
var ServerlessCloudAdapter = class extends CloudAdapter {
|
|
315
|
+
handleActivity(authHeader, activity, logic) {
|
|
316
|
+
return this.processActivity(authHeader, activity, logic);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
var TeamsAdapter = class {
|
|
320
|
+
name = "teams";
|
|
321
|
+
userName;
|
|
322
|
+
botUserId;
|
|
323
|
+
botAdapter;
|
|
324
|
+
chat = null;
|
|
325
|
+
logger = null;
|
|
326
|
+
formatConverter = new TeamsFormatConverter();
|
|
327
|
+
config;
|
|
328
|
+
constructor(config) {
|
|
329
|
+
this.config = config;
|
|
330
|
+
this.userName = config.userName || "bot";
|
|
331
|
+
if (config.appType === "SingleTenant" && !config.appTenantId) {
|
|
332
|
+
throw new Error("appTenantId is required for SingleTenant app type");
|
|
333
|
+
}
|
|
334
|
+
const auth = new ConfigurationBotFrameworkAuthentication({
|
|
335
|
+
MicrosoftAppId: config.appId,
|
|
336
|
+
MicrosoftAppPassword: config.appPassword,
|
|
337
|
+
MicrosoftAppType: config.appType || "MultiTenant",
|
|
338
|
+
MicrosoftAppTenantId: config.appType === "SingleTenant" ? config.appTenantId : void 0
|
|
339
|
+
});
|
|
340
|
+
this.botAdapter = new ServerlessCloudAdapter(auth);
|
|
341
|
+
}
|
|
342
|
+
async initialize(chat) {
|
|
343
|
+
this.chat = chat;
|
|
344
|
+
this.logger = chat.getLogger(this.name);
|
|
345
|
+
}
|
|
346
|
+
async handleWebhook(request, options) {
|
|
347
|
+
const body = await request.text();
|
|
348
|
+
this.logger?.debug("Teams webhook raw body", { body });
|
|
349
|
+
let activity;
|
|
350
|
+
try {
|
|
351
|
+
activity = JSON.parse(body);
|
|
352
|
+
} catch (e) {
|
|
353
|
+
this.logger?.error("Failed to parse request body", { error: e });
|
|
354
|
+
return new Response("Invalid JSON", { status: 400 });
|
|
355
|
+
}
|
|
356
|
+
const authHeader = request.headers.get("authorization") || "";
|
|
357
|
+
try {
|
|
358
|
+
await this.botAdapter.handleActivity(
|
|
359
|
+
authHeader,
|
|
360
|
+
activity,
|
|
361
|
+
async (context) => {
|
|
362
|
+
await this.handleTurn(context, options);
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
return new Response(JSON.stringify({}), {
|
|
366
|
+
status: 200,
|
|
367
|
+
headers: { "Content-Type": "application/json" }
|
|
368
|
+
});
|
|
369
|
+
} catch (error) {
|
|
370
|
+
this.logger?.error("Bot adapter process error", { error });
|
|
371
|
+
return new Response(JSON.stringify({ error: "Internal error" }), {
|
|
372
|
+
status: 500,
|
|
373
|
+
headers: { "Content-Type": "application/json" }
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async handleTurn(context, options) {
|
|
378
|
+
if (!this.chat) {
|
|
379
|
+
this.logger?.warn("Chat instance not initialized, ignoring event");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const activity = context.activity;
|
|
383
|
+
if (activity.from?.id && activity.serviceUrl) {
|
|
384
|
+
const userId = activity.from.id;
|
|
385
|
+
const tenantId = activity.channelData?.tenant?.id;
|
|
386
|
+
const ttl = 30 * 24 * 60 * 60 * 1e3;
|
|
387
|
+
this.chat.getState().set(`teams:serviceUrl:${userId}`, activity.serviceUrl, ttl);
|
|
388
|
+
if (tenantId) {
|
|
389
|
+
this.chat.getState().set(`teams:tenantId:${userId}`, tenantId, ttl);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (activity.type === ActivityTypes.MessageReaction) {
|
|
393
|
+
this.handleReactionActivity(activity, options);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (activity.type === ActivityTypes.Invoke) {
|
|
397
|
+
await this.handleInvokeActivity(context, options);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (activity.type !== ActivityTypes.Message) {
|
|
401
|
+
this.logger?.debug("Ignoring non-message activity", {
|
|
402
|
+
type: activity.type
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const actionValue = activity.value;
|
|
407
|
+
if (actionValue?.actionId) {
|
|
408
|
+
this.handleMessageAction(activity, actionValue, options);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const threadId = this.encodeThreadId({
|
|
412
|
+
conversationId: activity.conversation?.id || "",
|
|
413
|
+
serviceUrl: activity.serviceUrl || "",
|
|
414
|
+
replyToId: activity.replyToId
|
|
415
|
+
});
|
|
416
|
+
this.chat.processMessage(
|
|
417
|
+
this,
|
|
418
|
+
threadId,
|
|
419
|
+
this.parseTeamsMessage(activity, threadId),
|
|
420
|
+
options
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Handle Action.Submit button clicks sent as message activities.
|
|
425
|
+
* Teams sends these with type "message" and value.actionId.
|
|
426
|
+
*/
|
|
427
|
+
handleMessageAction(activity, actionValue, options) {
|
|
428
|
+
if (!this.chat || !actionValue.actionId) return;
|
|
429
|
+
const threadId = this.encodeThreadId({
|
|
430
|
+
conversationId: activity.conversation?.id || "",
|
|
431
|
+
serviceUrl: activity.serviceUrl || ""
|
|
432
|
+
});
|
|
433
|
+
const actionEvent = {
|
|
434
|
+
actionId: actionValue.actionId,
|
|
435
|
+
value: actionValue.value,
|
|
436
|
+
user: {
|
|
437
|
+
userId: activity.from?.id || "unknown",
|
|
438
|
+
userName: activity.from?.name || "unknown",
|
|
439
|
+
fullName: activity.from?.name || "unknown",
|
|
440
|
+
isBot: false,
|
|
441
|
+
isMe: false
|
|
442
|
+
},
|
|
443
|
+
messageId: activity.replyToId || activity.id || "",
|
|
444
|
+
threadId,
|
|
445
|
+
adapter: this,
|
|
446
|
+
raw: activity
|
|
447
|
+
};
|
|
448
|
+
this.logger?.debug("Processing Teams message action (Action.Submit)", {
|
|
449
|
+
actionId: actionValue.actionId,
|
|
450
|
+
value: actionValue.value,
|
|
451
|
+
messageId: actionEvent.messageId,
|
|
452
|
+
threadId
|
|
453
|
+
});
|
|
454
|
+
this.chat.processAction(actionEvent, options);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Handle invoke activities (adaptive card actions, etc.).
|
|
458
|
+
*/
|
|
459
|
+
async handleInvokeActivity(context, options) {
|
|
460
|
+
const activity = context.activity;
|
|
461
|
+
if (activity.name === "adaptiveCard/action") {
|
|
462
|
+
await this.handleAdaptiveCardAction(context, activity, options);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.logger?.debug("Ignoring unsupported invoke", {
|
|
466
|
+
name: activity.name
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Handle adaptive card button clicks.
|
|
471
|
+
* The action data is in activity.value with our { actionId, value } structure.
|
|
472
|
+
*/
|
|
473
|
+
async handleAdaptiveCardAction(context, activity, options) {
|
|
474
|
+
if (!this.chat) return;
|
|
475
|
+
const actionData = activity.value?.action?.data;
|
|
476
|
+
if (!actionData?.actionId) {
|
|
477
|
+
this.logger?.debug("Adaptive card action missing actionId", {
|
|
478
|
+
value: activity.value
|
|
479
|
+
});
|
|
480
|
+
await context.sendActivity({
|
|
481
|
+
type: ActivityTypes.InvokeResponse,
|
|
482
|
+
value: { status: 200 }
|
|
483
|
+
});
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const threadId = this.encodeThreadId({
|
|
487
|
+
conversationId: activity.conversation?.id || "",
|
|
488
|
+
serviceUrl: activity.serviceUrl || ""
|
|
489
|
+
});
|
|
490
|
+
const actionEvent = {
|
|
491
|
+
actionId: actionData.actionId,
|
|
492
|
+
value: actionData.value,
|
|
493
|
+
user: {
|
|
494
|
+
userId: activity.from?.id || "unknown",
|
|
495
|
+
userName: activity.from?.name || "unknown",
|
|
496
|
+
fullName: activity.from?.name || "unknown",
|
|
497
|
+
isBot: false,
|
|
498
|
+
isMe: false
|
|
499
|
+
},
|
|
500
|
+
messageId: activity.replyToId || activity.id || "",
|
|
501
|
+
threadId,
|
|
502
|
+
adapter: this,
|
|
503
|
+
raw: activity
|
|
504
|
+
};
|
|
505
|
+
this.logger?.debug("Processing Teams adaptive card action", {
|
|
506
|
+
actionId: actionData.actionId,
|
|
507
|
+
value: actionData.value,
|
|
508
|
+
messageId: actionEvent.messageId,
|
|
509
|
+
threadId
|
|
510
|
+
});
|
|
511
|
+
this.chat.processAction(actionEvent, options);
|
|
512
|
+
await context.sendActivity({
|
|
513
|
+
type: ActivityTypes.InvokeResponse,
|
|
514
|
+
value: { status: 200 }
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Handle Teams reaction events (reactionsAdded/reactionsRemoved).
|
|
519
|
+
*/
|
|
520
|
+
handleReactionActivity(activity, options) {
|
|
521
|
+
if (!this.chat) return;
|
|
522
|
+
const conversationId = activity.conversation?.id || "";
|
|
523
|
+
const messageIdMatch = conversationId.match(/messageid=(\d+)/);
|
|
524
|
+
const messageId = messageIdMatch?.[1] || activity.replyToId || "";
|
|
525
|
+
const threadId = this.encodeThreadId({
|
|
526
|
+
conversationId,
|
|
527
|
+
serviceUrl: activity.serviceUrl || ""
|
|
528
|
+
});
|
|
529
|
+
const user = {
|
|
530
|
+
userId: activity.from?.id || "unknown",
|
|
531
|
+
userName: activity.from?.name || "unknown",
|
|
532
|
+
fullName: activity.from?.name,
|
|
533
|
+
isBot: false,
|
|
534
|
+
isMe: this.isMessageFromSelf(activity)
|
|
535
|
+
};
|
|
536
|
+
const reactionsAdded = activity.reactionsAdded || [];
|
|
537
|
+
for (const reaction of reactionsAdded) {
|
|
538
|
+
const rawEmoji = reaction.type || "";
|
|
539
|
+
const emojiValue = defaultEmojiResolver.fromTeams(rawEmoji);
|
|
540
|
+
const event = {
|
|
541
|
+
emoji: emojiValue,
|
|
542
|
+
rawEmoji,
|
|
543
|
+
added: true,
|
|
544
|
+
user,
|
|
545
|
+
messageId,
|
|
546
|
+
threadId,
|
|
547
|
+
raw: activity
|
|
548
|
+
};
|
|
549
|
+
this.logger?.debug("Processing Teams reaction added", {
|
|
550
|
+
emoji: emojiValue.name,
|
|
551
|
+
rawEmoji,
|
|
552
|
+
messageId
|
|
553
|
+
});
|
|
554
|
+
this.chat.processReaction({ ...event, adapter: this }, options);
|
|
555
|
+
}
|
|
556
|
+
const reactionsRemoved = activity.reactionsRemoved || [];
|
|
557
|
+
for (const reaction of reactionsRemoved) {
|
|
558
|
+
const rawEmoji = reaction.type || "";
|
|
559
|
+
const emojiValue = defaultEmojiResolver.fromTeams(rawEmoji);
|
|
560
|
+
const event = {
|
|
561
|
+
emoji: emojiValue,
|
|
562
|
+
rawEmoji,
|
|
563
|
+
added: false,
|
|
564
|
+
user,
|
|
565
|
+
messageId,
|
|
566
|
+
threadId,
|
|
567
|
+
raw: activity
|
|
568
|
+
};
|
|
569
|
+
this.logger?.debug("Processing Teams reaction removed", {
|
|
570
|
+
emoji: emojiValue.name,
|
|
571
|
+
rawEmoji,
|
|
572
|
+
messageId
|
|
573
|
+
});
|
|
574
|
+
this.chat.processReaction({ ...event, adapter: this }, options);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
parseTeamsMessage(activity, threadId) {
|
|
578
|
+
const text = activity.text || "";
|
|
579
|
+
const normalizedText = this.normalizeMentions(text, activity);
|
|
580
|
+
const isMe = this.isMessageFromSelf(activity);
|
|
581
|
+
return {
|
|
582
|
+
id: activity.id || "",
|
|
583
|
+
threadId,
|
|
584
|
+
text: this.formatConverter.extractPlainText(normalizedText),
|
|
585
|
+
formatted: this.formatConverter.toAst(normalizedText),
|
|
586
|
+
raw: activity,
|
|
587
|
+
author: {
|
|
588
|
+
userId: activity.from?.id || "unknown",
|
|
589
|
+
userName: activity.from?.name || "unknown",
|
|
590
|
+
fullName: activity.from?.name || "unknown",
|
|
591
|
+
isBot: activity.from?.role === "bot",
|
|
592
|
+
isMe
|
|
593
|
+
},
|
|
594
|
+
metadata: {
|
|
595
|
+
dateSent: activity.timestamp ? new Date(activity.timestamp) : /* @__PURE__ */ new Date(),
|
|
596
|
+
edited: false
|
|
597
|
+
},
|
|
598
|
+
attachments: (activity.attachments || []).filter(
|
|
599
|
+
(att) => att.contentType !== "application/vnd.microsoft.card.adaptive"
|
|
600
|
+
).map((att) => this.createAttachment(att))
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Create an Attachment object from a Teams attachment.
|
|
605
|
+
*/
|
|
606
|
+
createAttachment(att) {
|
|
607
|
+
const url = att.contentUrl;
|
|
608
|
+
let type = "file";
|
|
609
|
+
if (att.contentType?.startsWith("image/")) {
|
|
610
|
+
type = "image";
|
|
611
|
+
} else if (att.contentType?.startsWith("video/")) {
|
|
612
|
+
type = "video";
|
|
613
|
+
} else if (att.contentType?.startsWith("audio/")) {
|
|
614
|
+
type = "audio";
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
type,
|
|
618
|
+
url,
|
|
619
|
+
name: att.name,
|
|
620
|
+
mimeType: att.contentType,
|
|
621
|
+
fetchData: url ? async () => {
|
|
622
|
+
const response = await fetch(url);
|
|
623
|
+
if (!response.ok) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
`Failed to fetch file: ${response.status} ${response.statusText}`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
629
|
+
return Buffer.from(arrayBuffer);
|
|
630
|
+
} : void 0
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
normalizeMentions(text, _activity) {
|
|
634
|
+
return text.trim();
|
|
635
|
+
}
|
|
636
|
+
async postMessage(threadId, message) {
|
|
637
|
+
const { conversationId, serviceUrl } = this.decodeThreadId(threadId);
|
|
638
|
+
const files = this.extractFiles(message);
|
|
639
|
+
const fileAttachments = files.length > 0 ? await this.filesToAttachments(files) : [];
|
|
640
|
+
const card = this.extractCard(message);
|
|
641
|
+
let activity;
|
|
642
|
+
if (card) {
|
|
643
|
+
const adaptiveCard = cardToAdaptiveCard(card);
|
|
644
|
+
activity = {
|
|
645
|
+
type: ActivityTypes.Message,
|
|
646
|
+
// Don't include text - Teams shows both text and card if text is present
|
|
647
|
+
attachments: [
|
|
648
|
+
{
|
|
649
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
650
|
+
content: adaptiveCard
|
|
651
|
+
},
|
|
652
|
+
...fileAttachments
|
|
653
|
+
]
|
|
654
|
+
};
|
|
655
|
+
this.logger?.debug("Teams API: sendActivity (adaptive card)", {
|
|
656
|
+
conversationId,
|
|
657
|
+
serviceUrl,
|
|
658
|
+
fileCount: fileAttachments.length
|
|
659
|
+
});
|
|
660
|
+
} else {
|
|
661
|
+
const text = convertEmojiPlaceholders2(
|
|
662
|
+
this.formatConverter.renderPostable(message),
|
|
663
|
+
"teams"
|
|
664
|
+
);
|
|
665
|
+
activity = {
|
|
666
|
+
type: ActivityTypes.Message,
|
|
667
|
+
text,
|
|
668
|
+
textFormat: "markdown",
|
|
669
|
+
attachments: fileAttachments.length > 0 ? fileAttachments : void 0
|
|
670
|
+
};
|
|
671
|
+
this.logger?.debug("Teams API: sendActivity (message)", {
|
|
672
|
+
conversationId,
|
|
673
|
+
serviceUrl,
|
|
674
|
+
textLength: text.length,
|
|
675
|
+
fileCount: fileAttachments.length
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const conversationReference = {
|
|
679
|
+
channelId: "msteams",
|
|
680
|
+
serviceUrl,
|
|
681
|
+
conversation: { id: conversationId }
|
|
682
|
+
};
|
|
683
|
+
let messageId = "";
|
|
684
|
+
await this.botAdapter.continueConversationAsync(
|
|
685
|
+
this.config.appId,
|
|
686
|
+
conversationReference,
|
|
687
|
+
async (context) => {
|
|
688
|
+
const response = await context.sendActivity(activity);
|
|
689
|
+
messageId = response?.id || "";
|
|
690
|
+
}
|
|
691
|
+
);
|
|
692
|
+
this.logger?.debug("Teams API: sendActivity response", { messageId });
|
|
693
|
+
return {
|
|
694
|
+
id: messageId,
|
|
695
|
+
threadId,
|
|
696
|
+
raw: activity
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Extract card element from a PostableMessage if present.
|
|
701
|
+
*/
|
|
702
|
+
extractCard(message) {
|
|
703
|
+
if (isCardElement(message)) {
|
|
704
|
+
return message;
|
|
705
|
+
}
|
|
706
|
+
if (typeof message === "object" && message !== null && "card" in message) {
|
|
707
|
+
return message.card;
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Extract files from a PostableMessage if present.
|
|
713
|
+
*/
|
|
714
|
+
extractFiles(message) {
|
|
715
|
+
if (typeof message === "object" && message !== null && "files" in message) {
|
|
716
|
+
return message.files ?? [];
|
|
717
|
+
}
|
|
718
|
+
return [];
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Convert files to Teams attachments.
|
|
722
|
+
* Uses inline data URIs for small files.
|
|
723
|
+
*/
|
|
724
|
+
async filesToAttachments(files) {
|
|
725
|
+
const attachments = [];
|
|
726
|
+
for (const file of files) {
|
|
727
|
+
let buffer;
|
|
728
|
+
if (Buffer.isBuffer(file.data)) {
|
|
729
|
+
buffer = file.data;
|
|
730
|
+
} else if (file.data instanceof ArrayBuffer) {
|
|
731
|
+
buffer = Buffer.from(file.data);
|
|
732
|
+
} else if (file.data instanceof Blob) {
|
|
733
|
+
const arrayBuffer = await file.data.arrayBuffer();
|
|
734
|
+
buffer = Buffer.from(arrayBuffer);
|
|
735
|
+
} else {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const mimeType = file.mimeType || "application/octet-stream";
|
|
739
|
+
const base64 = buffer.toString("base64");
|
|
740
|
+
const dataUri = `data:${mimeType};base64,${base64}`;
|
|
741
|
+
attachments.push({
|
|
742
|
+
contentType: mimeType,
|
|
743
|
+
contentUrl: dataUri,
|
|
744
|
+
name: file.filename
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
return attachments;
|
|
748
|
+
}
|
|
749
|
+
async editMessage(threadId, messageId, message) {
|
|
750
|
+
const { conversationId, serviceUrl } = this.decodeThreadId(threadId);
|
|
751
|
+
const card = this.extractCard(message);
|
|
752
|
+
let activity;
|
|
753
|
+
if (card) {
|
|
754
|
+
const adaptiveCard = cardToAdaptiveCard(card);
|
|
755
|
+
activity = {
|
|
756
|
+
id: messageId,
|
|
757
|
+
type: ActivityTypes.Message,
|
|
758
|
+
// Don't include text - Teams shows both text and card if text is present
|
|
759
|
+
attachments: [
|
|
760
|
+
{
|
|
761
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
762
|
+
content: adaptiveCard
|
|
763
|
+
}
|
|
764
|
+
]
|
|
765
|
+
};
|
|
766
|
+
this.logger?.debug("Teams API: updateActivity (adaptive card)", {
|
|
767
|
+
conversationId,
|
|
768
|
+
messageId
|
|
769
|
+
});
|
|
770
|
+
} else {
|
|
771
|
+
const text = convertEmojiPlaceholders2(
|
|
772
|
+
this.formatConverter.renderPostable(message),
|
|
773
|
+
"teams"
|
|
774
|
+
);
|
|
775
|
+
activity = {
|
|
776
|
+
id: messageId,
|
|
777
|
+
type: ActivityTypes.Message,
|
|
778
|
+
text,
|
|
779
|
+
textFormat: "markdown"
|
|
780
|
+
};
|
|
781
|
+
this.logger?.debug("Teams API: updateActivity", {
|
|
782
|
+
conversationId,
|
|
783
|
+
messageId,
|
|
784
|
+
textLength: text.length
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
const conversationReference = {
|
|
788
|
+
channelId: "msteams",
|
|
789
|
+
serviceUrl,
|
|
790
|
+
conversation: { id: conversationId }
|
|
791
|
+
};
|
|
792
|
+
await this.botAdapter.continueConversationAsync(
|
|
793
|
+
this.config.appId,
|
|
794
|
+
conversationReference,
|
|
795
|
+
async (context) => {
|
|
796
|
+
await context.updateActivity(activity);
|
|
797
|
+
}
|
|
798
|
+
);
|
|
799
|
+
this.logger?.debug("Teams API: updateActivity response", { ok: true });
|
|
800
|
+
return {
|
|
801
|
+
id: messageId,
|
|
802
|
+
threadId,
|
|
803
|
+
raw: activity
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
async deleteMessage(threadId, messageId) {
|
|
807
|
+
const { conversationId, serviceUrl } = this.decodeThreadId(threadId);
|
|
808
|
+
const conversationReference = {
|
|
809
|
+
channelId: "msteams",
|
|
810
|
+
serviceUrl,
|
|
811
|
+
conversation: { id: conversationId }
|
|
812
|
+
};
|
|
813
|
+
this.logger?.debug("Teams API: deleteActivity", {
|
|
814
|
+
conversationId,
|
|
815
|
+
messageId
|
|
816
|
+
});
|
|
817
|
+
await this.botAdapter.continueConversationAsync(
|
|
818
|
+
this.config.appId,
|
|
819
|
+
conversationReference,
|
|
820
|
+
async (context) => {
|
|
821
|
+
await context.deleteActivity(messageId);
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
this.logger?.debug("Teams API: deleteActivity response", { ok: true });
|
|
825
|
+
}
|
|
826
|
+
async addReaction(_threadId, _messageId, _emoji) {
|
|
827
|
+
throw new NotImplementedError(
|
|
828
|
+
"Teams Bot Framework does not expose reaction APIs",
|
|
829
|
+
"addReaction"
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
async removeReaction(_threadId, _messageId, _emoji) {
|
|
833
|
+
throw new NotImplementedError(
|
|
834
|
+
"Teams Bot Framework does not expose reaction APIs",
|
|
835
|
+
"removeReaction"
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
async startTyping(threadId) {
|
|
839
|
+
const { conversationId, serviceUrl } = this.decodeThreadId(threadId);
|
|
840
|
+
const conversationReference = {
|
|
841
|
+
channelId: "msteams",
|
|
842
|
+
serviceUrl,
|
|
843
|
+
conversation: { id: conversationId }
|
|
844
|
+
};
|
|
845
|
+
this.logger?.debug("Teams API: sendActivity (typing)", { conversationId });
|
|
846
|
+
await this.botAdapter.continueConversationAsync(
|
|
847
|
+
this.config.appId,
|
|
848
|
+
conversationReference,
|
|
849
|
+
async (context) => {
|
|
850
|
+
await context.sendActivity({ type: ActivityTypes.Typing });
|
|
851
|
+
}
|
|
852
|
+
);
|
|
853
|
+
this.logger?.debug("Teams API: sendActivity (typing) response", {
|
|
854
|
+
ok: true
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Open a direct message conversation with a user.
|
|
859
|
+
* Returns a thread ID that can be used to post messages.
|
|
860
|
+
*
|
|
861
|
+
* The serviceUrl and tenantId are automatically resolved from cached user interactions.
|
|
862
|
+
* If no cached values are found, defaults are used (which may not work for all tenants).
|
|
863
|
+
*/
|
|
864
|
+
async openDM(userId) {
|
|
865
|
+
const cachedServiceUrl = await this.chat?.getState().get(`teams:serviceUrl:${userId}`);
|
|
866
|
+
const cachedTenantId = await this.chat?.getState().get(`teams:tenantId:${userId}`);
|
|
867
|
+
const serviceUrl = cachedServiceUrl || "https://smba.trafficmanager.net/teams/";
|
|
868
|
+
const tenantId = cachedTenantId || this.config.appTenantId;
|
|
869
|
+
this.logger?.debug("Teams: creating 1:1 conversation", {
|
|
870
|
+
userId,
|
|
871
|
+
serviceUrl,
|
|
872
|
+
tenantId,
|
|
873
|
+
cachedServiceUrl: !!cachedServiceUrl,
|
|
874
|
+
cachedTenantId: !!cachedTenantId
|
|
875
|
+
});
|
|
876
|
+
if (!tenantId) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
"Cannot open DM: tenant ID not found. User must interact with the bot first (via @mention) to cache their tenant ID."
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
let conversationId = "";
|
|
882
|
+
await this.botAdapter.createConversationAsync(
|
|
883
|
+
this.config.appId,
|
|
884
|
+
"msteams",
|
|
885
|
+
serviceUrl,
|
|
886
|
+
"",
|
|
887
|
+
// empty audience
|
|
888
|
+
{
|
|
889
|
+
isGroup: false,
|
|
890
|
+
bot: { id: this.config.appId, name: this.userName },
|
|
891
|
+
members: [{ id: userId }],
|
|
892
|
+
tenantId,
|
|
893
|
+
channelData: {
|
|
894
|
+
tenant: { id: tenantId }
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
async (turnContext) => {
|
|
898
|
+
conversationId = turnContext?.activity?.conversation?.id || "";
|
|
899
|
+
this.logger?.debug("Teams: conversation created in callback", {
|
|
900
|
+
conversationId,
|
|
901
|
+
activityId: turnContext?.activity?.id
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
);
|
|
905
|
+
if (!conversationId) {
|
|
906
|
+
throw new Error("Failed to create 1:1 conversation - no ID returned");
|
|
907
|
+
}
|
|
908
|
+
this.logger?.debug("Teams: 1:1 conversation created", { conversationId });
|
|
909
|
+
return this.encodeThreadId({
|
|
910
|
+
conversationId,
|
|
911
|
+
serviceUrl
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
async fetchMessages(_threadId, _options = {}) {
|
|
915
|
+
throw new NotImplementedError(
|
|
916
|
+
"Teams does not provide a bot API to fetch message history. Use Microsoft Graph API instead.",
|
|
917
|
+
"fetchMessages"
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
async fetchThread(threadId) {
|
|
921
|
+
const { conversationId } = this.decodeThreadId(threadId);
|
|
922
|
+
return {
|
|
923
|
+
id: threadId,
|
|
924
|
+
channelId: conversationId,
|
|
925
|
+
metadata: {}
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
encodeThreadId(platformData) {
|
|
929
|
+
const encodedConversationId = Buffer.from(
|
|
930
|
+
platformData.conversationId
|
|
931
|
+
).toString("base64url");
|
|
932
|
+
const encodedServiceUrl = Buffer.from(platformData.serviceUrl).toString(
|
|
933
|
+
"base64url"
|
|
934
|
+
);
|
|
935
|
+
return `teams:${encodedConversationId}:${encodedServiceUrl}`;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Check if a thread is a direct message conversation.
|
|
939
|
+
* Teams DMs have conversation IDs that don't start with "19:" (which is for groups/channels).
|
|
940
|
+
*/
|
|
941
|
+
isDM(threadId) {
|
|
942
|
+
const { conversationId } = this.decodeThreadId(threadId);
|
|
943
|
+
return !conversationId.startsWith("19:");
|
|
944
|
+
}
|
|
945
|
+
decodeThreadId(threadId) {
|
|
946
|
+
const parts = threadId.split(":");
|
|
947
|
+
if (parts.length !== 3 || parts[0] !== "teams") {
|
|
948
|
+
throw new Error(`Invalid Teams thread ID: ${threadId}`);
|
|
949
|
+
}
|
|
950
|
+
const conversationId = Buffer.from(
|
|
951
|
+
parts[1],
|
|
952
|
+
"base64url"
|
|
953
|
+
).toString("utf-8");
|
|
954
|
+
const serviceUrl = Buffer.from(parts[2], "base64url").toString(
|
|
955
|
+
"utf-8"
|
|
956
|
+
);
|
|
957
|
+
return { conversationId, serviceUrl };
|
|
958
|
+
}
|
|
959
|
+
parseMessage(raw) {
|
|
960
|
+
const activity = raw;
|
|
961
|
+
const threadId = this.encodeThreadId({
|
|
962
|
+
conversationId: activity.conversation?.id || "",
|
|
963
|
+
serviceUrl: activity.serviceUrl || ""
|
|
964
|
+
});
|
|
965
|
+
return this.parseTeamsMessage(activity, threadId);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Check if a Teams activity is from this bot.
|
|
969
|
+
*
|
|
970
|
+
* Teams bot IDs can appear in different formats:
|
|
971
|
+
* - Just the app ID: "abc123-def456-..."
|
|
972
|
+
* - With prefix: "28:abc123-def456-..."
|
|
973
|
+
*
|
|
974
|
+
* We check both exact match and suffix match (after colon delimiter)
|
|
975
|
+
* to handle all formats safely.
|
|
976
|
+
*/
|
|
977
|
+
isMessageFromSelf(activity) {
|
|
978
|
+
const fromId = activity.from?.id;
|
|
979
|
+
if (!fromId || !this.config.appId) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
if (fromId === this.config.appId) {
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
if (fromId.endsWith(`:${this.config.appId}`)) {
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
renderFormatted(content) {
|
|
991
|
+
return this.formatConverter.fromAst(content);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
function createTeamsAdapter(config) {
|
|
995
|
+
return new TeamsAdapter(config);
|
|
996
|
+
}
|
|
997
|
+
export {
|
|
998
|
+
TeamsAdapter,
|
|
999
|
+
TeamsFormatConverter,
|
|
1000
|
+
cardToAdaptiveCard,
|
|
1001
|
+
cardToFallbackText,
|
|
1002
|
+
createTeamsAdapter
|
|
1003
|
+
};
|
|
1004
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/cards.ts","../src/markdown.ts"],"sourcesContent":["import type { Activity, ConversationReference } from \"botbuilder\";\nimport {\n ActivityTypes,\n CloudAdapter,\n ConfigurationBotFrameworkAuthentication,\n type TurnContext,\n} from \"botbuilder\";\n\n/** Extended CloudAdapter that exposes processActivity for serverless environments */\nclass ServerlessCloudAdapter extends CloudAdapter {\n handleActivity(\n authHeader: string,\n activity: Activity,\n logic: (context: TurnContext) => Promise<void>,\n ) {\n return this.processActivity(authHeader, activity, logic);\n }\n}\n\nimport type {\n ActionEvent,\n Adapter,\n Attachment,\n ChatInstance,\n EmojiValue,\n FetchOptions,\n FileUpload,\n FormattedContent,\n Logger,\n Message,\n PostableMessage,\n RawMessage,\n ReactionEvent,\n ThreadInfo,\n WebhookOptions,\n} from \"chat\";\nimport {\n convertEmojiPlaceholders,\n defaultEmojiResolver,\n isCardElement,\n NotImplementedError,\n} from \"chat\";\nimport { cardToAdaptiveCard } from \"./cards\";\nimport { TeamsFormatConverter } from \"./markdown\";\n\nexport interface TeamsAdapterConfig {\n /** Microsoft App ID */\n appId: string;\n /** Microsoft App Password */\n appPassword: string;\n /** Microsoft App Type */\n appType?: \"MultiTenant\" | \"SingleTenant\";\n /** Microsoft App Tenant ID */\n appTenantId?: string;\n /** Override bot username (optional) */\n userName?: string;\n}\n\n/** Teams-specific thread ID data */\nexport interface TeamsThreadId {\n conversationId: string;\n serviceUrl: string;\n replyToId?: string;\n}\n\nexport class TeamsAdapter implements Adapter<TeamsThreadId, unknown> {\n readonly name = \"teams\";\n readonly userName: string;\n readonly botUserId?: string;\n\n private botAdapter: ServerlessCloudAdapter;\n private chat: ChatInstance | null = null;\n private logger: Logger | null = null;\n private formatConverter = new TeamsFormatConverter();\n private config: TeamsAdapterConfig;\n\n constructor(config: TeamsAdapterConfig) {\n this.config = config;\n this.userName = config.userName || \"bot\";\n\n if (config.appType === \"SingleTenant\" && !config.appTenantId) {\n throw new Error(\"appTenantId is required for SingleTenant app type\");\n }\n\n // Pass empty config object, credentials go via factory\n const auth = new ConfigurationBotFrameworkAuthentication({\n MicrosoftAppId: config.appId,\n MicrosoftAppPassword: config.appPassword,\n MicrosoftAppType: config.appType || \"MultiTenant\",\n MicrosoftAppTenantId:\n config.appType === \"SingleTenant\" ? config.appTenantId : undefined,\n });\n\n this.botAdapter = new ServerlessCloudAdapter(auth);\n }\n\n async initialize(chat: ChatInstance): Promise<void> {\n this.chat = chat;\n this.logger = chat.getLogger(this.name);\n }\n\n async handleWebhook(\n request: Request,\n options?: WebhookOptions,\n ): Promise<Response> {\n const body = await request.text();\n this.logger?.debug(\"Teams webhook raw body\", { body });\n\n let activity: Activity;\n try {\n activity = JSON.parse(body);\n } catch (e) {\n this.logger?.error(\"Failed to parse request body\", { error: e });\n return new Response(\"Invalid JSON\", { status: 400 });\n }\n\n // Get the auth header for token validation\n const authHeader = request.headers.get(\"authorization\") || \"\";\n\n try {\n // Use handleActivity which takes the activity directly\n // instead of mocking Node.js req/res objects\n await this.botAdapter.handleActivity(\n authHeader,\n activity,\n async (context) => {\n await this.handleTurn(context, options);\n },\n );\n\n return new Response(JSON.stringify({}), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (error) {\n this.logger?.error(\"Bot adapter process error\", { error });\n return new Response(JSON.stringify({ error: \"Internal error\" }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n }\n\n private async handleTurn(\n context: TurnContext,\n options?: WebhookOptions,\n ): Promise<void> {\n if (!this.chat) {\n this.logger?.warn(\"Chat instance not initialized, ignoring event\");\n return;\n }\n\n const activity = context.activity;\n\n // Cache serviceUrl and tenantId for the user - needed for opening DMs later\n if (activity.from?.id && activity.serviceUrl) {\n const userId = activity.from.id;\n const tenantId = (activity.channelData as { tenant?: { id?: string } })\n ?.tenant?.id;\n const ttl = 30 * 24 * 60 * 60 * 1000; // 30 days\n\n // Store serviceUrl and tenantId for DM creation\n this.chat\n .getState()\n .set(`teams:serviceUrl:${userId}`, activity.serviceUrl, ttl);\n if (tenantId) {\n this.chat.getState().set(`teams:tenantId:${userId}`, tenantId, ttl);\n }\n }\n\n // Handle message reactions\n if (activity.type === ActivityTypes.MessageReaction) {\n this.handleReactionActivity(activity, options);\n return;\n }\n\n // Handle adaptive card actions (button clicks)\n if (activity.type === ActivityTypes.Invoke) {\n await this.handleInvokeActivity(context, options);\n return;\n }\n\n // Only handle message activities\n if (activity.type !== ActivityTypes.Message) {\n this.logger?.debug(\"Ignoring non-message activity\", {\n type: activity.type,\n });\n return;\n }\n\n // Check if this message activity is actually a button click (Action.Submit)\n // Teams sends Action.Submit as a message with value.actionId\n const actionValue = activity.value as\n | { actionId?: string; value?: string }\n | undefined;\n if (actionValue?.actionId) {\n this.handleMessageAction(activity, actionValue, options);\n return;\n }\n\n const threadId = this.encodeThreadId({\n conversationId: activity.conversation?.id || \"\",\n serviceUrl: activity.serviceUrl || \"\",\n replyToId: activity.replyToId,\n });\n\n // Let Chat class handle async processing and waitUntil\n this.chat.processMessage(\n this,\n threadId,\n this.parseTeamsMessage(activity, threadId),\n options,\n );\n }\n\n /**\n * Handle Action.Submit button clicks sent as message activities.\n * Teams sends these with type \"message\" and value.actionId.\n */\n private handleMessageAction(\n activity: Activity,\n actionValue: { actionId?: string; value?: string },\n options?: WebhookOptions,\n ): void {\n if (!this.chat || !actionValue.actionId) return;\n\n const threadId = this.encodeThreadId({\n conversationId: activity.conversation?.id || \"\",\n serviceUrl: activity.serviceUrl || \"\",\n });\n\n const actionEvent: Omit<ActionEvent, \"thread\"> & { adapter: TeamsAdapter } =\n {\n actionId: actionValue.actionId,\n value: actionValue.value,\n user: {\n userId: activity.from?.id || \"unknown\",\n userName: activity.from?.name || \"unknown\",\n fullName: activity.from?.name || \"unknown\",\n isBot: false,\n isMe: false,\n },\n messageId: activity.replyToId || activity.id || \"\",\n threadId,\n adapter: this,\n raw: activity,\n };\n\n this.logger?.debug(\"Processing Teams message action (Action.Submit)\", {\n actionId: actionValue.actionId,\n value: actionValue.value,\n messageId: actionEvent.messageId,\n threadId,\n });\n\n this.chat.processAction(actionEvent, options);\n }\n\n /**\n * Handle invoke activities (adaptive card actions, etc.).\n */\n private async handleInvokeActivity(\n context: TurnContext,\n options?: WebhookOptions,\n ): Promise<void> {\n const activity = context.activity;\n\n // Handle adaptive card action invokes\n if (activity.name === \"adaptiveCard/action\") {\n await this.handleAdaptiveCardAction(context, activity, options);\n return;\n }\n\n this.logger?.debug(\"Ignoring unsupported invoke\", {\n name: activity.name,\n });\n }\n\n /**\n * Handle adaptive card button clicks.\n * The action data is in activity.value with our { actionId, value } structure.\n */\n private async handleAdaptiveCardAction(\n context: TurnContext,\n activity: Activity,\n options?: WebhookOptions,\n ): Promise<void> {\n if (!this.chat) return;\n\n // Activity.value contains our action data\n const actionData = activity.value?.action?.data as\n | { actionId?: string; value?: string }\n | undefined;\n\n if (!actionData?.actionId) {\n this.logger?.debug(\"Adaptive card action missing actionId\", {\n value: activity.value,\n });\n // Send acknowledgment response\n await context.sendActivity({\n type: ActivityTypes.InvokeResponse,\n value: { status: 200 },\n });\n return;\n }\n\n const threadId = this.encodeThreadId({\n conversationId: activity.conversation?.id || \"\",\n serviceUrl: activity.serviceUrl || \"\",\n });\n\n const actionEvent: Omit<ActionEvent, \"thread\"> & { adapter: TeamsAdapter } =\n {\n actionId: actionData.actionId,\n value: actionData.value,\n user: {\n userId: activity.from?.id || \"unknown\",\n userName: activity.from?.name || \"unknown\",\n fullName: activity.from?.name || \"unknown\",\n isBot: false,\n isMe: false,\n },\n messageId: activity.replyToId || activity.id || \"\",\n threadId,\n adapter: this,\n raw: activity,\n };\n\n this.logger?.debug(\"Processing Teams adaptive card action\", {\n actionId: actionData.actionId,\n value: actionData.value,\n messageId: actionEvent.messageId,\n threadId,\n });\n\n this.chat.processAction(actionEvent, options);\n\n // Send acknowledgment response to prevent timeout\n await context.sendActivity({\n type: ActivityTypes.InvokeResponse,\n value: { status: 200 },\n });\n }\n\n /**\n * Handle Teams reaction events (reactionsAdded/reactionsRemoved).\n */\n private handleReactionActivity(\n activity: Activity,\n options?: WebhookOptions,\n ): void {\n if (!this.chat) return;\n\n // Extract the message ID from conversation ID\n // Format: \"19:xxx@thread.tacv2;messageid=1767297849909\"\n const conversationId = activity.conversation?.id || \"\";\n const messageIdMatch = conversationId.match(/messageid=(\\d+)/);\n const messageId = messageIdMatch?.[1] || activity.replyToId || \"\";\n\n // Build thread ID - KEEP the full conversation ID including ;messageid=XXX\n // This is required for Teams to reply in the correct thread\n const threadId = this.encodeThreadId({\n conversationId: conversationId,\n serviceUrl: activity.serviceUrl || \"\",\n });\n\n const user = {\n userId: activity.from?.id || \"unknown\",\n userName: activity.from?.name || \"unknown\",\n fullName: activity.from?.name,\n isBot: false,\n isMe: this.isMessageFromSelf(activity),\n };\n\n // Process added reactions\n const reactionsAdded = activity.reactionsAdded || [];\n for (const reaction of reactionsAdded) {\n const rawEmoji = reaction.type || \"\";\n const emojiValue = defaultEmojiResolver.fromTeams(rawEmoji);\n\n const event: Omit<ReactionEvent, \"adapter\" | \"thread\"> = {\n emoji: emojiValue,\n rawEmoji,\n added: true,\n user,\n messageId,\n threadId,\n raw: activity,\n };\n\n this.logger?.debug(\"Processing Teams reaction added\", {\n emoji: emojiValue.name,\n rawEmoji,\n messageId,\n });\n\n this.chat.processReaction({ ...event, adapter: this }, options);\n }\n\n // Process removed reactions\n const reactionsRemoved = activity.reactionsRemoved || [];\n for (const reaction of reactionsRemoved) {\n const rawEmoji = reaction.type || \"\";\n const emojiValue = defaultEmojiResolver.fromTeams(rawEmoji);\n\n const event: Omit<ReactionEvent, \"adapter\" | \"thread\"> = {\n emoji: emojiValue,\n rawEmoji,\n added: false,\n user,\n messageId,\n threadId,\n raw: activity,\n };\n\n this.logger?.debug(\"Processing Teams reaction removed\", {\n emoji: emojiValue.name,\n rawEmoji,\n messageId,\n });\n\n this.chat.processReaction({ ...event, adapter: this }, options);\n }\n }\n\n private parseTeamsMessage(\n activity: Activity,\n threadId: string,\n ): Message<unknown> {\n const text = activity.text || \"\";\n // Normalize mentions - format converter will convert <at>name</at> to @name\n const normalizedText = this.normalizeMentions(text, activity);\n\n const isMe = this.isMessageFromSelf(activity);\n\n return {\n id: activity.id || \"\",\n threadId,\n text: this.formatConverter.extractPlainText(normalizedText),\n formatted: this.formatConverter.toAst(normalizedText),\n raw: activity,\n author: {\n userId: activity.from?.id || \"unknown\",\n userName: activity.from?.name || \"unknown\",\n fullName: activity.from?.name || \"unknown\",\n isBot: activity.from?.role === \"bot\",\n isMe,\n },\n metadata: {\n dateSent: activity.timestamp\n ? new Date(activity.timestamp)\n : new Date(),\n edited: false,\n },\n attachments: (activity.attachments || [])\n .filter(\n (att) =>\n att.contentType !== \"application/vnd.microsoft.card.adaptive\",\n )\n .map((att) => this.createAttachment(att)),\n };\n }\n\n /**\n * Create an Attachment object from a Teams attachment.\n */\n private createAttachment(att: {\n contentType?: string;\n contentUrl?: string;\n name?: string;\n }): Attachment {\n const url = att.contentUrl;\n\n // Determine type based on contentType\n let type: Attachment[\"type\"] = \"file\";\n if (att.contentType?.startsWith(\"image/\")) {\n type = \"image\";\n } else if (att.contentType?.startsWith(\"video/\")) {\n type = \"video\";\n } else if (att.contentType?.startsWith(\"audio/\")) {\n type = \"audio\";\n }\n\n return {\n type,\n url,\n name: att.name,\n mimeType: att.contentType,\n fetchData: url\n ? async () => {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch file: ${response.status} ${response.statusText}`,\n );\n }\n const arrayBuffer = await response.arrayBuffer();\n return Buffer.from(arrayBuffer);\n }\n : undefined,\n };\n }\n\n private normalizeMentions(text: string, _activity: Activity): string {\n // Don't strip mentions - the format converter will convert <at>name</at> to @name\n // Just trim any leading/trailing whitespace that might result from mention placement\n return text.trim();\n }\n\n async postMessage(\n threadId: string,\n message: PostableMessage,\n ): Promise<RawMessage<unknown>> {\n const { conversationId, serviceUrl } = this.decodeThreadId(threadId);\n\n // Check for files to upload\n const files = this.extractFiles(message);\n const fileAttachments =\n files.length > 0 ? await this.filesToAttachments(files) : [];\n\n // Check if message contains a card\n const card = this.extractCard(message);\n let activity: Partial<Activity>;\n\n if (card) {\n // Render card as Adaptive Card\n const adaptiveCard = cardToAdaptiveCard(card);\n\n activity = {\n type: ActivityTypes.Message,\n // Don't include text - Teams shows both text and card if text is present\n attachments: [\n {\n contentType: \"application/vnd.microsoft.card.adaptive\",\n content: adaptiveCard,\n },\n ...fileAttachments,\n ],\n };\n\n this.logger?.debug(\"Teams API: sendActivity (adaptive card)\", {\n conversationId,\n serviceUrl,\n fileCount: fileAttachments.length,\n });\n } else {\n // Regular text message\n const text = convertEmojiPlaceholders(\n this.formatConverter.renderPostable(message),\n \"teams\",\n );\n\n activity = {\n type: ActivityTypes.Message,\n text,\n textFormat: \"markdown\",\n attachments: fileAttachments.length > 0 ? fileAttachments : undefined,\n };\n\n this.logger?.debug(\"Teams API: sendActivity (message)\", {\n conversationId,\n serviceUrl,\n textLength: text.length,\n fileCount: fileAttachments.length,\n });\n }\n\n // Use the adapter to send the message\n const conversationReference = {\n channelId: \"msteams\",\n serviceUrl,\n conversation: { id: conversationId },\n };\n\n let messageId = \"\";\n\n await this.botAdapter.continueConversationAsync(\n this.config.appId,\n conversationReference as Partial<ConversationReference>,\n async (context) => {\n const response = await context.sendActivity(activity);\n messageId = response?.id || \"\";\n },\n );\n\n this.logger?.debug(\"Teams API: sendActivity response\", { messageId });\n\n return {\n id: messageId,\n threadId,\n raw: activity,\n };\n }\n\n /**\n * Extract card element from a PostableMessage if present.\n */\n private extractCard(\n message: PostableMessage,\n ): import(\"chat\").CardElement | null {\n if (isCardElement(message)) {\n return message;\n }\n if (typeof message === \"object\" && message !== null && \"card\" in message) {\n return message.card;\n }\n return null;\n }\n\n /**\n * Extract files from a PostableMessage if present.\n */\n private extractFiles(message: PostableMessage): FileUpload[] {\n if (typeof message === \"object\" && message !== null && \"files\" in message) {\n return (message as { files?: FileUpload[] }).files ?? [];\n }\n return [];\n }\n\n /**\n * Convert files to Teams attachments.\n * Uses inline data URIs for small files.\n */\n private async filesToAttachments(\n files: FileUpload[],\n ): Promise<Array<{ contentType: string; contentUrl: string; name: string }>> {\n const attachments: Array<{\n contentType: string;\n contentUrl: string;\n name: string;\n }> = [];\n\n for (const file of files) {\n // Convert data to Buffer\n let buffer: Buffer;\n if (Buffer.isBuffer(file.data)) {\n buffer = file.data;\n } else if (file.data instanceof ArrayBuffer) {\n buffer = Buffer.from(file.data);\n } else if (file.data instanceof Blob) {\n const arrayBuffer = await file.data.arrayBuffer();\n buffer = Buffer.from(arrayBuffer);\n } else {\n continue;\n }\n\n // Create data URI\n const mimeType = file.mimeType || \"application/octet-stream\";\n const base64 = buffer.toString(\"base64\");\n const dataUri = `data:${mimeType};base64,${base64}`;\n\n attachments.push({\n contentType: mimeType,\n contentUrl: dataUri,\n name: file.filename,\n });\n }\n\n return attachments;\n }\n\n async editMessage(\n threadId: string,\n messageId: string,\n message: PostableMessage,\n ): Promise<RawMessage<unknown>> {\n const { conversationId, serviceUrl } = this.decodeThreadId(threadId);\n\n // Check if message contains a card\n const card = this.extractCard(message);\n let activity: Partial<Activity>;\n\n if (card) {\n // Render card as Adaptive Card\n const adaptiveCard = cardToAdaptiveCard(card);\n\n activity = {\n id: messageId,\n type: ActivityTypes.Message,\n // Don't include text - Teams shows both text and card if text is present\n attachments: [\n {\n contentType: \"application/vnd.microsoft.card.adaptive\",\n content: adaptiveCard,\n },\n ],\n };\n\n this.logger?.debug(\"Teams API: updateActivity (adaptive card)\", {\n conversationId,\n messageId,\n });\n } else {\n // Regular text message\n const text = convertEmojiPlaceholders(\n this.formatConverter.renderPostable(message),\n \"teams\",\n );\n\n activity = {\n id: messageId,\n type: ActivityTypes.Message,\n text,\n textFormat: \"markdown\",\n };\n\n this.logger?.debug(\"Teams API: updateActivity\", {\n conversationId,\n messageId,\n textLength: text.length,\n });\n }\n\n const conversationReference = {\n channelId: \"msteams\",\n serviceUrl,\n conversation: { id: conversationId },\n };\n\n await this.botAdapter.continueConversationAsync(\n this.config.appId,\n conversationReference as Partial<ConversationReference>,\n async (context) => {\n await context.updateActivity(activity);\n },\n );\n\n this.logger?.debug(\"Teams API: updateActivity response\", { ok: true });\n\n return {\n id: messageId,\n threadId,\n raw: activity,\n };\n }\n\n async deleteMessage(threadId: string, messageId: string): Promise<void> {\n const { conversationId, serviceUrl } = this.decodeThreadId(threadId);\n\n const conversationReference = {\n channelId: \"msteams\",\n serviceUrl,\n conversation: { id: conversationId },\n };\n\n this.logger?.debug(\"Teams API: deleteActivity\", {\n conversationId,\n messageId,\n });\n\n await this.botAdapter.continueConversationAsync(\n this.config.appId,\n conversationReference as Partial<ConversationReference>,\n async (context) => {\n await context.deleteActivity(messageId);\n },\n );\n\n this.logger?.debug(\"Teams API: deleteActivity response\", { ok: true });\n }\n\n async addReaction(\n _threadId: string,\n _messageId: string,\n _emoji: EmojiValue | string,\n ): Promise<void> {\n throw new NotImplementedError(\n \"Teams Bot Framework does not expose reaction APIs\",\n \"addReaction\",\n );\n }\n\n async removeReaction(\n _threadId: string,\n _messageId: string,\n _emoji: EmojiValue | string,\n ): Promise<void> {\n throw new NotImplementedError(\n \"Teams Bot Framework does not expose reaction APIs\",\n \"removeReaction\",\n );\n }\n\n async startTyping(threadId: string): Promise<void> {\n const { conversationId, serviceUrl } = this.decodeThreadId(threadId);\n\n const conversationReference = {\n channelId: \"msteams\",\n serviceUrl,\n conversation: { id: conversationId },\n };\n\n this.logger?.debug(\"Teams API: sendActivity (typing)\", { conversationId });\n\n await this.botAdapter.continueConversationAsync(\n this.config.appId,\n conversationReference as Partial<ConversationReference>,\n async (context) => {\n await context.sendActivity({ type: ActivityTypes.Typing });\n },\n );\n\n this.logger?.debug(\"Teams API: sendActivity (typing) response\", {\n ok: true,\n });\n }\n\n /**\n * Open a direct message conversation with a user.\n * Returns a thread ID that can be used to post messages.\n *\n * The serviceUrl and tenantId are automatically resolved from cached user interactions.\n * If no cached values are found, defaults are used (which may not work for all tenants).\n */\n async openDM(userId: string): Promise<string> {\n // Look up cached serviceUrl and tenantId for this user from state\n const cachedServiceUrl = await this.chat\n ?.getState()\n .get<string>(`teams:serviceUrl:${userId}`);\n const cachedTenantId = await this.chat\n ?.getState()\n .get<string>(`teams:tenantId:${userId}`);\n\n const serviceUrl =\n cachedServiceUrl || \"https://smba.trafficmanager.net/teams/\";\n // Use cached tenant ID, config tenant ID, or undefined (will fail for multi-tenant)\n const tenantId = cachedTenantId || this.config.appTenantId;\n\n this.logger?.debug(\"Teams: creating 1:1 conversation\", {\n userId,\n serviceUrl,\n tenantId,\n cachedServiceUrl: !!cachedServiceUrl,\n cachedTenantId: !!cachedTenantId,\n });\n\n if (!tenantId) {\n throw new Error(\n \"Cannot open DM: tenant ID not found. User must interact with the bot first (via @mention) to cache their tenant ID.\",\n );\n }\n\n let conversationId = \"\";\n\n // Create the 1:1 conversation using createConversationAsync\n // The conversation ID is captured from within the callback, not from the return value\n // biome-ignore lint/suspicious/noExplicitAny: BotBuilder types are incomplete\n await (this.botAdapter as any).createConversationAsync(\n this.config.appId,\n \"msteams\",\n serviceUrl,\n \"\", // empty audience\n {\n isGroup: false,\n bot: { id: this.config.appId, name: this.userName },\n members: [{ id: userId }],\n tenantId,\n channelData: {\n tenant: { id: tenantId },\n },\n },\n async (turnContext: TurnContext) => {\n // Capture the conversation ID from the new context\n conversationId = turnContext?.activity?.conversation?.id || \"\";\n this.logger?.debug(\"Teams: conversation created in callback\", {\n conversationId,\n activityId: turnContext?.activity?.id,\n });\n },\n );\n\n if (!conversationId) {\n throw new Error(\"Failed to create 1:1 conversation - no ID returned\");\n }\n\n this.logger?.debug(\"Teams: 1:1 conversation created\", { conversationId });\n\n return this.encodeThreadId({\n conversationId,\n serviceUrl,\n });\n }\n\n async fetchMessages(\n _threadId: string,\n _options: FetchOptions = {},\n ): Promise<Message<unknown>[]> {\n throw new NotImplementedError(\n \"Teams does not provide a bot API to fetch message history. Use Microsoft Graph API instead.\",\n \"fetchMessages\",\n );\n }\n\n async fetchThread(threadId: string): Promise<ThreadInfo> {\n const { conversationId } = this.decodeThreadId(threadId);\n\n return {\n id: threadId,\n channelId: conversationId,\n metadata: {},\n };\n }\n\n encodeThreadId(platformData: TeamsThreadId): string {\n // Base64 encode both since conversationId and serviceUrl can contain special characters\n const encodedConversationId = Buffer.from(\n platformData.conversationId,\n ).toString(\"base64url\");\n const encodedServiceUrl = Buffer.from(platformData.serviceUrl).toString(\n \"base64url\",\n );\n return `teams:${encodedConversationId}:${encodedServiceUrl}`;\n }\n\n /**\n * Check if a thread is a direct message conversation.\n * Teams DMs have conversation IDs that don't start with \"19:\" (which is for groups/channels).\n */\n isDM(threadId: string): boolean {\n const { conversationId } = this.decodeThreadId(threadId);\n // Group chats and channels start with \"19:\", DMs don't\n return !conversationId.startsWith(\"19:\");\n }\n\n decodeThreadId(threadId: string): TeamsThreadId {\n const parts = threadId.split(\":\");\n if (parts.length !== 3 || parts[0] !== \"teams\") {\n throw new Error(`Invalid Teams thread ID: ${threadId}`);\n }\n const conversationId = Buffer.from(\n parts[1] as string,\n \"base64url\",\n ).toString(\"utf-8\");\n const serviceUrl = Buffer.from(parts[2] as string, \"base64url\").toString(\n \"utf-8\",\n );\n return { conversationId, serviceUrl };\n }\n\n parseMessage(raw: unknown): Message<unknown> {\n const activity = raw as Activity;\n const threadId = this.encodeThreadId({\n conversationId: activity.conversation?.id || \"\",\n serviceUrl: activity.serviceUrl || \"\",\n });\n return this.parseTeamsMessage(activity, threadId);\n }\n\n /**\n * Check if a Teams activity is from this bot.\n *\n * Teams bot IDs can appear in different formats:\n * - Just the app ID: \"abc123-def456-...\"\n * - With prefix: \"28:abc123-def456-...\"\n *\n * We check both exact match and suffix match (after colon delimiter)\n * to handle all formats safely.\n */\n private isMessageFromSelf(activity: Activity): boolean {\n const fromId = activity.from?.id;\n if (!fromId || !this.config.appId) {\n return false;\n }\n\n // Exact match (bot ID is just the app ID)\n if (fromId === this.config.appId) {\n return true;\n }\n\n // Teams format: \"28:{appId}\" or similar prefix patterns\n // Check if it ends with our appId after a colon delimiter\n if (fromId.endsWith(`:${this.config.appId}`)) {\n return true;\n }\n\n return false;\n }\n\n renderFormatted(content: FormattedContent): string {\n return this.formatConverter.fromAst(content);\n }\n}\n\nexport function createTeamsAdapter(config: TeamsAdapterConfig): TeamsAdapter {\n return new TeamsAdapter(config);\n}\n\n// Re-export card converter for advanced use\nexport { cardToAdaptiveCard, cardToFallbackText } from \"./cards\";\nexport { TeamsFormatConverter } from \"./markdown\";\n","/**\n * Teams Adaptive Card converter for cross-platform cards.\n *\n * Converts CardElement to Microsoft Adaptive Cards format.\n * @see https://adaptivecards.io/\n */\n\nimport {\n type ActionsElement,\n type ButtonElement,\n type CardChild,\n type CardElement,\n convertEmojiPlaceholders,\n type DividerElement,\n type FieldsElement,\n type ImageElement,\n type SectionElement,\n type TextElement,\n} from \"chat\";\n\n/**\n * Convert emoji placeholders in text to Teams format.\n */\nfunction convertEmoji(text: string): string {\n return convertEmojiPlaceholders(text, \"teams\");\n}\n\n// Adaptive Card types (simplified)\nexport interface AdaptiveCard {\n type: \"AdaptiveCard\";\n $schema: string;\n version: string;\n body: AdaptiveCardElement[];\n actions?: AdaptiveCardAction[];\n}\n\nexport interface AdaptiveCardElement {\n type: string;\n [key: string]: unknown;\n}\n\nexport interface AdaptiveCardAction {\n type: string;\n title: string;\n data?: Record<string, unknown>;\n style?: string;\n}\n\nconst ADAPTIVE_CARD_SCHEMA =\n \"http://adaptivecards.io/schemas/adaptive-card.json\";\nconst ADAPTIVE_CARD_VERSION = \"1.4\";\n\n/**\n * Convert a CardElement to a Teams Adaptive Card.\n */\nexport function cardToAdaptiveCard(card: CardElement): AdaptiveCard {\n const body: AdaptiveCardElement[] = [];\n const actions: AdaptiveCardAction[] = [];\n\n // Add title as TextBlock\n if (card.title) {\n body.push({\n type: \"TextBlock\",\n text: convertEmoji(card.title),\n weight: \"bolder\",\n size: \"large\",\n wrap: true,\n });\n }\n\n // Add subtitle as TextBlock\n if (card.subtitle) {\n body.push({\n type: \"TextBlock\",\n text: convertEmoji(card.subtitle),\n isSubtle: true,\n wrap: true,\n });\n }\n\n // Add header image if present\n if (card.imageUrl) {\n body.push({\n type: \"Image\",\n url: card.imageUrl,\n size: \"stretch\",\n });\n }\n\n // Convert children\n for (const child of card.children) {\n const result = convertChildToAdaptive(child);\n body.push(...result.elements);\n actions.push(...result.actions);\n }\n\n const adaptiveCard: AdaptiveCard = {\n type: \"AdaptiveCard\",\n $schema: ADAPTIVE_CARD_SCHEMA,\n version: ADAPTIVE_CARD_VERSION,\n body,\n };\n\n if (actions.length > 0) {\n adaptiveCard.actions = actions;\n }\n\n return adaptiveCard;\n}\n\ninterface ConvertResult {\n elements: AdaptiveCardElement[];\n actions: AdaptiveCardAction[];\n}\n\n/**\n * Convert a card child element to Adaptive Card elements.\n */\nfunction convertChildToAdaptive(child: CardChild): ConvertResult {\n switch (child.type) {\n case \"text\":\n return { elements: [convertTextToElement(child)], actions: [] };\n case \"image\":\n return { elements: [convertImageToElement(child)], actions: [] };\n case \"divider\":\n return { elements: [convertDividerToElement(child)], actions: [] };\n case \"actions\":\n return convertActionsToElements(child);\n case \"section\":\n return convertSectionToElements(child);\n case \"fields\":\n return { elements: [convertFieldsToElement(child)], actions: [] };\n default:\n return { elements: [], actions: [] };\n }\n}\n\nfunction convertTextToElement(element: TextElement): AdaptiveCardElement {\n const textBlock: AdaptiveCardElement = {\n type: \"TextBlock\",\n text: convertEmoji(element.content),\n wrap: true,\n };\n\n if (element.style === \"bold\") {\n textBlock.weight = \"bolder\";\n } else if (element.style === \"muted\") {\n textBlock.isSubtle = true;\n }\n\n return textBlock;\n}\n\nfunction convertImageToElement(element: ImageElement): AdaptiveCardElement {\n return {\n type: \"Image\",\n url: element.url,\n altText: element.alt || \"Image\",\n size: \"auto\",\n };\n}\n\nfunction convertDividerToElement(\n _element: DividerElement,\n): AdaptiveCardElement {\n // Adaptive Cards don't have a native divider, use a separator container\n return {\n type: \"Container\",\n separator: true,\n items: [],\n };\n}\n\nfunction convertActionsToElements(element: ActionsElement): ConvertResult {\n // In Adaptive Cards, actions go at the card level, not inline\n const actions: AdaptiveCardAction[] = element.children.map((button) =>\n convertButtonToAction(button),\n );\n\n return { elements: [], actions };\n}\n\nfunction convertButtonToAction(button: ButtonElement): AdaptiveCardAction {\n const action: AdaptiveCardAction = {\n type: \"Action.Submit\",\n title: convertEmoji(button.label),\n data: {\n actionId: button.id,\n value: button.value,\n },\n };\n\n if (button.style === \"primary\") {\n action.style = \"positive\";\n } else if (button.style === \"danger\") {\n action.style = \"destructive\";\n }\n\n return action;\n}\n\nfunction convertSectionToElements(element: SectionElement): ConvertResult {\n const elements: AdaptiveCardElement[] = [];\n const actions: AdaptiveCardAction[] = [];\n\n // Wrap section in a container\n const containerItems: AdaptiveCardElement[] = [];\n\n for (const child of element.children) {\n const result = convertChildToAdaptive(child);\n containerItems.push(...result.elements);\n actions.push(...result.actions);\n }\n\n if (containerItems.length > 0) {\n elements.push({\n type: \"Container\",\n items: containerItems,\n });\n }\n\n return { elements, actions };\n}\n\nfunction convertFieldsToElement(element: FieldsElement): AdaptiveCardElement {\n // Use FactSet for key-value pairs\n const facts = element.children.map((field) => ({\n title: convertEmoji(field.label),\n value: convertEmoji(field.value),\n }));\n\n return {\n type: \"FactSet\",\n facts,\n };\n}\n\n/**\n * Generate fallback text from a card element.\n * Used when adaptive cards aren't supported.\n */\nexport function cardToFallbackText(card: CardElement): string {\n const parts: string[] = [];\n\n if (card.title) {\n parts.push(`**${convertEmoji(card.title)}**`);\n }\n\n if (card.subtitle) {\n parts.push(convertEmoji(card.subtitle));\n }\n\n for (const child of card.children) {\n const text = childToFallbackText(child);\n if (text) {\n parts.push(text);\n }\n }\n\n return parts.join(\"\\n\\n\");\n}\n\nfunction childToFallbackText(child: CardChild): string | null {\n switch (child.type) {\n case \"text\":\n return convertEmoji(child.content);\n case \"fields\":\n return child.children\n .map((f) => `**${convertEmoji(f.label)}**: ${convertEmoji(f.value)}`)\n .join(\"\\n\");\n case \"actions\":\n return `[${child.children.map((b) => convertEmoji(b.label)).join(\"] [\")}]`;\n case \"section\":\n return child.children\n .map((c) => childToFallbackText(c))\n .filter(Boolean)\n .join(\"\\n\");\n default:\n return null;\n }\n}\n","/**\n * Teams-specific format conversion using AST-based parsing.\n *\n * Teams supports a subset of HTML for formatting:\n * - Bold: <b> or <strong>\n * - Italic: <i> or <em>\n * - Strikethrough: <s> or <strike>\n * - Links: <a href=\"url\">text</a>\n * - Code: <pre> and <code>\n *\n * Teams also accepts standard markdown in most cases.\n */\n\nimport {\n BaseFormatConverter,\n type Code,\n type Content,\n type Delete,\n type Emphasis,\n type InlineCode,\n type Link,\n type Paragraph,\n type PostableMessage,\n parseMarkdown,\n type Root,\n type Strong,\n type Text,\n} from \"chat\";\n\nexport class TeamsFormatConverter extends BaseFormatConverter {\n /**\n * Convert @mentions to Teams format in plain text.\n * @name → <at>name</at>\n */\n private convertMentionsToTeams(text: string): string {\n return text.replace(/@(\\w+)/g, \"<at>$1</at>\");\n }\n\n /**\n * Override renderPostable to convert @mentions in plain strings.\n */\n override renderPostable(message: PostableMessage): string {\n if (typeof message === \"string\") {\n return this.convertMentionsToTeams(message);\n }\n if (\"raw\" in message) {\n return this.convertMentionsToTeams(message.raw);\n }\n if (\"markdown\" in message) {\n return this.fromAst(parseMarkdown(message.markdown));\n }\n if (\"ast\" in message) {\n return this.fromAst(message.ast);\n }\n return \"\";\n }\n\n /**\n * Render an AST to Teams format.\n * Teams accepts standard markdown, so we just stringify cleanly.\n */\n fromAst(ast: Root): string {\n const parts: string[] = [];\n\n for (const node of ast.children) {\n parts.push(this.nodeToTeams(node as Content));\n }\n\n return parts.join(\"\\n\\n\");\n }\n\n /**\n * Parse Teams message into an AST.\n * Converts Teams HTML/mentions to standard markdown format.\n */\n toAst(teamsText: string): Root {\n // Convert Teams HTML to markdown, then parse\n let markdown = teamsText;\n\n // Convert @mentions from Teams format: <at>Name</at> -> @Name\n markdown = markdown.replace(/<at>([^<]+)<\\/at>/gi, \"@$1\");\n\n // Convert HTML tags to markdown\n // Bold: <b>, <strong> -> **text**\n markdown = markdown.replace(\n /<(b|strong)>([^<]+)<\\/(b|strong)>/gi,\n \"**$2**\",\n );\n\n // Italic: <i>, <em> -> _text_\n markdown = markdown.replace(/<(i|em)>([^<]+)<\\/(i|em)>/gi, \"_$2_\");\n\n // Strikethrough: <s>, <strike> -> ~~text~~\n markdown = markdown.replace(\n /<(s|strike)>([^<]+)<\\/(s|strike)>/gi,\n \"~~$2~~\",\n );\n\n // Links: <a href=\"url\">text</a> -> [text](url)\n markdown = markdown.replace(\n /<a[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>/gi,\n \"[$2]($1)\",\n );\n\n // Code: <code>text</code> -> `text`\n markdown = markdown.replace(/<code>([^<]+)<\\/code>/gi, \"`$1`\");\n\n // Pre: <pre>text</pre> -> ```text```\n markdown = markdown.replace(/<pre>([^<]+)<\\/pre>/gi, \"```\\n$1\\n```\");\n\n // Strip remaining HTML tags\n markdown = markdown.replace(/<[^>]+>/g, \"\");\n\n // Decode HTML entities\n markdown = markdown\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/&/g, \"&\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\");\n\n return parseMarkdown(markdown);\n }\n\n private nodeToTeams(node: Content): string {\n switch (node.type) {\n case \"paragraph\":\n return (node as Paragraph).children\n .map((child) => this.nodeToTeams(child as Content))\n .join(\"\");\n\n case \"text\": {\n // Convert @mentions to Teams format <at>mention</at>\n const textValue = (node as Text).value;\n return textValue.replace(/@(\\w+)/g, \"<at>$1</at>\");\n }\n\n case \"strong\":\n // Teams supports **text** markdown\n return `**${(node as Strong).children\n .map((child) => this.nodeToTeams(child as Content))\n .join(\"\")}**`;\n\n case \"emphasis\":\n // Teams supports _text_ markdown\n return `_${(node as Emphasis).children\n .map((child) => this.nodeToTeams(child as Content))\n .join(\"\")}_`;\n\n case \"delete\":\n // Teams supports ~~text~~ markdown\n return `~~${(node as Delete).children\n .map((child) => this.nodeToTeams(child as Content))\n .join(\"\")}~~`;\n\n case \"inlineCode\":\n return `\\`${(node as InlineCode).value}\\``;\n\n case \"code\": {\n const codeNode = node as Code;\n return `\\`\\`\\`${codeNode.lang || \"\"}\\n${codeNode.value}\\n\\`\\`\\``;\n }\n\n case \"link\": {\n const linkNode = node as Link;\n const linkText = linkNode.children\n .map((child) => this.nodeToTeams(child as Content))\n .join(\"\");\n // Standard markdown link format\n return `[${linkText}](${linkNode.url})`;\n }\n\n case \"blockquote\":\n return node.children\n .map((child) => `> ${this.nodeToTeams(child as Content)}`)\n .join(\"\\n\");\n\n case \"list\":\n return node.children\n .map((item, i) => {\n const prefix = node.ordered ? `${i + 1}.` : \"-\";\n const content = item.children\n .map((child) => this.nodeToTeams(child as Content))\n .join(\"\");\n return `${prefix} ${content}`;\n })\n .join(\"\\n\");\n\n case \"listItem\":\n return node.children\n .map((child) => this.nodeToTeams(child as Content))\n .join(\"\");\n\n case \"break\":\n return \"\\n\";\n\n case \"thematicBreak\":\n return \"---\";\n\n default:\n // For unsupported nodes, try to extract text\n if (\"children\" in node && Array.isArray(node.children)) {\n return node.children\n .map((child) => this.nodeToTeams(child as Content))\n .join(\"\");\n }\n if (\"value\" in node) {\n return String(node.value);\n }\n return \"\";\n }\n }\n}\n"],"mappings":"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","names":["convertEmojiPlaceholders","convertEmojiPlaceholders"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chat-adapter/teams",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Microsoft Teams adapter for chat",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"botbuilder": "^4.23.1",
|
|
20
|
+
"chat": "4.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.10.2",
|
|
24
|
+
"tsup": "^8.3.5",
|
|
25
|
+
"typescript": "^5.7.2",
|
|
26
|
+
"vitest": "^2.1.8"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"chat",
|
|
33
|
+
"teams",
|
|
34
|
+
"microsoft",
|
|
35
|
+
"bot",
|
|
36
|
+
"adapter"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"lint": "biome check src",
|
|
46
|
+
"clean": "rm -rf dist"
|
|
47
|
+
}
|
|
48
|
+
}
|