@chat-adapter/teams 4.2.0 → 4.4.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/README.md +135 -16
- package/dist/index.d.ts +7 -9
- package/dist/index.js +302 -207
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -11,19 +11,29 @@ import {
|
|
|
11
11
|
TeamsInfo
|
|
12
12
|
} from "botbuilder";
|
|
13
13
|
import {
|
|
14
|
-
|
|
14
|
+
AdapterRateLimitError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
bufferToDataUri,
|
|
17
|
+
extractCard,
|
|
18
|
+
extractFiles,
|
|
19
|
+
NetworkError,
|
|
20
|
+
PermissionError,
|
|
21
|
+
toBuffer,
|
|
22
|
+
ValidationError
|
|
23
|
+
} from "@chat-adapter/shared";
|
|
24
|
+
import {
|
|
25
|
+
convertEmojiPlaceholders,
|
|
15
26
|
defaultEmojiResolver,
|
|
16
|
-
isCardElement,
|
|
17
27
|
NotImplementedError
|
|
18
28
|
} from "chat";
|
|
19
29
|
|
|
20
30
|
// src/cards.ts
|
|
21
31
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
createEmojiConverter,
|
|
33
|
+
mapButtonStyle,
|
|
34
|
+
cardToFallbackText as sharedCardToFallbackText
|
|
35
|
+
} from "@chat-adapter/shared";
|
|
36
|
+
var convertEmoji = createEmojiConverter("teams");
|
|
27
37
|
var ADAPTIVE_CARD_SCHEMA = "http://adaptivecards.io/schemas/adaptive-card.json";
|
|
28
38
|
var ADAPTIVE_CARD_VERSION = "1.4";
|
|
29
39
|
function cardToAdaptiveCard(card) {
|
|
@@ -130,10 +140,9 @@ function convertButtonToAction(button) {
|
|
|
130
140
|
value: button.value
|
|
131
141
|
}
|
|
132
142
|
};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
action.style = "destructive";
|
|
143
|
+
const style = mapButtonStyle(button.style, "teams");
|
|
144
|
+
if (style) {
|
|
145
|
+
action.style = style;
|
|
137
146
|
}
|
|
138
147
|
return action;
|
|
139
148
|
}
|
|
@@ -165,39 +174,29 @@ function convertFieldsToElement(element) {
|
|
|
165
174
|
};
|
|
166
175
|
}
|
|
167
176
|
function cardToFallbackText(card) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
parts.push(convertEmoji(card.subtitle));
|
|
174
|
-
}
|
|
175
|
-
for (const child of card.children) {
|
|
176
|
-
const text = childToFallbackText(child);
|
|
177
|
-
if (text) {
|
|
178
|
-
parts.push(text);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return parts.join("\n\n");
|
|
182
|
-
}
|
|
183
|
-
function childToFallbackText(child) {
|
|
184
|
-
switch (child.type) {
|
|
185
|
-
case "text":
|
|
186
|
-
return convertEmoji(child.content);
|
|
187
|
-
case "fields":
|
|
188
|
-
return child.children.map((f) => `**${convertEmoji(f.label)}**: ${convertEmoji(f.value)}`).join("\n");
|
|
189
|
-
case "actions":
|
|
190
|
-
return `[${child.children.map((b) => convertEmoji(b.label)).join("] [")}]`;
|
|
191
|
-
case "section":
|
|
192
|
-
return child.children.map((c) => childToFallbackText(c)).filter(Boolean).join("\n");
|
|
193
|
-
default:
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
177
|
+
return sharedCardToFallbackText(card, {
|
|
178
|
+
boldFormat: "**",
|
|
179
|
+
lineBreak: "\n\n",
|
|
180
|
+
platform: "teams"
|
|
181
|
+
});
|
|
196
182
|
}
|
|
197
183
|
|
|
198
184
|
// src/markdown.ts
|
|
199
185
|
import {
|
|
200
186
|
BaseFormatConverter,
|
|
187
|
+
getNodeChildren,
|
|
188
|
+
getNodeValue,
|
|
189
|
+
isBlockquoteNode,
|
|
190
|
+
isCodeNode,
|
|
191
|
+
isDeleteNode,
|
|
192
|
+
isEmphasisNode,
|
|
193
|
+
isInlineCodeNode,
|
|
194
|
+
isLinkNode,
|
|
195
|
+
isListItemNode,
|
|
196
|
+
isListNode,
|
|
197
|
+
isParagraphNode,
|
|
198
|
+
isStrongNode,
|
|
199
|
+
isTextNode,
|
|
201
200
|
parseMarkdown
|
|
202
201
|
} from "chat";
|
|
203
202
|
var TeamsFormatConverter = class extends BaseFormatConverter {
|
|
@@ -231,11 +230,7 @@ var TeamsFormatConverter = class extends BaseFormatConverter {
|
|
|
231
230
|
* Teams accepts standard markdown, so we just stringify cleanly.
|
|
232
231
|
*/
|
|
233
232
|
fromAst(ast) {
|
|
234
|
-
|
|
235
|
-
for (const node of ast.children) {
|
|
236
|
-
parts.push(this.nodeToTeams(node));
|
|
237
|
-
}
|
|
238
|
-
return parts.join("\n\n");
|
|
233
|
+
return this.fromAstWithNodeConverter(ast, (node) => this.nodeToTeams(node));
|
|
239
234
|
}
|
|
240
235
|
/**
|
|
241
236
|
* Parse Teams message into an AST.
|
|
@@ -264,55 +259,60 @@ var TeamsFormatConverter = class extends BaseFormatConverter {
|
|
|
264
259
|
return parseMarkdown(markdown);
|
|
265
260
|
}
|
|
266
261
|
nodeToTeams(node) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
262
|
+
if (isParagraphNode(node)) {
|
|
263
|
+
return getNodeChildren(node).map((child) => this.nodeToTeams(child)).join("");
|
|
264
|
+
}
|
|
265
|
+
if (isTextNode(node)) {
|
|
266
|
+
return node.value.replace(/@(\w+)/g, "<at>$1</at>");
|
|
267
|
+
}
|
|
268
|
+
if (isStrongNode(node)) {
|
|
269
|
+
const content = getNodeChildren(node).map((child) => this.nodeToTeams(child)).join("");
|
|
270
|
+
return `**${content}**`;
|
|
271
|
+
}
|
|
272
|
+
if (isEmphasisNode(node)) {
|
|
273
|
+
const content = getNodeChildren(node).map((child) => this.nodeToTeams(child)).join("");
|
|
274
|
+
return `_${content}_`;
|
|
275
|
+
}
|
|
276
|
+
if (isDeleteNode(node)) {
|
|
277
|
+
const content = getNodeChildren(node).map((child) => this.nodeToTeams(child)).join("");
|
|
278
|
+
return `~~${content}~~`;
|
|
279
|
+
}
|
|
280
|
+
if (isInlineCodeNode(node)) {
|
|
281
|
+
return `\`${node.value}\``;
|
|
282
|
+
}
|
|
283
|
+
if (isCodeNode(node)) {
|
|
284
|
+
return `\`\`\`${node.lang || ""}
|
|
285
|
+
${node.value}
|
|
286
286
|
\`\`\``;
|
|
287
|
-
}
|
|
288
|
-
case "link": {
|
|
289
|
-
const linkNode = node;
|
|
290
|
-
const linkText = linkNode.children.map((child) => this.nodeToTeams(child)).join("");
|
|
291
|
-
return `[${linkText}](${linkNode.url})`;
|
|
292
|
-
}
|
|
293
|
-
case "blockquote":
|
|
294
|
-
return node.children.map((child) => `> ${this.nodeToTeams(child)}`).join("\n");
|
|
295
|
-
case "list":
|
|
296
|
-
return node.children.map((item, i) => {
|
|
297
|
-
const prefix = node.ordered ? `${i + 1}.` : "-";
|
|
298
|
-
const content = item.children.map((child) => this.nodeToTeams(child)).join("");
|
|
299
|
-
return `${prefix} ${content}`;
|
|
300
|
-
}).join("\n");
|
|
301
|
-
case "listItem":
|
|
302
|
-
return node.children.map((child) => this.nodeToTeams(child)).join("");
|
|
303
|
-
case "break":
|
|
304
|
-
return "\n";
|
|
305
|
-
case "thematicBreak":
|
|
306
|
-
return "---";
|
|
307
|
-
default:
|
|
308
|
-
if ("children" in node && Array.isArray(node.children)) {
|
|
309
|
-
return node.children.map((child) => this.nodeToTeams(child)).join("");
|
|
310
|
-
}
|
|
311
|
-
if ("value" in node) {
|
|
312
|
-
return String(node.value);
|
|
313
|
-
}
|
|
314
|
-
return "";
|
|
315
287
|
}
|
|
288
|
+
if (isLinkNode(node)) {
|
|
289
|
+
const linkText = getNodeChildren(node).map((child) => this.nodeToTeams(child)).join("");
|
|
290
|
+
return `[${linkText}](${node.url})`;
|
|
291
|
+
}
|
|
292
|
+
if (isBlockquoteNode(node)) {
|
|
293
|
+
return getNodeChildren(node).map((child) => `> ${this.nodeToTeams(child)}`).join("\n");
|
|
294
|
+
}
|
|
295
|
+
if (isListNode(node)) {
|
|
296
|
+
return getNodeChildren(node).map((item, i) => {
|
|
297
|
+
const prefix = node.ordered ? `${i + 1}.` : "-";
|
|
298
|
+
const content = getNodeChildren(item).map((child) => this.nodeToTeams(child)).join("");
|
|
299
|
+
return `${prefix} ${content}`;
|
|
300
|
+
}).join("\n");
|
|
301
|
+
}
|
|
302
|
+
if (isListItemNode(node)) {
|
|
303
|
+
return getNodeChildren(node).map((child) => this.nodeToTeams(child)).join("");
|
|
304
|
+
}
|
|
305
|
+
if (node.type === "break") {
|
|
306
|
+
return "\n";
|
|
307
|
+
}
|
|
308
|
+
if (node.type === "thematicBreak") {
|
|
309
|
+
return "---";
|
|
310
|
+
}
|
|
311
|
+
const children = getNodeChildren(node);
|
|
312
|
+
if (children.length > 0) {
|
|
313
|
+
return children.map((child) => this.nodeToTeams(child)).join("");
|
|
314
|
+
}
|
|
315
|
+
return getNodeValue(node);
|
|
316
316
|
}
|
|
317
317
|
};
|
|
318
318
|
|
|
@@ -329,14 +329,18 @@ var TeamsAdapter = class {
|
|
|
329
329
|
botAdapter;
|
|
330
330
|
graphClient = null;
|
|
331
331
|
chat = null;
|
|
332
|
-
logger
|
|
332
|
+
logger;
|
|
333
333
|
formatConverter = new TeamsFormatConverter();
|
|
334
334
|
config;
|
|
335
335
|
constructor(config) {
|
|
336
336
|
this.config = config;
|
|
337
|
+
this.logger = config.logger;
|
|
337
338
|
this.userName = config.userName || "bot";
|
|
338
339
|
if (config.appType === "SingleTenant" && !config.appTenantId) {
|
|
339
|
-
throw new
|
|
340
|
+
throw new ValidationError(
|
|
341
|
+
"teams",
|
|
342
|
+
"appTenantId is required for SingleTenant app type"
|
|
343
|
+
);
|
|
340
344
|
}
|
|
341
345
|
const auth = new ConfigurationBotFrameworkAuthentication({
|
|
342
346
|
MicrosoftAppId: config.appId,
|
|
@@ -362,16 +366,15 @@ var TeamsAdapter = class {
|
|
|
362
366
|
}
|
|
363
367
|
async initialize(chat) {
|
|
364
368
|
this.chat = chat;
|
|
365
|
-
this.logger = chat.getLogger(this.name);
|
|
366
369
|
}
|
|
367
370
|
async handleWebhook(request, options) {
|
|
368
371
|
const body = await request.text();
|
|
369
|
-
this.logger
|
|
372
|
+
this.logger.debug("Teams webhook raw body", { body });
|
|
370
373
|
let activity;
|
|
371
374
|
try {
|
|
372
375
|
activity = JSON.parse(body);
|
|
373
376
|
} catch (e) {
|
|
374
|
-
this.logger
|
|
377
|
+
this.logger.error("Failed to parse request body", { error: e });
|
|
375
378
|
return new Response("Invalid JSON", { status: 400 });
|
|
376
379
|
}
|
|
377
380
|
const authHeader = request.headers.get("authorization") || "";
|
|
@@ -388,7 +391,7 @@ var TeamsAdapter = class {
|
|
|
388
391
|
headers: { "Content-Type": "application/json" }
|
|
389
392
|
});
|
|
390
393
|
} catch (error) {
|
|
391
|
-
this.logger
|
|
394
|
+
this.logger.error("Bot adapter process error", { error });
|
|
392
395
|
return new Response(JSON.stringify({ error: "Internal error" }), {
|
|
393
396
|
status: 500,
|
|
394
397
|
headers: { "Content-Type": "application/json" }
|
|
@@ -397,7 +400,7 @@ var TeamsAdapter = class {
|
|
|
397
400
|
}
|
|
398
401
|
async handleTurn(context, options) {
|
|
399
402
|
if (!this.chat) {
|
|
400
|
-
this.logger
|
|
403
|
+
this.logger.warn("Chat instance not initialized, ignoring event");
|
|
401
404
|
return;
|
|
402
405
|
}
|
|
403
406
|
const activity = context.activity;
|
|
@@ -406,9 +409,19 @@ var TeamsAdapter = class {
|
|
|
406
409
|
const channelData = activity.channelData;
|
|
407
410
|
const tenantId = channelData?.tenant?.id;
|
|
408
411
|
const ttl = 30 * 24 * 60 * 60 * 1e3;
|
|
409
|
-
this.chat.getState().set(`teams:serviceUrl:${userId}`, activity.serviceUrl, ttl)
|
|
412
|
+
this.chat.getState().set(`teams:serviceUrl:${userId}`, activity.serviceUrl, ttl).catch((err) => {
|
|
413
|
+
this.logger.error("Failed to cache serviceUrl", {
|
|
414
|
+
userId,
|
|
415
|
+
error: err
|
|
416
|
+
});
|
|
417
|
+
});
|
|
410
418
|
if (tenantId) {
|
|
411
|
-
this.chat.getState().set(`teams:tenantId:${userId}`, tenantId, ttl)
|
|
419
|
+
this.chat.getState().set(`teams:tenantId:${userId}`, tenantId, ttl).catch((err) => {
|
|
420
|
+
this.logger.error("Failed to cache tenantId", {
|
|
421
|
+
userId,
|
|
422
|
+
error: err
|
|
423
|
+
});
|
|
424
|
+
});
|
|
412
425
|
}
|
|
413
426
|
const team = channelData?.team;
|
|
414
427
|
const teamAadGroupId = team?.aadGroupId;
|
|
@@ -423,11 +436,21 @@ var TeamsAdapter = class {
|
|
|
423
436
|
tenantId
|
|
424
437
|
};
|
|
425
438
|
const contextJson = JSON.stringify(context2);
|
|
426
|
-
this.chat.getState().set(`teams:channelContext:${baseChannelId}`, contextJson, ttl)
|
|
439
|
+
this.chat.getState().set(`teams:channelContext:${baseChannelId}`, contextJson, ttl).catch((err) => {
|
|
440
|
+
this.logger.error("Failed to cache channel context", {
|
|
441
|
+
conversationId: baseChannelId,
|
|
442
|
+
error: err
|
|
443
|
+
});
|
|
444
|
+
});
|
|
427
445
|
if (teamThreadId) {
|
|
428
|
-
this.chat.getState().set(`teams:teamContext:${teamThreadId}`, contextJson, ttl)
|
|
446
|
+
this.chat.getState().set(`teams:teamContext:${teamThreadId}`, contextJson, ttl).catch((err) => {
|
|
447
|
+
this.logger.error("Failed to cache team context", {
|
|
448
|
+
teamThreadId,
|
|
449
|
+
error: err
|
|
450
|
+
});
|
|
451
|
+
});
|
|
429
452
|
}
|
|
430
|
-
this.logger
|
|
453
|
+
this.logger.info(
|
|
431
454
|
"Cached Teams team GUID from installation/update event",
|
|
432
455
|
{
|
|
433
456
|
activityType: activity.type,
|
|
@@ -444,8 +467,13 @@ var TeamsAdapter = class {
|
|
|
444
467
|
`teams:channelContext:${baseChannelId}`,
|
|
445
468
|
cachedTeamContext,
|
|
446
469
|
ttl
|
|
447
|
-
)
|
|
448
|
-
|
|
470
|
+
).catch((err) => {
|
|
471
|
+
this.logger.error("Failed to cache channel context from team", {
|
|
472
|
+
conversationId: baseChannelId,
|
|
473
|
+
error: err
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
this.logger.info("Using cached Teams team GUID for channel", {
|
|
449
477
|
conversationId: baseChannelId,
|
|
450
478
|
teamThreadId
|
|
451
479
|
});
|
|
@@ -459,9 +487,19 @@ var TeamsAdapter = class {
|
|
|
459
487
|
tenantId
|
|
460
488
|
};
|
|
461
489
|
const contextJson = JSON.stringify(fetchedContext);
|
|
462
|
-
this.chat.getState().set(`teams:channelContext:${baseChannelId}`, contextJson, ttl)
|
|
463
|
-
|
|
464
|
-
|
|
490
|
+
this.chat.getState().set(`teams:channelContext:${baseChannelId}`, contextJson, ttl).catch((err) => {
|
|
491
|
+
this.logger.error("Failed to cache fetched channel context", {
|
|
492
|
+
conversationId: baseChannelId,
|
|
493
|
+
error: err
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
this.chat.getState().set(`teams:teamContext:${teamThreadId}`, contextJson, ttl).catch((err) => {
|
|
497
|
+
this.logger.error("Failed to cache fetched team context", {
|
|
498
|
+
teamThreadId,
|
|
499
|
+
error: err
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
this.logger.info(
|
|
465
503
|
"Fetched and cached Teams team GUID via TeamsInfo API",
|
|
466
504
|
{
|
|
467
505
|
conversationId: baseChannelId,
|
|
@@ -472,7 +510,7 @@ var TeamsAdapter = class {
|
|
|
472
510
|
);
|
|
473
511
|
}
|
|
474
512
|
} catch (error) {
|
|
475
|
-
this.logger
|
|
513
|
+
this.logger.debug(
|
|
476
514
|
"Could not fetch team details (may not be a team scope)",
|
|
477
515
|
{ teamThreadId, error }
|
|
478
516
|
);
|
|
@@ -489,7 +527,7 @@ var TeamsAdapter = class {
|
|
|
489
527
|
return;
|
|
490
528
|
}
|
|
491
529
|
if (activity.type !== ActivityTypes.Message) {
|
|
492
|
-
this.logger
|
|
530
|
+
this.logger.debug("Ignoring non-message activity", {
|
|
493
531
|
type: activity.type
|
|
494
532
|
});
|
|
495
533
|
return;
|
|
@@ -536,7 +574,7 @@ var TeamsAdapter = class {
|
|
|
536
574
|
adapter: this,
|
|
537
575
|
raw: activity
|
|
538
576
|
};
|
|
539
|
-
this.logger
|
|
577
|
+
this.logger.debug("Processing Teams message action (Action.Submit)", {
|
|
540
578
|
actionId: actionValue.actionId,
|
|
541
579
|
value: actionValue.value,
|
|
542
580
|
messageId: actionEvent.messageId,
|
|
@@ -553,7 +591,7 @@ var TeamsAdapter = class {
|
|
|
553
591
|
await this.handleAdaptiveCardAction(context, activity, options);
|
|
554
592
|
return;
|
|
555
593
|
}
|
|
556
|
-
this.logger
|
|
594
|
+
this.logger.debug("Ignoring unsupported invoke", {
|
|
557
595
|
name: activity.name
|
|
558
596
|
});
|
|
559
597
|
}
|
|
@@ -565,7 +603,7 @@ var TeamsAdapter = class {
|
|
|
565
603
|
if (!this.chat) return;
|
|
566
604
|
const actionData = activity.value?.action?.data;
|
|
567
605
|
if (!actionData?.actionId) {
|
|
568
|
-
this.logger
|
|
606
|
+
this.logger.debug("Adaptive card action missing actionId", {
|
|
569
607
|
value: activity.value
|
|
570
608
|
});
|
|
571
609
|
await context.sendActivity({
|
|
@@ -593,7 +631,7 @@ var TeamsAdapter = class {
|
|
|
593
631
|
adapter: this,
|
|
594
632
|
raw: activity
|
|
595
633
|
};
|
|
596
|
-
this.logger
|
|
634
|
+
this.logger.debug("Processing Teams adaptive card action", {
|
|
597
635
|
actionId: actionData.actionId,
|
|
598
636
|
value: actionData.value,
|
|
599
637
|
messageId: actionEvent.messageId,
|
|
@@ -637,7 +675,7 @@ var TeamsAdapter = class {
|
|
|
637
675
|
threadId,
|
|
638
676
|
raw: activity
|
|
639
677
|
};
|
|
640
|
-
this.logger
|
|
678
|
+
this.logger.debug("Processing Teams reaction added", {
|
|
641
679
|
emoji: emojiValue.name,
|
|
642
680
|
rawEmoji,
|
|
643
681
|
messageId
|
|
@@ -657,7 +695,7 @@ var TeamsAdapter = class {
|
|
|
657
695
|
threadId,
|
|
658
696
|
raw: activity
|
|
659
697
|
};
|
|
660
|
-
this.logger
|
|
698
|
+
this.logger.debug("Processing Teams reaction removed", {
|
|
661
699
|
emoji: emojiValue.name,
|
|
662
700
|
rawEmoji,
|
|
663
701
|
messageId
|
|
@@ -718,7 +756,8 @@ var TeamsAdapter = class {
|
|
|
718
756
|
fetchData: url ? async () => {
|
|
719
757
|
const response = await fetch(url);
|
|
720
758
|
if (!response.ok) {
|
|
721
|
-
throw new
|
|
759
|
+
throw new NetworkError(
|
|
760
|
+
"teams",
|
|
722
761
|
`Failed to fetch file: ${response.status} ${response.statusText}`
|
|
723
762
|
);
|
|
724
763
|
}
|
|
@@ -732,9 +771,9 @@ var TeamsAdapter = class {
|
|
|
732
771
|
}
|
|
733
772
|
async postMessage(threadId, message) {
|
|
734
773
|
const { conversationId, serviceUrl } = this.decodeThreadId(threadId);
|
|
735
|
-
const files =
|
|
774
|
+
const files = extractFiles(message);
|
|
736
775
|
const fileAttachments = files.length > 0 ? await this.filesToAttachments(files) : [];
|
|
737
|
-
const card =
|
|
776
|
+
const card = extractCard(message);
|
|
738
777
|
let activity;
|
|
739
778
|
if (card) {
|
|
740
779
|
const adaptiveCard = cardToAdaptiveCard(card);
|
|
@@ -749,13 +788,13 @@ var TeamsAdapter = class {
|
|
|
749
788
|
...fileAttachments
|
|
750
789
|
]
|
|
751
790
|
};
|
|
752
|
-
this.logger
|
|
791
|
+
this.logger.debug("Teams API: sendActivity (adaptive card)", {
|
|
753
792
|
conversationId,
|
|
754
793
|
serviceUrl,
|
|
755
794
|
fileCount: fileAttachments.length
|
|
756
795
|
});
|
|
757
796
|
} else {
|
|
758
|
-
const text =
|
|
797
|
+
const text = convertEmojiPlaceholders(
|
|
759
798
|
this.formatConverter.renderPostable(message),
|
|
760
799
|
"teams"
|
|
761
800
|
);
|
|
@@ -765,7 +804,7 @@ var TeamsAdapter = class {
|
|
|
765
804
|
textFormat: "markdown",
|
|
766
805
|
attachments: fileAttachments.length > 0 ? fileAttachments : void 0
|
|
767
806
|
};
|
|
768
|
-
this.logger
|
|
807
|
+
this.logger.debug("Teams API: sendActivity (message)", {
|
|
769
808
|
conversationId,
|
|
770
809
|
serviceUrl,
|
|
771
810
|
textLength: text.length,
|
|
@@ -778,42 +817,29 @@ var TeamsAdapter = class {
|
|
|
778
817
|
conversation: { id: conversationId }
|
|
779
818
|
};
|
|
780
819
|
let messageId = "";
|
|
781
|
-
|
|
782
|
-
this.
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
820
|
+
try {
|
|
821
|
+
await this.botAdapter.continueConversationAsync(
|
|
822
|
+
this.config.appId,
|
|
823
|
+
conversationReference,
|
|
824
|
+
async (context) => {
|
|
825
|
+
const response = await context.sendActivity(activity);
|
|
826
|
+
messageId = response?.id || "";
|
|
827
|
+
}
|
|
828
|
+
);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
this.logger.error("Teams API: sendActivity failed", {
|
|
831
|
+
conversationId,
|
|
832
|
+
error
|
|
833
|
+
});
|
|
834
|
+
this.handleTeamsError(error, "postMessage");
|
|
835
|
+
}
|
|
836
|
+
this.logger.debug("Teams API: sendActivity response", { messageId });
|
|
790
837
|
return {
|
|
791
838
|
id: messageId,
|
|
792
839
|
threadId,
|
|
793
840
|
raw: activity
|
|
794
841
|
};
|
|
795
842
|
}
|
|
796
|
-
/**
|
|
797
|
-
* Extract card element from a AdapterPostableMessage if present.
|
|
798
|
-
*/
|
|
799
|
-
extractCard(message) {
|
|
800
|
-
if (isCardElement(message)) {
|
|
801
|
-
return message;
|
|
802
|
-
}
|
|
803
|
-
if (typeof message === "object" && message !== null && "card" in message) {
|
|
804
|
-
return message.card;
|
|
805
|
-
}
|
|
806
|
-
return null;
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Extract files from a AdapterPostableMessage if present.
|
|
810
|
-
*/
|
|
811
|
-
extractFiles(message) {
|
|
812
|
-
if (typeof message === "object" && message !== null && "files" in message) {
|
|
813
|
-
return message.files ?? [];
|
|
814
|
-
}
|
|
815
|
-
return [];
|
|
816
|
-
}
|
|
817
843
|
/**
|
|
818
844
|
* Convert files to Teams attachments.
|
|
819
845
|
* Uses inline data URIs for small files.
|
|
@@ -821,20 +847,15 @@ var TeamsAdapter = class {
|
|
|
821
847
|
async filesToAttachments(files) {
|
|
822
848
|
const attachments = [];
|
|
823
849
|
for (const file of files) {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
} else if (file.data instanceof Blob) {
|
|
830
|
-
const arrayBuffer = await file.data.arrayBuffer();
|
|
831
|
-
buffer = Buffer.from(arrayBuffer);
|
|
832
|
-
} else {
|
|
850
|
+
const buffer = await toBuffer(file.data, {
|
|
851
|
+
platform: "teams",
|
|
852
|
+
throwOnUnsupported: false
|
|
853
|
+
});
|
|
854
|
+
if (!buffer) {
|
|
833
855
|
continue;
|
|
834
856
|
}
|
|
835
857
|
const mimeType = file.mimeType || "application/octet-stream";
|
|
836
|
-
const
|
|
837
|
-
const dataUri = `data:${mimeType};base64,${base64}`;
|
|
858
|
+
const dataUri = bufferToDataUri(buffer, mimeType);
|
|
838
859
|
attachments.push({
|
|
839
860
|
contentType: mimeType,
|
|
840
861
|
contentUrl: dataUri,
|
|
@@ -845,7 +866,7 @@ var TeamsAdapter = class {
|
|
|
845
866
|
}
|
|
846
867
|
async editMessage(threadId, messageId, message) {
|
|
847
868
|
const { conversationId, serviceUrl } = this.decodeThreadId(threadId);
|
|
848
|
-
const card =
|
|
869
|
+
const card = extractCard(message);
|
|
849
870
|
let activity;
|
|
850
871
|
if (card) {
|
|
851
872
|
const adaptiveCard = cardToAdaptiveCard(card);
|
|
@@ -860,12 +881,12 @@ var TeamsAdapter = class {
|
|
|
860
881
|
}
|
|
861
882
|
]
|
|
862
883
|
};
|
|
863
|
-
this.logger
|
|
884
|
+
this.logger.debug("Teams API: updateActivity (adaptive card)", {
|
|
864
885
|
conversationId,
|
|
865
886
|
messageId
|
|
866
887
|
});
|
|
867
888
|
} else {
|
|
868
|
-
const text =
|
|
889
|
+
const text = convertEmojiPlaceholders(
|
|
869
890
|
this.formatConverter.renderPostable(message),
|
|
870
891
|
"teams"
|
|
871
892
|
);
|
|
@@ -875,7 +896,7 @@ var TeamsAdapter = class {
|
|
|
875
896
|
text,
|
|
876
897
|
textFormat: "markdown"
|
|
877
898
|
};
|
|
878
|
-
this.logger
|
|
899
|
+
this.logger.debug("Teams API: updateActivity", {
|
|
879
900
|
conversationId,
|
|
880
901
|
messageId,
|
|
881
902
|
textLength: text.length
|
|
@@ -886,14 +907,23 @@ var TeamsAdapter = class {
|
|
|
886
907
|
serviceUrl,
|
|
887
908
|
conversation: { id: conversationId }
|
|
888
909
|
};
|
|
889
|
-
|
|
890
|
-
this.
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
910
|
+
try {
|
|
911
|
+
await this.botAdapter.continueConversationAsync(
|
|
912
|
+
this.config.appId,
|
|
913
|
+
conversationReference,
|
|
914
|
+
async (context) => {
|
|
915
|
+
await context.updateActivity(activity);
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
this.logger.error("Teams API: updateActivity failed", {
|
|
920
|
+
conversationId,
|
|
921
|
+
messageId,
|
|
922
|
+
error
|
|
923
|
+
});
|
|
924
|
+
this.handleTeamsError(error, "editMessage");
|
|
925
|
+
}
|
|
926
|
+
this.logger.debug("Teams API: updateActivity response", { ok: true });
|
|
897
927
|
return {
|
|
898
928
|
id: messageId,
|
|
899
929
|
threadId,
|
|
@@ -907,18 +937,27 @@ var TeamsAdapter = class {
|
|
|
907
937
|
serviceUrl,
|
|
908
938
|
conversation: { id: conversationId }
|
|
909
939
|
};
|
|
910
|
-
this.logger
|
|
940
|
+
this.logger.debug("Teams API: deleteActivity", {
|
|
911
941
|
conversationId,
|
|
912
942
|
messageId
|
|
913
943
|
});
|
|
914
|
-
|
|
915
|
-
this.
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
944
|
+
try {
|
|
945
|
+
await this.botAdapter.continueConversationAsync(
|
|
946
|
+
this.config.appId,
|
|
947
|
+
conversationReference,
|
|
948
|
+
async (context) => {
|
|
949
|
+
await context.deleteActivity(messageId);
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
this.logger.error("Teams API: deleteActivity failed", {
|
|
954
|
+
conversationId,
|
|
955
|
+
messageId,
|
|
956
|
+
error
|
|
957
|
+
});
|
|
958
|
+
this.handleTeamsError(error, "deleteMessage");
|
|
959
|
+
}
|
|
960
|
+
this.logger.debug("Teams API: deleteActivity response", { ok: true });
|
|
922
961
|
}
|
|
923
962
|
async addReaction(_threadId, _messageId, _emoji) {
|
|
924
963
|
throw new NotImplementedError(
|
|
@@ -939,15 +978,23 @@ var TeamsAdapter = class {
|
|
|
939
978
|
serviceUrl,
|
|
940
979
|
conversation: { id: conversationId }
|
|
941
980
|
};
|
|
942
|
-
this.logger
|
|
943
|
-
|
|
944
|
-
this.
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
981
|
+
this.logger.debug("Teams API: sendActivity (typing)", { conversationId });
|
|
982
|
+
try {
|
|
983
|
+
await this.botAdapter.continueConversationAsync(
|
|
984
|
+
this.config.appId,
|
|
985
|
+
conversationReference,
|
|
986
|
+
async (context) => {
|
|
987
|
+
await context.sendActivity({ type: ActivityTypes.Typing });
|
|
988
|
+
}
|
|
989
|
+
);
|
|
990
|
+
} catch (error) {
|
|
991
|
+
this.logger.error("Teams API: sendActivity (typing) failed", {
|
|
992
|
+
conversationId,
|
|
993
|
+
error
|
|
994
|
+
});
|
|
995
|
+
this.handleTeamsError(error, "startTyping");
|
|
996
|
+
}
|
|
997
|
+
this.logger.debug("Teams API: sendActivity (typing) response", {
|
|
951
998
|
ok: true
|
|
952
999
|
});
|
|
953
1000
|
}
|
|
@@ -963,7 +1010,7 @@ var TeamsAdapter = class {
|
|
|
963
1010
|
const cachedTenantId = await this.chat?.getState().get(`teams:tenantId:${userId}`);
|
|
964
1011
|
const serviceUrl = cachedServiceUrl || "https://smba.trafficmanager.net/teams/";
|
|
965
1012
|
const tenantId = cachedTenantId || this.config.appTenantId;
|
|
966
|
-
this.logger
|
|
1013
|
+
this.logger.debug("Teams: creating 1:1 conversation", {
|
|
967
1014
|
userId,
|
|
968
1015
|
serviceUrl,
|
|
969
1016
|
tenantId,
|
|
@@ -971,7 +1018,8 @@ var TeamsAdapter = class {
|
|
|
971
1018
|
cachedTenantId: !!cachedTenantId
|
|
972
1019
|
});
|
|
973
1020
|
if (!tenantId) {
|
|
974
|
-
throw new
|
|
1021
|
+
throw new ValidationError(
|
|
1022
|
+
"teams",
|
|
975
1023
|
"Cannot open DM: tenant ID not found. User must interact with the bot first (via @mention) to cache their tenant ID."
|
|
976
1024
|
);
|
|
977
1025
|
}
|
|
@@ -993,16 +1041,19 @@ var TeamsAdapter = class {
|
|
|
993
1041
|
},
|
|
994
1042
|
async (turnContext) => {
|
|
995
1043
|
conversationId = turnContext?.activity?.conversation?.id || "";
|
|
996
|
-
this.logger
|
|
1044
|
+
this.logger.debug("Teams: conversation created in callback", {
|
|
997
1045
|
conversationId,
|
|
998
1046
|
activityId: turnContext?.activity?.id
|
|
999
1047
|
});
|
|
1000
1048
|
}
|
|
1001
1049
|
);
|
|
1002
1050
|
if (!conversationId) {
|
|
1003
|
-
throw new
|
|
1051
|
+
throw new NetworkError(
|
|
1052
|
+
"teams",
|
|
1053
|
+
"Failed to create 1:1 conversation - no ID returned"
|
|
1054
|
+
);
|
|
1004
1055
|
}
|
|
1005
|
-
this.logger
|
|
1056
|
+
this.logger.debug("Teams: 1:1 conversation created", { conversationId });
|
|
1006
1057
|
return this.encodeThreadId({
|
|
1007
1058
|
conversationId,
|
|
1008
1059
|
serviceUrl
|
|
@@ -1033,7 +1084,7 @@ var TeamsAdapter = class {
|
|
|
1033
1084
|
}
|
|
1034
1085
|
}
|
|
1035
1086
|
try {
|
|
1036
|
-
this.logger
|
|
1087
|
+
this.logger.debug("Teams Graph API: fetching messages", {
|
|
1037
1088
|
conversationId: baseConversationId,
|
|
1038
1089
|
threadMessageId,
|
|
1039
1090
|
hasChannelContext: !!channelContext,
|
|
@@ -1086,12 +1137,12 @@ var TeamsAdapter = class {
|
|
|
1086
1137
|
graphMessages = graphMessages.filter((msg) => {
|
|
1087
1138
|
return msg.id && msg.id >= threadMessageId;
|
|
1088
1139
|
});
|
|
1089
|
-
this.logger
|
|
1140
|
+
this.logger.debug("Filtered group chat messages to thread", {
|
|
1090
1141
|
threadMessageId,
|
|
1091
1142
|
filteredCount: graphMessages.length
|
|
1092
1143
|
});
|
|
1093
1144
|
}
|
|
1094
|
-
this.logger
|
|
1145
|
+
this.logger.debug("Teams Graph API: fetched messages", {
|
|
1095
1146
|
count: graphMessages.length,
|
|
1096
1147
|
direction,
|
|
1097
1148
|
hasMoreMessages
|
|
@@ -1136,7 +1187,7 @@ var TeamsAdapter = class {
|
|
|
1136
1187
|
}
|
|
1137
1188
|
return { messages, nextCursor };
|
|
1138
1189
|
} catch (error) {
|
|
1139
|
-
this.logger
|
|
1190
|
+
this.logger.error("Teams Graph API: fetchMessages error", { error });
|
|
1140
1191
|
if (error instanceof Error && error.message?.includes("403")) {
|
|
1141
1192
|
throw new NotImplementedError(
|
|
1142
1193
|
"Teams fetchMessages requires one of these Azure AD app permissions: ChatMessage.Read.Chat, Chat.Read.All, or Chat.Read.WhereInstalled",
|
|
@@ -1156,7 +1207,7 @@ var TeamsAdapter = class {
|
|
|
1156
1207
|
const limit = options.limit || 50;
|
|
1157
1208
|
const cursor = options.cursor;
|
|
1158
1209
|
const direction = options.direction ?? "backward";
|
|
1159
|
-
this.logger
|
|
1210
|
+
this.logger.debug("Teams Graph API: fetching channel thread messages", {
|
|
1160
1211
|
teamId: context.teamId,
|
|
1161
1212
|
channelId: context.channelId,
|
|
1162
1213
|
threadMessageId,
|
|
@@ -1168,13 +1219,13 @@ var TeamsAdapter = class {
|
|
|
1168
1219
|
const repliesUrl = `${parentUrl}/replies`;
|
|
1169
1220
|
const graphClient = this.graphClient;
|
|
1170
1221
|
if (!graphClient) {
|
|
1171
|
-
throw new
|
|
1222
|
+
throw new AuthenticationError("teams", "Graph client not initialized");
|
|
1172
1223
|
}
|
|
1173
1224
|
let parentMessage = null;
|
|
1174
1225
|
try {
|
|
1175
1226
|
parentMessage = await graphClient.api(parentUrl).get();
|
|
1176
1227
|
} catch (err) {
|
|
1177
|
-
this.logger
|
|
1228
|
+
this.logger.warn("Failed to fetch parent message", {
|
|
1178
1229
|
threadMessageId,
|
|
1179
1230
|
err
|
|
1180
1231
|
});
|
|
@@ -1231,7 +1282,7 @@ var TeamsAdapter = class {
|
|
|
1231
1282
|
hasMoreMessages = allMessages.length > limit;
|
|
1232
1283
|
}
|
|
1233
1284
|
}
|
|
1234
|
-
this.logger
|
|
1285
|
+
this.logger.debug("Teams Graph API: fetched channel thread messages", {
|
|
1235
1286
|
count: graphMessages.length,
|
|
1236
1287
|
direction,
|
|
1237
1288
|
hasMoreMessages
|
|
@@ -1376,7 +1427,10 @@ var TeamsAdapter = class {
|
|
|
1376
1427
|
decodeThreadId(threadId) {
|
|
1377
1428
|
const parts = threadId.split(":");
|
|
1378
1429
|
if (parts.length !== 3 || parts[0] !== "teams") {
|
|
1379
|
-
throw new
|
|
1430
|
+
throw new ValidationError(
|
|
1431
|
+
"teams",
|
|
1432
|
+
`Invalid Teams thread ID: ${threadId}`
|
|
1433
|
+
);
|
|
1380
1434
|
}
|
|
1381
1435
|
const conversationId = Buffer.from(
|
|
1382
1436
|
parts[1],
|
|
@@ -1421,6 +1475,47 @@ var TeamsAdapter = class {
|
|
|
1421
1475
|
renderFormatted(content) {
|
|
1422
1476
|
return this.formatConverter.fromAst(content);
|
|
1423
1477
|
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Convert Teams/BotBuilder errors to standardized AdapterError types.
|
|
1480
|
+
*/
|
|
1481
|
+
handleTeamsError(error, operation) {
|
|
1482
|
+
if (error && typeof error === "object") {
|
|
1483
|
+
const err = error;
|
|
1484
|
+
const statusCode = err.statusCode || err.status || err.code;
|
|
1485
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
1486
|
+
throw new AuthenticationError(
|
|
1487
|
+
"teams",
|
|
1488
|
+
`Authentication failed for ${operation}: ${err.message || "unauthorized"}`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
if (statusCode === 404) {
|
|
1492
|
+
throw new NetworkError(
|
|
1493
|
+
"teams",
|
|
1494
|
+
`Resource not found during ${operation}: conversation or message may no longer exist`,
|
|
1495
|
+
error instanceof Error ? error : void 0
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
if (statusCode === 429) {
|
|
1499
|
+
const retryAfter = typeof err.retryAfter === "number" ? err.retryAfter : void 0;
|
|
1500
|
+
throw new AdapterRateLimitError("teams", retryAfter);
|
|
1501
|
+
}
|
|
1502
|
+
if (statusCode === 403 || err.message && typeof err.message === "string" && err.message.toLowerCase().includes("permission")) {
|
|
1503
|
+
throw new PermissionError("teams", operation);
|
|
1504
|
+
}
|
|
1505
|
+
if (err.message && typeof err.message === "string") {
|
|
1506
|
+
throw new NetworkError(
|
|
1507
|
+
"teams",
|
|
1508
|
+
`Teams API error during ${operation}: ${err.message}`,
|
|
1509
|
+
error instanceof Error ? error : void 0
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
throw new NetworkError(
|
|
1514
|
+
"teams",
|
|
1515
|
+
`Teams API error during ${operation}: ${String(error)}`,
|
|
1516
|
+
error instanceof Error ? error : void 0
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1424
1519
|
};
|
|
1425
1520
|
function createTeamsAdapter(config) {
|
|
1426
1521
|
return new TeamsAdapter(config);
|