@gakr-gakr/msteams 0.1.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.
Files changed (107) hide show
  1. package/api.ts +3 -0
  2. package/autobot.plugin.json +15 -0
  3. package/channel-config-api.ts +1 -0
  4. package/channel-plugin-api.ts +2 -0
  5. package/config-api.ts +4 -0
  6. package/contract-api.ts +4 -0
  7. package/index.ts +20 -0
  8. package/package.json +72 -0
  9. package/runtime-api.ts +66 -0
  10. package/secret-contract-api.ts +5 -0
  11. package/setup-entry.ts +13 -0
  12. package/setup-plugin-api.ts +3 -0
  13. package/src/ai-entity.ts +7 -0
  14. package/src/approval-auth.ts +44 -0
  15. package/src/attachments/bot-framework.ts +348 -0
  16. package/src/attachments/download.ts +328 -0
  17. package/src/attachments/graph.ts +489 -0
  18. package/src/attachments/html.ts +122 -0
  19. package/src/attachments/payload.ts +14 -0
  20. package/src/attachments/remote-media.ts +86 -0
  21. package/src/attachments/shared.ts +655 -0
  22. package/src/attachments/types.ts +47 -0
  23. package/src/attachments.ts +18 -0
  24. package/src/channel-api.ts +1 -0
  25. package/src/channel.runtime.ts +56 -0
  26. package/src/channel.setup.ts +77 -0
  27. package/src/channel.ts +1176 -0
  28. package/src/config-schema.ts +6 -0
  29. package/src/config-ui-hints.ts +40 -0
  30. package/src/conversation-store-fs.ts +149 -0
  31. package/src/conversation-store-helpers.ts +105 -0
  32. package/src/conversation-store-memory.ts +51 -0
  33. package/src/conversation-store.ts +71 -0
  34. package/src/directory-live.ts +111 -0
  35. package/src/doctor.ts +27 -0
  36. package/src/errors.ts +270 -0
  37. package/src/feedback-reflection-prompt.ts +117 -0
  38. package/src/feedback-reflection-store.ts +113 -0
  39. package/src/feedback-reflection.ts +271 -0
  40. package/src/file-consent-helpers.ts +115 -0
  41. package/src/file-consent-invoke.ts +150 -0
  42. package/src/file-consent.ts +223 -0
  43. package/src/graph-chat.ts +36 -0
  44. package/src/graph-group-management.ts +168 -0
  45. package/src/graph-members.ts +48 -0
  46. package/src/graph-messages.ts +534 -0
  47. package/src/graph-teams.ts +114 -0
  48. package/src/graph-thread.ts +146 -0
  49. package/src/graph-upload.ts +531 -0
  50. package/src/graph-users.ts +29 -0
  51. package/src/graph.ts +308 -0
  52. package/src/inbound.ts +148 -0
  53. package/src/index.ts +4 -0
  54. package/src/media-helpers.ts +105 -0
  55. package/src/mentions.ts +114 -0
  56. package/src/messenger.ts +608 -0
  57. package/src/monitor-handler/access.ts +136 -0
  58. package/src/monitor-handler/inbound-media.ts +180 -0
  59. package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
  60. package/src/monitor-handler/message-handler.test-support.ts +102 -0
  61. package/src/monitor-handler/message-handler.ts +1015 -0
  62. package/src/monitor-handler/reaction-handler.ts +124 -0
  63. package/src/monitor-handler/thread-session.ts +30 -0
  64. package/src/monitor-handler.ts +538 -0
  65. package/src/monitor-handler.types.ts +27 -0
  66. package/src/monitor-types.ts +6 -0
  67. package/src/monitor.ts +476 -0
  68. package/src/oauth.flow.ts +77 -0
  69. package/src/oauth.shared.ts +37 -0
  70. package/src/oauth.token.ts +162 -0
  71. package/src/oauth.ts +130 -0
  72. package/src/outbound.ts +198 -0
  73. package/src/pending-uploads-fs.ts +235 -0
  74. package/src/pending-uploads.ts +121 -0
  75. package/src/policy.ts +245 -0
  76. package/src/polls-store-memory.ts +32 -0
  77. package/src/polls.ts +312 -0
  78. package/src/presentation.ts +93 -0
  79. package/src/probe.ts +132 -0
  80. package/src/reply-dispatcher.ts +523 -0
  81. package/src/reply-stream-controller.ts +334 -0
  82. package/src/resolve-allowlist.ts +309 -0
  83. package/src/revoked-context.ts +17 -0
  84. package/src/runtime.ts +12 -0
  85. package/src/sdk-types.ts +59 -0
  86. package/src/sdk.ts +916 -0
  87. package/src/secret-contract.ts +49 -0
  88. package/src/secret-input.ts +7 -0
  89. package/src/send-context.ts +269 -0
  90. package/src/send.ts +697 -0
  91. package/src/sent-message-cache.ts +174 -0
  92. package/src/session-route.ts +40 -0
  93. package/src/setup-core.ts +162 -0
  94. package/src/setup-surface.ts +319 -0
  95. package/src/sso-token-store.ts +166 -0
  96. package/src/sso.ts +300 -0
  97. package/src/storage.ts +25 -0
  98. package/src/store-fs.ts +42 -0
  99. package/src/streaming-message.ts +327 -0
  100. package/src/thread-parent-context.ts +159 -0
  101. package/src/token-response.ts +11 -0
  102. package/src/token.ts +194 -0
  103. package/src/user-agent.ts +53 -0
  104. package/src/webhook-timeouts.ts +27 -0
  105. package/src/welcome-card.ts +57 -0
  106. package/test-api.ts +1 -0
  107. package/tsconfig.json +16 -0
@@ -0,0 +1,136 @@
1
+ import {
2
+ channelIngressRoutes,
3
+ resolveStableChannelMessageIngress,
4
+ type StableChannelIngressIdentityParams,
5
+ } from "autobot/plugin-sdk/channel-ingress-runtime";
6
+ import { normalizeOptionalLowercaseString } from "autobot/plugin-sdk/string-coerce-runtime";
7
+ import {
8
+ DEFAULT_ACCOUNT_ID,
9
+ createChannelPairingController,
10
+ isDangerousNameMatchingEnabled,
11
+ resolveDefaultGroupPolicy,
12
+ type AutoBotConfig,
13
+ } from "../../runtime-api.js";
14
+ import { normalizeMSTeamsConversationId } from "../inbound.js";
15
+ import { resolveMSTeamsRouteConfig } from "../policy.js";
16
+ import { getMSTeamsRuntime } from "../runtime.js";
17
+ import type { MSTeamsTurnContext } from "../sdk-types.js";
18
+
19
+ const MSTEAMS_SENDER_NAME_KIND = "plugin:msteams-sender-name" as const;
20
+ const msteamsIngressIdentity = {
21
+ key: "sender-id",
22
+ normalize: normalizeIngressValue,
23
+ aliases: [
24
+ {
25
+ key: "sender-name",
26
+ kind: MSTEAMS_SENDER_NAME_KIND,
27
+ normalizeEntry: normalizeIngressValue,
28
+ normalizeSubject: normalizeIngressValue,
29
+ dangerous: true,
30
+ },
31
+ ],
32
+ isWildcardEntry: (entry) => normalizeIngressValue(entry) === "*",
33
+ resolveEntryId: ({ entryIndex, fieldKey }) =>
34
+ `msteams-entry-${entryIndex + 1}:${fieldKey === "sender-name" ? "name" : "id"}`,
35
+ } satisfies StableChannelIngressIdentityParams;
36
+
37
+ function normalizeIngressValue(value?: string | null): string | null {
38
+ return normalizeOptionalLowercaseString(value) ?? null;
39
+ }
40
+
41
+ export async function resolveMSTeamsSenderAccess(params: {
42
+ cfg: AutoBotConfig;
43
+ activity: MSTeamsTurnContext["activity"];
44
+ hasControlCommand?: boolean;
45
+ }) {
46
+ const activity = params.activity;
47
+ const msteamsCfg = params.cfg.channels?.msteams;
48
+ const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "unknown");
49
+ const convType = normalizeOptionalLowercaseString(activity.conversation?.conversationType);
50
+ const isDirectMessage = convType === "personal" || (!convType && !activity.conversation?.isGroup);
51
+ const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
52
+ const senderName = activity.from?.name ?? activity.from?.id ?? senderId;
53
+
54
+ const core = getMSTeamsRuntime();
55
+ const pairing = createChannelPairingController({
56
+ core,
57
+ channel: "msteams",
58
+ accountId: DEFAULT_ACCOUNT_ID,
59
+ });
60
+ const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
61
+ const configuredDmAllowFrom = msteamsCfg?.allowFrom ?? [];
62
+ const groupAllowFrom = msteamsCfg?.groupAllowFrom;
63
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
64
+ const groupPolicy =
65
+ !isDirectMessage && msteamsCfg
66
+ ? (msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist")
67
+ : "disabled";
68
+ const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
69
+ const channelGate = resolveMSTeamsRouteConfig({
70
+ cfg: msteamsCfg,
71
+ teamId: activity.channelData?.team?.id,
72
+ teamName: activity.channelData?.team?.name,
73
+ conversationId,
74
+ channelName: activity.channelData?.channel?.name,
75
+ allowNameMatching,
76
+ });
77
+
78
+ const resolved = await resolveStableChannelMessageIngress({
79
+ channelId: "msteams",
80
+ accountId: pairing.accountId,
81
+ identity: msteamsIngressIdentity,
82
+ cfg: params.cfg,
83
+ readStoreAllowFrom: pairing.readAllowFromStore,
84
+ subject: {
85
+ stableId: senderId,
86
+ aliases: { "sender-name": senderName },
87
+ },
88
+ conversation: {
89
+ kind: isDirectMessage ? "direct" : convType === "channel" ? "channel" : "group",
90
+ id: conversationId,
91
+ parentId: activity.channelData?.team?.id,
92
+ },
93
+ route: channelIngressRoutes(
94
+ !isDirectMessage &&
95
+ channelGate.allowlistConfigured && {
96
+ id: "msteams:team-channel",
97
+ kind: "nestedAllowlist",
98
+ allowed: channelGate.allowed,
99
+ precedence: 0,
100
+ matchId: "msteams-route",
101
+ ...(channelGate.allowed && groupPolicy === "allowlist"
102
+ ? {
103
+ senderPolicy: "deny-when-empty" as const,
104
+ senderAllowFromSource: "effective-group" as const,
105
+ }
106
+ : {}),
107
+ },
108
+ ),
109
+ dmPolicy,
110
+ groupPolicy,
111
+ policy: {
112
+ groupAllowFromFallbackToAllowFrom: true,
113
+ mutableIdentifierMatching: allowNameMatching ? "enabled" : "disabled",
114
+ },
115
+ allowFrom: configuredDmAllowFrom,
116
+ groupAllowFrom,
117
+ command: {
118
+ allowTextCommands: true,
119
+ hasControlCommand: params.hasControlCommand === true,
120
+ directGroupAllowFrom: isDirectMessage ? "effective" : "none",
121
+ },
122
+ });
123
+ return {
124
+ ...resolved,
125
+ pairing,
126
+ isDirectMessage,
127
+ conversationId,
128
+ senderId,
129
+ senderName,
130
+ msteamsCfg,
131
+ dmPolicy,
132
+ channelGate,
133
+ allowNameMatching,
134
+ groupPolicy,
135
+ };
136
+ }
@@ -0,0 +1,180 @@
1
+ import {
2
+ buildMSTeamsGraphMessageUrls,
3
+ downloadMSTeamsAttachments,
4
+ downloadMSTeamsBotFrameworkAttachments,
5
+ downloadMSTeamsGraphMedia,
6
+ extractMSTeamsHtmlAttachmentIds,
7
+ isBotFrameworkPersonalChatId,
8
+ type MSTeamsAccessTokenProvider,
9
+ type MSTeamsAttachmentLike,
10
+ type MSTeamsHtmlAttachmentSummary,
11
+ type MSTeamsInboundMedia,
12
+ } from "../attachments.js";
13
+ import type { MSTeamsTurnContext } from "../sdk-types.js";
14
+
15
+ type MSTeamsLogger = {
16
+ debug?: (message: string, meta?: Record<string, unknown>) => void;
17
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
18
+ error?: (message: string, meta?: Record<string, unknown>) => void;
19
+ };
20
+
21
+ export async function resolveMSTeamsInboundMedia(params: {
22
+ attachments: MSTeamsAttachmentLike[];
23
+ htmlSummary?: MSTeamsHtmlAttachmentSummary;
24
+ maxBytes: number;
25
+ allowHosts?: string[];
26
+ authAllowHosts?: string[];
27
+ tokenProvider: MSTeamsAccessTokenProvider;
28
+ conversationType: string;
29
+ conversationId: string;
30
+ conversationMessageId?: string;
31
+ serviceUrl?: string;
32
+ activity: Pick<MSTeamsTurnContext["activity"], "id" | "replyToId" | "channelData">;
33
+ log: MSTeamsLogger;
34
+ /** When true, embeds original filename in stored path for later extraction. */
35
+ preserveFilenames?: boolean;
36
+ }): Promise<MSTeamsInboundMedia[]> {
37
+ const {
38
+ attachments,
39
+ htmlSummary,
40
+ maxBytes,
41
+ tokenProvider,
42
+ allowHosts,
43
+ conversationType,
44
+ conversationId,
45
+ conversationMessageId,
46
+ serviceUrl,
47
+ activity,
48
+ log,
49
+ preserveFilenames,
50
+ } = params;
51
+
52
+ let mediaList = await downloadMSTeamsAttachments({
53
+ attachments,
54
+ maxBytes,
55
+ tokenProvider,
56
+ allowHosts,
57
+ authAllowHosts: params.authAllowHosts,
58
+ preserveFilenames,
59
+ logger: log,
60
+ });
61
+
62
+ if (mediaList.length === 0) {
63
+ // Gate the Graph/Bot Framework media fallback on the presence of real
64
+ // `<attachment id="...">` tags inside any `text/html` attachment. Teams
65
+ // delivers @mention cards and other chrome as `text/html` attachments
66
+ // too, so keying off contentType alone produces spurious 404 diagnostics
67
+ // for every mention-only message and masks real file attachments (#58617).
68
+ const attachmentIds = extractMSTeamsHtmlAttachmentIds(attachments);
69
+ const hasHtmlFileAttachment = attachmentIds.length > 0;
70
+
71
+ // Personal DMs with the bot use Bot Framework conversation IDs (`a:...`
72
+ // or `8:orgid:...`) which Graph's `/chats/{id}` endpoint rejects with
73
+ // "Invalid ThreadId". Fetch media via the Bot Framework v3 attachments
74
+ // endpoint instead, which speaks the same identifier space.
75
+ if (hasHtmlFileAttachment && isBotFrameworkPersonalChatId(conversationId)) {
76
+ if (!serviceUrl) {
77
+ log.debug?.("bot framework attachment skipped (missing serviceUrl)", {
78
+ conversationType,
79
+ conversationId,
80
+ });
81
+ } else {
82
+ const bfMedia = await downloadMSTeamsBotFrameworkAttachments({
83
+ serviceUrl,
84
+ attachmentIds,
85
+ tokenProvider,
86
+ maxBytes,
87
+ allowHosts,
88
+ authAllowHosts: params.authAllowHosts,
89
+ preserveFilenames,
90
+ });
91
+ if (bfMedia.media.length > 0) {
92
+ mediaList = bfMedia.media;
93
+ } else {
94
+ log.debug?.("bot framework attachments fetch empty", {
95
+ conversationType,
96
+ attachmentCount: bfMedia.attachmentCount ?? attachmentIds.length,
97
+ });
98
+ }
99
+ }
100
+ }
101
+
102
+ if (
103
+ hasHtmlFileAttachment &&
104
+ mediaList.length === 0 &&
105
+ !isBotFrameworkPersonalChatId(conversationId)
106
+ ) {
107
+ const messageUrls = buildMSTeamsGraphMessageUrls({
108
+ conversationType,
109
+ conversationId,
110
+ messageId: activity.id ?? undefined,
111
+ replyToId: activity.replyToId ?? undefined,
112
+ conversationMessageId,
113
+ channelData: activity.channelData,
114
+ });
115
+ if (messageUrls.length === 0) {
116
+ log.debug?.("graph message url unavailable", {
117
+ conversationType,
118
+ hasChannelData: Boolean(activity.channelData),
119
+ messageId: activity.id ?? undefined,
120
+ replyToId: activity.replyToId ?? undefined,
121
+ });
122
+ } else {
123
+ const attempts: Array<{
124
+ url: string;
125
+ hostedStatus?: number;
126
+ attachmentStatus?: number;
127
+ hostedCount?: number;
128
+ attachmentCount?: number;
129
+ tokenError?: boolean;
130
+ }> = [];
131
+ for (const messageUrl of messageUrls) {
132
+ const graphMedia = await downloadMSTeamsGraphMedia({
133
+ messageUrl,
134
+ tokenProvider,
135
+ maxBytes,
136
+ allowHosts,
137
+ authAllowHosts: params.authAllowHosts,
138
+ preserveFilenames,
139
+ log,
140
+ logger: log,
141
+ });
142
+ attempts.push({
143
+ url: messageUrl,
144
+ hostedStatus: graphMedia.hostedStatus,
145
+ attachmentStatus: graphMedia.attachmentStatus,
146
+ hostedCount: graphMedia.hostedCount,
147
+ attachmentCount: graphMedia.attachmentCount,
148
+ tokenError: graphMedia.tokenError,
149
+ });
150
+ if (graphMedia.media.length > 0) {
151
+ mediaList = graphMedia.media;
152
+ break;
153
+ }
154
+ if (graphMedia.tokenError) {
155
+ break;
156
+ }
157
+ }
158
+ if (mediaList.length === 0) {
159
+ log.debug?.("graph media fetch empty", {
160
+ attempts,
161
+ attachmentIdCount: attachmentIds.length,
162
+ });
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ if (mediaList.length > 0) {
169
+ log.debug?.("downloaded attachments", { count: mediaList.length });
170
+ } else if (htmlSummary?.imgTags) {
171
+ log.debug?.("inline images detected but none downloaded", {
172
+ imgTags: htmlSummary.imgTags,
173
+ srcHosts: htmlSummary.srcHosts,
174
+ dataImages: htmlSummary.dataImages,
175
+ cidImages: htmlSummary.cidImages,
176
+ });
177
+ }
178
+
179
+ return mediaList;
180
+ }
@@ -0,0 +1,28 @@
1
+ import { vi } from "vitest";
2
+
3
+ const runtimeApiMockState = vi.hoisted(() => ({
4
+ dispatchReplyFromConfigWithSettledDispatcher: vi.fn(async (params: { ctxPayload: unknown }) => ({
5
+ queuedFinal: false,
6
+ counts: {},
7
+ capturedCtxPayload: params.ctxPayload,
8
+ })),
9
+ }));
10
+
11
+ export function getRuntimeApiMockState() {
12
+ return runtimeApiMockState;
13
+ }
14
+
15
+ vi.mock("autobot/plugin-sdk/inbound-reply-dispatch", () => {
16
+ return {
17
+ dispatchReplyFromConfigWithSettledDispatcher:
18
+ runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher,
19
+ };
20
+ });
21
+
22
+ vi.mock("../reply-dispatcher.js", () => ({
23
+ createMSTeamsReplyDispatcher: () => ({
24
+ dispatcher: {},
25
+ replyOptions: {},
26
+ markDispatchIdle: vi.fn(),
27
+ }),
28
+ }));
@@ -0,0 +1,102 @@
1
+ import { vi } from "vitest";
2
+ import type { AutoBotConfig, PluginRuntime, RuntimeEnv } from "../../runtime-api.js";
3
+ import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js";
4
+ import { installMSTeamsTestRuntime } from "../monitor-handler.test-helpers.js";
5
+
6
+ export const channelConversationId = "19:general@thread.tacv2";
7
+
8
+ type MessageHandlerDepsOptions = {
9
+ enqueueSystemEvent?: ReturnType<typeof vi.fn>;
10
+ readAllowFromStore?: ReturnType<typeof vi.fn>;
11
+ upsertPairingRequest?: ReturnType<typeof vi.fn>;
12
+ recordInboundSession?: ReturnType<typeof vi.fn>;
13
+ resolveAgentRoute?: (params: { peer: { kind: string; id: string } }) => unknown;
14
+ hasControlCommand?: PluginRuntime["channel"]["text"]["hasControlCommand"];
15
+ };
16
+
17
+ export function createMessageHandlerDeps(
18
+ cfg: AutoBotConfig,
19
+ options: MessageHandlerDepsOptions = {},
20
+ ) {
21
+ const enqueueSystemEvent = options.enqueueSystemEvent ?? vi.fn();
22
+ const readAllowFromStore = options.readAllowFromStore ?? vi.fn(async () => []);
23
+ const upsertPairingRequest = options.upsertPairingRequest ?? vi.fn(async () => null);
24
+ const recordInboundSession =
25
+ options.recordInboundSession ?? vi.fn(async (_params: { sessionKey: string }) => undefined);
26
+ const resolveAgentRoute =
27
+ options.resolveAgentRoute ??
28
+ vi.fn(({ peer }: { peer: { kind: string; id: string } }) => ({
29
+ sessionKey: `agent:main:msteams:${peer.kind}:${peer.id}`,
30
+ agentId: "main",
31
+ accountId: "default",
32
+ mainSessionKey: "agent:main:main",
33
+ lastRoutePolicy: "session" as const,
34
+ matchedBy: "default" as const,
35
+ }));
36
+
37
+ installMSTeamsTestRuntime({
38
+ enqueueSystemEvent,
39
+ readAllowFromStore,
40
+ upsertPairingRequest,
41
+ recordInboundSession,
42
+ resolveAgentRoute,
43
+ hasControlCommand: options.hasControlCommand,
44
+ resolveTextChunkLimit: () => 4000,
45
+ resolveStorePath: () => "/tmp/test-store",
46
+ });
47
+
48
+ const conversationStore = {
49
+ get: vi.fn(async () => null),
50
+ upsert: vi.fn(async () => undefined),
51
+ list: vi.fn(async () => []),
52
+ remove: vi.fn(async () => false),
53
+ findPreferredDmByUserId: vi.fn(async () => null),
54
+ findByUserId: vi.fn(async () => null),
55
+ } satisfies MSTeamsMessageHandlerDeps["conversationStore"];
56
+
57
+ const deps: MSTeamsMessageHandlerDeps = {
58
+ cfg,
59
+ runtime: { error: vi.fn() } as unknown as RuntimeEnv,
60
+ appId: "test-app",
61
+ adapter: {} as MSTeamsMessageHandlerDeps["adapter"],
62
+ tokenProvider: {
63
+ getAccessToken: vi.fn(async () => "token"),
64
+ },
65
+ textLimit: 4000,
66
+ mediaMaxBytes: 1024 * 1024,
67
+ conversationStore,
68
+ pollStore: {
69
+ recordVote: vi.fn(async () => null),
70
+ } as unknown as MSTeamsMessageHandlerDeps["pollStore"],
71
+ log: {
72
+ info: vi.fn(),
73
+ debug: vi.fn(),
74
+ error: vi.fn(),
75
+ } as unknown as MSTeamsMessageHandlerDeps["log"],
76
+ };
77
+
78
+ return {
79
+ conversationStore,
80
+ deps,
81
+ enqueueSystemEvent,
82
+ readAllowFromStore,
83
+ upsertPairingRequest,
84
+ recordInboundSession,
85
+ resolveAgentRoute,
86
+ };
87
+ }
88
+
89
+ export function buildChannelActivity(overrides: Record<string, unknown> = {}) {
90
+ return {
91
+ id: "msg-1",
92
+ type: "message",
93
+ text: "hello",
94
+ from: { id: "user-id", aadObjectId: "user-aad", name: "Test User" },
95
+ recipient: { id: "bot-id", name: "Bot" },
96
+ conversation: { id: channelConversationId, conversationType: "channel" },
97
+ channelData: { team: { id: "team-1" } },
98
+ attachments: [],
99
+ entities: [{ type: "mention", mentioned: { id: "bot-id" } }],
100
+ ...overrides,
101
+ };
102
+ }