chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,93 @@
import type {
MicrosoftTeamsDeleteMessageParams,
MicrosoftTeamsDeleteResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const deleteChannelMessageTool: ToolConfig<
MicrosoftTeamsDeleteMessageParams,
MicrosoftTeamsDeleteResponse
> = {
id: 'microsoft_teams_delete_channel_message',
name: 'Delete Microsoft Teams Channel Message',
description: 'Soft delete a message in a Microsoft Teams channel',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the team (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings or channel info)',
},
channelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the channel containing the message (e.g., "19:abc123def456@thread.tacv2" - from channel listings)',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the message to delete (e.g., "1234567890123" - a numeric string from message responses)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the deletion was successful' },
deleted: { type: 'boolean', description: 'Confirmation of deletion' },
messageId: { type: 'string', description: 'ID of the deleted message' },
},
request: {
url: (params) => {
const teamId = params.teamId?.trim()
const channelId = params.channelId?.trim()
const messageId = params.messageId?.trim()
if (!teamId || !channelId || !messageId) {
throw new Error('Team ID, Channel ID, and Message ID are required')
}
return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/softDelete`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (_response: Response, params?: MicrosoftTeamsDeleteMessageParams) => {
// Soft delete returns 204 No Content on success
return {
success: true,
output: {
deleted: true,
messageId: params?.messageId || '',
metadata: {
messageId: params?.messageId || '',
teamId: params?.teamId || '',
channelId: params?.channelId || '',
},
},
}
},
}
@@ -0,0 +1,91 @@
import type {
MicrosoftTeamsDeleteMessageParams,
MicrosoftTeamsDeleteResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const deleteChatMessageTool: ToolConfig<
MicrosoftTeamsDeleteMessageParams,
MicrosoftTeamsDeleteResponse
> = {
id: 'microsoft_teams_delete_chat_message',
name: 'Delete Microsoft Teams Chat Message',
description: 'Soft delete a message in a Microsoft Teams chat',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
chatId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the chat containing the message (e.g., "19:abc123def456@thread.v2" - from chat listings)',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the message to delete (e.g., "1234567890123" - a numeric string from message responses)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the deletion was successful' },
deleted: { type: 'boolean', description: 'Confirmation of deletion' },
messageId: { type: 'string', description: 'ID of the deleted message' },
},
request: {
url: (params) => {
const chatId = params.chatId?.trim()
const messageId = params.messageId?.trim()
if (!chatId || !messageId) {
throw new Error('Chat ID and Message ID are required')
}
return '/api/tools/microsoft_teams/delete_chat_message'
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
return {
accessToken: params.accessToken,
chatId: params.chatId,
messageId: params.messageId,
}
},
},
transformResponse: async (_response: Response, params?: MicrosoftTeamsDeleteMessageParams) => {
// Soft delete returns 204 No Content on success
return {
success: true,
output: {
deleted: true,
messageId: params?.messageId || '',
metadata: {
messageId: params?.messageId || '',
chatId: params?.chatId || '',
},
},
}
},
}
@@ -0,0 +1,139 @@
import type {
MicrosoftTeamsGetMessageParams,
MicrosoftTeamsReadResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const getMessageTool: ToolConfig<
MicrosoftTeamsGetMessageParams,
MicrosoftTeamsReadResponse
> = {
id: 'microsoft_teams_get_message',
name: 'Get Microsoft Teams Message',
description: 'Get a specific message from a Microsoft Teams chat or channel',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the team for channel messages (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID)',
},
channelId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the channel for channel messages (e.g., "19:abc123def456@thread.tacv2")',
},
chatId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The ID of the chat for chat messages (e.g., "19:abc123def456@thread.v2")',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the message to retrieve (e.g., "1234567890123" - a numeric string from message responses)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the retrieval was successful' },
content: { type: 'string', description: 'The message content' },
metadata: {
type: 'object',
description: 'Message metadata including sender, timestamp, etc.',
properties: {
messageId: { type: 'string', description: 'Message ID' },
content: { type: 'string', description: 'Message content' },
createdTime: { type: 'string', description: 'Message creation timestamp' },
url: { type: 'string', description: 'Web URL to the message' },
teamId: { type: 'string', description: 'Team ID' },
channelId: { type: 'string', description: 'Channel ID' },
chatId: { type: 'string', description: 'Chat ID' },
messages: { type: 'array', description: 'Array of message details' },
messageCount: { type: 'number', description: 'Number of messages' },
},
},
},
request: {
url: (params) => {
const messageId = params.messageId?.trim()
if (!messageId) {
throw new Error('Message ID is required')
}
// Check if it's a channel or chat message
if (params.teamId && params.channelId) {
const teamId = params.teamId.trim()
const channelId = params.channelId.trim()
return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}`
}
if (params.chatId) {
const chatId = params.chatId.trim()
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}`
}
throw new Error('Either (teamId and channelId) or chatId is required')
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsGetMessageParams) => {
const data = await response.json()
const metadata = {
messageId: data.id || params?.messageId || '',
content: data.body?.content || '',
createdTime: data.createdDateTime || '',
url: data.webUrl || '',
teamId: params?.teamId,
channelId: params?.channelId,
chatId: params?.chatId,
messages: [
{
id: data.id || '',
content: data.body?.content || '',
sender: data.from?.user?.displayName || 'Unknown',
timestamp: data.createdDateTime || '',
messageType: data.messageType || 'message',
attachments: data.attachments || [],
},
],
messageCount: 1,
}
return {
success: true,
output: {
content: data.body?.content || '',
metadata,
},
}
},
}
+37
View File
@@ -0,0 +1,37 @@
import { deleteChannelMessageTool } from '@/tools/microsoft_teams/delete_channel_message'
import { deleteChatMessageTool } from '@/tools/microsoft_teams/delete_chat_message'
import { getMessageTool } from '@/tools/microsoft_teams/get_message'
import { listChannelMembersTool } from '@/tools/microsoft_teams/list_channel_members'
import { listChannelsTool } from '@/tools/microsoft_teams/list_channels'
import { listChatMembersTool } from '@/tools/microsoft_teams/list_chat_members'
import { listChatsTool } from '@/tools/microsoft_teams/list_chats'
import { listTeamMembersTool } from '@/tools/microsoft_teams/list_team_members'
import { listTeamsTool } from '@/tools/microsoft_teams/list_teams'
import { readChannelTool } from '@/tools/microsoft_teams/read_channel'
import { readChatTool } from '@/tools/microsoft_teams/read_chat'
import { replyToMessageTool } from '@/tools/microsoft_teams/reply_to_message'
import { setReactionTool } from '@/tools/microsoft_teams/set_reaction'
import { unsetReactionTool } from '@/tools/microsoft_teams/unset_reaction'
import { updateChannelMessageTool } from '@/tools/microsoft_teams/update_channel_message'
import { updateChatMessageTool } from '@/tools/microsoft_teams/update_chat_message'
import { writeChannelTool } from '@/tools/microsoft_teams/write_channel'
import { writeChatTool } from '@/tools/microsoft_teams/write_chat'
export const microsoftTeamsReadChannelTool = readChannelTool
export const microsoftTeamsReadChatTool = readChatTool
export const microsoftTeamsGetMessageTool = getMessageTool
export const microsoftTeamsWriteChannelTool = writeChannelTool
export const microsoftTeamsWriteChatTool = writeChatTool
export const microsoftTeamsUpdateChatMessageTool = updateChatMessageTool
export const microsoftTeamsUpdateChannelMessageTool = updateChannelMessageTool
export const microsoftTeamsDeleteChatMessageTool = deleteChatMessageTool
export const microsoftTeamsDeleteChannelMessageTool = deleteChannelMessageTool
export const microsoftTeamsReplyToMessageTool = replyToMessageTool
export const microsoftTeamsSetReactionTool = setReactionTool
export const microsoftTeamsUnsetReactionTool = unsetReactionTool
export const microsoftTeamsListTeamMembersTool = listTeamMembersTool
export const microsoftTeamsListChannelMembersTool = listChannelMembersTool
export const microsoftTeamsListChatMembersTool = listChatMembersTool
export const microsoftTeamsListTeamsTool = listTeamsTool
export const microsoftTeamsListChatsTool = listChatsTool
export const microsoftTeamsListChannelsTool = listChannelsTool
@@ -0,0 +1,92 @@
import type {
MicrosoftTeamsListMembersResponse,
MicrosoftTeamsToolParams,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const listChannelMembersTool: ToolConfig<
MicrosoftTeamsToolParams,
MicrosoftTeamsListMembersResponse
> = {
id: 'microsoft_teams_list_channel_members',
name: 'List Microsoft Teams Channel Members',
description: 'List all members of a Microsoft Teams channel',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the team (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings)',
},
channelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the channel (e.g., "19:abc123def456@thread.tacv2" - from channel listings)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the listing was successful' },
members: { type: 'array', description: 'Array of channel members' },
memberCount: { type: 'number', description: 'Total number of members' },
},
request: {
url: (params) => {
const teamId = params.teamId?.trim()
const channelId = params.channelId?.trim()
if (!teamId || !channelId) {
throw new Error('Team ID and Channel ID are required')
}
return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/members`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => {
const data = await response.json()
const members = (data.value || []).map((member: any) => ({
id: member.id || '',
displayName: member.displayName || '',
email: member.email || '',
userId: member.userId || '',
roles: member.roles || [],
}))
return {
success: true,
output: {
members,
memberCount: members.length,
metadata: {
teamId: params?.teamId || '',
channelId: params?.channelId || '',
},
},
}
},
}
@@ -0,0 +1,88 @@
import type {
MicrosoftTeamsListChannelsResponse,
MicrosoftTeamsToolParams,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const listChannelsTool: ToolConfig<
MicrosoftTeamsToolParams,
MicrosoftTeamsListChannelsResponse
> = {
id: 'microsoft_teams_list_channels',
name: 'List Microsoft Teams Channels',
description: 'List all channels in a Microsoft Teams team',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the team (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the listing was successful' },
channels: { type: 'array', description: 'Array of channels in the team' },
channelCount: { type: 'number', description: 'Total number of channels' },
hasMore: {
type: 'boolean',
description: 'Whether Graph indicated additional pages beyond this response',
},
},
request: {
url: (params) => {
const teamId = params.teamId?.trim()
if (!teamId) {
throw new Error('Team ID is required')
}
return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => {
const data = await response.json()
const channels = (data.value || []).map((channel: any) => ({
id: channel.id || '',
displayName: channel.displayName || '',
description: channel.description ?? '',
membershipType: channel.membershipType || 'standard',
webUrl: channel.webUrl || '',
}))
return {
success: true,
output: {
channels,
channelCount: channels.length,
hasMore: Boolean(data['@odata.nextLink']),
metadata: {
teamId: params?.teamId || '',
},
},
}
},
}
@@ -0,0 +1,87 @@
import type {
MicrosoftTeamsListMembersResponse,
MicrosoftTeamsToolParams,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const listChatMembersTool: ToolConfig<
MicrosoftTeamsToolParams,
MicrosoftTeamsListMembersResponse
> = {
id: 'microsoft_teams_list_chat_members',
name: 'List Microsoft Teams Chat Members',
description: 'List all members of a Microsoft Teams chat',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
chatId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the chat (e.g., "19:abc123def456@thread.v2" - from chat listings)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the listing was successful' },
members: { type: 'array', description: 'Array of chat members' },
memberCount: { type: 'number', description: 'Total number of members' },
hasMore: {
type: 'boolean',
description: 'Whether Graph indicated additional pages beyond this response',
},
},
request: {
url: (params) => {
const chatId = params.chatId?.trim()
if (!chatId) {
throw new Error('Chat ID is required')
}
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => {
const data = await response.json()
const members = (data.value || []).map((member: any) => ({
id: member.id || '',
displayName: member.displayName || '',
email: member.email || '',
userId: member.userId || '',
roles: member.roles || [],
}))
return {
success: true,
output: {
members,
memberCount: members.length,
hasMore: Boolean(data['@odata.nextLink']),
metadata: {
chatId: params?.chatId || '',
},
},
}
},
}
@@ -0,0 +1,72 @@
import type {
MicrosoftTeamsListChatsResponse,
MicrosoftTeamsToolParams,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const listChatsTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsListChatsResponse> =
{
id: 'microsoft_teams_list_chats',
name: 'List Microsoft Teams Chats',
description: 'List the Microsoft Teams chats the current user is part of',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the listing was successful' },
chats: { type: 'array', description: 'Array of chats the user is part of' },
chatCount: { type: 'number', description: 'Total number of chats' },
hasMore: {
type: 'boolean',
description: 'Whether Graph indicated additional pages beyond this response',
},
},
request: {
// $top=50 is the maximum page size Graph allows for this endpoint.
url: () => 'https://graph.microsoft.com/v1.0/me/chats?$top=50',
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
const chats = (data.value || []).map((chat: any) => ({
id: chat.id || '',
topic: chat.topic ?? null,
chatType: chat.chatType || '',
webUrl: chat.webUrl || '',
createdDateTime: chat.createdDateTime || '',
lastUpdatedDateTime: chat.lastUpdatedDateTime || '',
}))
return {
success: true,
output: {
chats,
chatCount: chats.length,
hasMore: Boolean(data['@odata.nextLink']),
},
}
},
}
@@ -0,0 +1,83 @@
import type {
MicrosoftTeamsListMembersResponse,
MicrosoftTeamsToolParams,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const listTeamMembersTool: ToolConfig<
MicrosoftTeamsToolParams,
MicrosoftTeamsListMembersResponse
> = {
id: 'microsoft_teams_list_team_members',
name: 'List Microsoft Teams Team Members',
description: 'List all members of a Microsoft Teams team',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the team (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the listing was successful' },
members: { type: 'array', description: 'Array of team members' },
memberCount: { type: 'number', description: 'Total number of members' },
},
request: {
url: (params) => {
const teamId = params.teamId?.trim()
if (!teamId) {
throw new Error('Team ID is required')
}
return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/members`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => {
const data = await response.json()
const members = (data.value || []).map((member: any) => ({
id: member.id || '',
displayName: member.displayName || '',
email: member.email || '',
userId: member.userId || '',
roles: member.roles || [],
}))
return {
success: true,
output: {
members,
memberCount: members.length,
metadata: {
teamId: params?.teamId || '',
},
},
}
},
}
@@ -0,0 +1,71 @@
import type {
MicrosoftTeamsListTeamsResponse,
MicrosoftTeamsToolParams,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const listTeamsTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsListTeamsResponse> =
{
id: 'microsoft_teams_list_teams',
name: 'List Microsoft Teams',
description: 'List the Microsoft Teams the current user is a direct member of',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the listing was successful' },
teams: { type: 'array', description: 'Array of teams the user is a member of' },
teamCount: { type: 'number', description: 'Total number of teams' },
hasMore: {
type: 'boolean',
description: 'Whether Graph indicated additional pages beyond this response',
},
},
request: {
// Note: GET /me/joinedTeams does not support OData query parameters ($top, etc.) per Graph docs:
// https://learn.microsoft.com/en-us/graph/api/user-list-joinedteams
url: () => 'https://graph.microsoft.com/v1.0/me/joinedTeams',
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
const teams = (data.value || []).map((team: any) => ({
id: team.id || '',
displayName: team.displayName || '',
description: team.description || '',
isArchived: Boolean(team.isArchived),
}))
return {
success: true,
output: {
teams,
teamCount: teams.length,
hasMore: Boolean(data['@odata.nextLink']),
},
}
},
}
@@ -0,0 +1,237 @@
import { createLogger } from '@sim/logger'
import type {
MicrosoftTeamsReadResponse,
MicrosoftTeamsToolParams,
} from '@/tools/microsoft_teams/types'
import {
downloadAllReferenceAttachments,
extractMessageAttachments,
fetchHostedContentsForChannelMessage,
} from '@/tools/microsoft_teams/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('MicrosoftTeamsReadChannel')
export const readChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsReadResponse> = {
id: 'microsoft_teams_read_channel',
name: 'Read Microsoft Teams Channel',
description: 'Read content from a Microsoft Teams channel',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the team to read from (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings)',
},
channelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the channel to read from (e.g., "19:abc123def456@thread.tacv2" - from channel listings)',
},
includeAttachments: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Download and include message attachments (hosted contents) into storage',
},
},
request: {
url: (params) => {
const teamId = params.teamId?.trim()
if (!teamId) {
throw new Error('Team ID is required')
}
const channelId = params.channelId?.trim()
if (!channelId) {
throw new Error('Channel ID is required')
}
const encodedTeamId = encodeURIComponent(teamId)
const encodedChannelId = encodeURIComponent(channelId)
// Graph API's default page size for channel messages is 20; request the max of 50
// so a single call surfaces more history without requiring pagination.
const url = `https://graph.microsoft.com/v1.0/teams/${encodedTeamId}/channels/${encodedChannelId}/messages?$top=50`
return url
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => {
const data = await response.json()
const messages = data.value || []
if (messages.length === 0) {
return {
success: true,
output: {
content: 'No messages found in this channel.',
metadata: {
teamId: '',
channelId: '',
messageCount: 0,
messages: [],
totalAttachments: 0,
attachmentTypes: [],
},
},
}
}
const processedMessages = await Promise.all(
messages.map(async (message: any, index: number) => {
try {
const content = message.body?.content || 'No content'
const messageId = message.id
const attachments = extractMessageAttachments(message)
let sender = 'Unknown'
if (message.from?.user?.displayName) {
sender = message.from.user.displayName
} else if (message.messageType === 'systemEventMessage') {
sender = 'System'
}
let uploaded: any[] = []
if (
params?.includeAttachments &&
params.accessToken &&
params.teamId &&
params.channelId &&
messageId
) {
try {
const hostedContents = await fetchHostedContentsForChannelMessage({
accessToken: params.accessToken,
teamId: params.teamId,
channelId: params.channelId,
messageId,
})
uploaded.push(...hostedContents)
const referenceFiles = await downloadAllReferenceAttachments({
accessToken: params.accessToken,
attachments,
})
uploaded.push(...referenceFiles)
} catch (_e) {
uploaded = []
}
}
return {
id: messageId,
content: content,
sender,
timestamp: message.createdDateTime,
messageType: message.messageType || 'message',
attachments,
uploadedFiles: uploaded,
}
} catch (error) {
logger.error(`Error processing message at index ${index}:`, error)
return {
id: message.id || `unknown-${index}`,
content: 'Error processing message',
sender: 'Unknown',
timestamp: message.createdDateTime || new Date().toISOString(),
messageType: 'error',
attachments: [],
}
}
})
)
const formattedMessages = processedMessages
.map((message: any) => {
const sender = message.sender
const timestamp = message.timestamp
? new Date(message.timestamp).toLocaleString()
: 'Unknown time'
return `[${timestamp}] ${sender}: ${message.content}`
})
.join('\n\n')
const allAttachments = processedMessages.flatMap((msg: any) => msg.attachments || [])
const attachmentTypes: string[] = []
const seenTypes = new Set<string>()
allAttachments.forEach((att: any) => {
if (
att.contentType &&
typeof att.contentType === 'string' &&
!seenTypes.has(att.contentType)
) {
attachmentTypes.push(att.contentType)
seenTypes.add(att.contentType)
}
})
const metadata = {
teamId: messages[0]?.channelIdentity?.teamId || params?.teamId || '',
channelId: messages[0]?.channelIdentity?.channelId || params?.channelId || '',
messageCount: messages.length,
totalAttachments: allAttachments.length,
attachmentTypes,
messages: processedMessages,
}
const flattenedUploads = processedMessages.flatMap((m: any) => m.uploadedFiles || [])
return {
success: true,
output: {
content: formattedMessages,
metadata,
attachments: flattenedUploads,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Teams channel read operation success status' },
messageCount: { type: 'number', description: 'Number of messages retrieved from channel' },
teamId: { type: 'string', description: 'ID of the team that was read from' },
channelId: { type: 'string', description: 'ID of the channel that was read from' },
messages: { type: 'array', description: 'Array of channel message objects' },
attachmentCount: { type: 'number', description: 'Total number of attachments found' },
attachmentTypes: { type: 'array', description: 'Types of attachments found' },
content: { type: 'string', description: 'Formatted content of channel messages' },
attachments: {
type: 'file[]',
description: 'Uploaded attachments for convenience (flattened)',
},
},
}
+189
View File
@@ -0,0 +1,189 @@
import type {
MicrosoftTeamsReadResponse,
MicrosoftTeamsToolParams,
} from '@/tools/microsoft_teams/types'
import {
downloadAllReferenceAttachments,
extractMessageAttachments,
fetchHostedContentsForChatMessage,
} from '@/tools/microsoft_teams/utils'
import type { ToolConfig } from '@/tools/types'
export const readChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsReadResponse> = {
id: 'microsoft_teams_read_chat',
name: 'Read Microsoft Teams Chat',
description: 'Read content from a Microsoft Teams chat',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
chatId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the chat to read from (e.g., "19:abc123def456@thread.v2" - from chat listings)',
},
includeAttachments: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Download and include message attachments (hosted contents) into storage',
},
},
request: {
url: (params) => {
const chatId = params.chatId?.trim()
if (!chatId) {
throw new Error('Chat ID is required')
}
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages?$top=50&$orderby=createdDateTime desc`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => {
const data = await response.json()
const messages = data.value || []
if (messages.length === 0) {
return {
success: true,
output: {
content: 'No messages found in this chat.',
metadata: {
chatId: '',
messageCount: 0,
messages: [],
totalAttachments: 0,
attachmentTypes: [],
},
},
}
}
const processedMessages = await Promise.all(
messages.map(async (message: any) => {
const content = message.body?.content || 'No content'
const messageId = message.id
const attachments = extractMessageAttachments(message)
let uploaded: any[] = []
if (params?.includeAttachments && params.accessToken && params.chatId && messageId) {
try {
const hostedContents = await fetchHostedContentsForChatMessage({
accessToken: params.accessToken,
chatId: params.chatId,
messageId,
})
uploaded.push(...hostedContents)
const referenceFiles = await downloadAllReferenceAttachments({
accessToken: params.accessToken,
attachments,
})
uploaded.push(...referenceFiles)
} catch (_e) {
uploaded = []
}
}
return {
id: messageId,
content: content, // Keep original content without modification
sender: message.from?.user?.displayName || 'Unknown',
timestamp: message.createdDateTime,
messageType: message.messageType || 'message',
attachments, // Raw attachment metadata
uploadedFiles: uploaded, // Uploaded file infos (paths/keys)
}
})
)
// Format the messages into a readable text (no attachment info in content)
const formattedMessages = processedMessages
.map((message: any) => {
const sender = message.sender
const timestamp = message.timestamp
? new Date(message.timestamp).toLocaleString()
: 'Unknown time'
return `[${timestamp}] ${sender}: ${message.content}`
})
.join('\n\n')
// Calculate attachment statistics
const allAttachments = processedMessages.flatMap((msg: any) => msg.attachments || [])
const attachmentTypes: string[] = []
const seenTypes = new Set<string>()
allAttachments.forEach((att: any) => {
if (
att.contentType &&
typeof att.contentType === 'string' &&
!seenTypes.has(att.contentType)
) {
attachmentTypes.push(att.contentType)
seenTypes.add(att.contentType)
}
})
// Create document metadata
const metadata = {
chatId: messages[0]?.chatId || params?.chatId || '',
messageCount: messages.length,
totalAttachments: allAttachments.length,
attachmentTypes,
messages: processedMessages,
}
// Flatten uploaded files across all messages for convenience
const flattenedUploads = processedMessages.flatMap((m: any) => m.uploadedFiles || [])
return {
success: true,
output: {
content: formattedMessages,
metadata,
attachments: flattenedUploads,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Teams chat read operation success status' },
messageCount: { type: 'number', description: 'Number of messages retrieved from chat' },
chatId: { type: 'string', description: 'ID of the chat that was read from' },
messages: { type: 'array', description: 'Array of chat message objects' },
attachmentCount: { type: 'number', description: 'Total number of attachments found' },
attachmentTypes: { type: 'array', description: 'Types of attachments found' },
content: { type: 'string', description: 'Formatted content of chat messages' },
attachments: {
type: 'file[]',
description: 'Uploaded attachments for convenience (flattened)',
},
},
}
@@ -0,0 +1,115 @@
import type {
MicrosoftTeamsReplyParams,
MicrosoftTeamsWriteResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const replyToMessageTool: ToolConfig<
MicrosoftTeamsReplyParams,
MicrosoftTeamsWriteResponse
> = {
id: 'microsoft_teams_reply_to_message',
name: 'Reply to Microsoft Teams Channel Message',
description: 'Reply to an existing message in a Microsoft Teams channel',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the team (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings or channel info)',
},
channelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the channel (e.g., "19:abc123def456@thread.tacv2" - from channel listings)',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the message to reply to (e.g., "1234567890123" - a numeric string from message responses)',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The reply content (plain text or HTML formatted message)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the reply was successful' },
messageId: { type: 'string', description: 'ID of the reply message' },
updatedContent: { type: 'boolean', description: 'Whether content was successfully sent' },
},
request: {
url: (params) => {
const teamId = params.teamId?.trim()
const channelId = params.channelId?.trim()
const messageId = params.messageId?.trim()
if (!teamId || !channelId || !messageId) {
throw new Error('Team ID, Channel ID, and Message ID are required')
}
return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/replies`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.content) {
throw new Error('Content is required')
}
return {
body: {
contentType: 'text',
content: params.content,
},
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsReplyParams) => {
const data = await response.json()
const metadata = {
messageId: data.id || '',
teamId: params?.teamId || '',
channelId: params?.channelId || '',
content: data.body?.content || params?.content || '',
createdTime: data.createdDateTime || '',
url: data.webUrl || '',
}
return {
success: true,
output: {
updatedContent: true,
metadata,
},
}
},
}
@@ -0,0 +1,190 @@
/**
* Server-side utilities for Microsoft Teams integration.
* This file contains functions that require server-side dependencies and should
* only be imported by API routes, NOT by tool definitions (to avoid circular imports).
*/
import type { Logger } from '@sim/logger'
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { FileAccessDeniedError, verifyFileAccess } from '@/app/api/files/authorization'
import type { UserFile } from '@/executor/types'
import type { GraphApiErrorResponse, GraphDriveItem } from '@/tools/microsoft_teams/types'
/** Maximum file size for Teams direct upload (4MB) */
const MAX_TEAMS_FILE_SIZE = 4 * 1024 * 1024
/** Output format for uploaded files */
interface TeamsFileOutput {
name: string
mimeType: string
data: string
size: number
}
/** Attachment reference for Teams message */
interface TeamsAttachmentRef {
id: string
contentType: 'reference'
contentUrl: string
name: string
}
/** Result from processing and uploading files for Teams */
export interface TeamsFileUploadResult {
attachments: TeamsAttachmentRef[]
filesOutput: TeamsFileOutput[]
}
/**
* Process and upload files to OneDrive for Teams message attachments.
* Handles size validation, downloading from storage, uploading to OneDrive,
* and creating attachment references.
*/
export async function uploadFilesForTeamsMessage(params: {
rawFiles: RawFileInput[]
accessToken: string
requestId: string
logger: Logger
userId: string
}): Promise<TeamsFileUploadResult> {
const { rawFiles, accessToken, requestId, logger: log, userId } = params
const attachments: TeamsAttachmentRef[] = []
const filesOutput: TeamsFileOutput[] = []
if (!rawFiles || rawFiles.length === 0) {
return { attachments, filesOutput }
}
log.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to OneDrive`)
const userFiles = processFilesToUserFiles(rawFiles, requestId, log) as UserFile[]
for (const file of userFiles) {
// Check size limit
if (file.size > MAX_TEAMS_FILE_SIZE) {
const sizeMB = (file.size / (1024 * 1024)).toFixed(2)
log.error(
`[${requestId}] File ${file.name} is ${sizeMB}MB, exceeds 4MB limit for direct upload`
)
throw new Error(
`File "${file.name}" (${sizeMB}MB) exceeds the 4MB limit for Teams attachments. Use smaller files or upload to SharePoint/OneDrive first.`
)
}
log.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`)
const hasAccess = await verifyFileAccess(file.key, userId)
if (!hasAccess) {
throw new FileAccessDeniedError()
}
// Download file from storage
const { buffer, contentType } = await downloadServableFileFromStorage(file, requestId, log)
if (buffer.length > MAX_TEAMS_FILE_SIZE) {
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(2)
throw new Error(
`File "${file.name}" (${sizeMB}MB) exceeds the 4MB limit for Teams attachments. Use smaller files or upload to SharePoint/OneDrive first.`
)
}
const resolvedMimeType = contentType || file.type || 'application/octet-stream'
filesOutput.push({
name: file.name,
mimeType: resolvedMimeType,
data: buffer.toString('base64'),
size: buffer.length,
})
// Upload to OneDrive
const uploadUrl =
'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' +
encodeURIComponent(file.name) +
':/content'
log.info(`[${requestId}] Uploading to OneDrive: ${uploadUrl}`)
const uploadResponse = await secureFetchWithValidation(
uploadUrl,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': resolvedMimeType,
},
body: buffer,
},
'uploadUrl'
)
if (!uploadResponse.ok) {
const errorData = (await uploadResponse.json().catch(() => ({}))) as GraphApiErrorResponse
log.error(`[${requestId}] Teams upload failed:`, errorData)
throw new Error(
`Failed to upload file to Teams: ${errorData.error?.message || 'Unknown error'}`
)
}
const uploadedFile = (await uploadResponse.json()) as GraphDriveItem
log.info(`[${requestId}] File uploaded to OneDrive successfully`, {
id: uploadedFile.id,
webUrl: uploadedFile.webUrl,
})
// Get file details for attachment reference
// Note: webDavUrl requires 'select' without the '$' prefix to be reliably returned
const fileDetailsUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${uploadedFile.id}?select=id,name,webDavUrl,eTag,size`
const fileDetailsResponse = await secureFetchWithValidation(
fileDetailsUrl,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
'fileDetailsUrl'
)
if (!fileDetailsResponse.ok) {
const errorData = (await fileDetailsResponse
.json()
.catch(() => ({}))) as GraphApiErrorResponse
log.error(`[${requestId}] Failed to get file details:`, errorData)
throw new Error(`Failed to get file details: ${errorData.error?.message || 'Unknown error'}`)
}
const fileDetails = (await fileDetailsResponse.json()) as GraphDriveItem
log.info(`[${requestId}] Got file details`, {
webDavUrl: fileDetails.webDavUrl,
eTag: fileDetails.eTag,
})
// Validate webDavUrl is present (required for Teams attachment references)
if (!fileDetails.webDavUrl) {
log.error(`[${requestId}] webDavUrl missing from file details`, { fileId: uploadedFile.id })
throw new Error(
`Failed to get file URL for attachment "${file.name}". The file was uploaded but Teams attachment reference could not be created.`
)
}
// Create attachment reference
const attachmentId = fileDetails.eTag?.match(/\{([a-f0-9-]+)\}/i)?.[1] || fileDetails.id
attachments.push({
id: attachmentId,
contentType: 'reference',
contentUrl: fileDetails.webDavUrl,
name: file.name,
})
log.info(`[${requestId}] Created attachment reference for ${file.name}`)
}
log.info(
`[${requestId}] All ${attachments.length} file(s) uploaded and attachment references created`
)
return { attachments, filesOutput }
}
@@ -0,0 +1,125 @@
import type {
MicrosoftTeamsReactionParams,
MicrosoftTeamsReactionResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const setReactionTool: ToolConfig<
MicrosoftTeamsReactionParams,
MicrosoftTeamsReactionResponse
> = {
id: 'microsoft_teams_set_reaction',
name: 'Add Reaction to Microsoft Teams Message',
description: 'Add an emoji reaction to a message in Microsoft Teams',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the team for channel messages (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID)',
},
channelId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the channel for channel messages (e.g., "19:abc123def456@thread.tacv2")',
},
chatId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The ID of the chat for chat messages (e.g., "19:abc123def456@thread.v2")',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the message to react to (e.g., "1234567890123" - a numeric string from message responses)',
},
reactionType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The emoji reaction (e.g., ❤️, 👍, 😊)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the reaction was added successfully' },
reactionType: { type: 'string', description: 'The emoji that was added' },
messageId: { type: 'string', description: 'ID of the message' },
},
request: {
url: (params) => {
const messageId = params.messageId?.trim()
if (!messageId) {
throw new Error('Message ID is required')
}
// Check if it's a channel or chat message
if (params.teamId && params.channelId) {
const teamId = params.teamId.trim()
const channelId = params.channelId.trim()
return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/setReaction`
}
if (params.chatId) {
const chatId = params.chatId.trim()
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}/setReaction`
}
throw new Error('Either (teamId and channelId) or chatId is required')
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.reactionType) {
throw new Error('Reaction type is required')
}
return {
reactionType: params.reactionType,
}
},
},
transformResponse: async (_response: Response, params?: MicrosoftTeamsReactionParams) => {
// setReaction returns 204 No Content on success
return {
success: true,
output: {
success: true,
reactionType: params?.reactionType || '',
messageId: params?.messageId || '',
metadata: {
messageId: params?.messageId || '',
teamId: params?.teamId,
channelId: params?.channelId,
chatId: params?.chatId,
},
},
}
},
}
+235
View File
@@ -0,0 +1,235 @@
import type { UserFile } from '@/executor/types'
import type { ToolFileData, ToolResponse } from '@/tools/types'
export interface GraphApiErrorResponse {
error?: {
message?: string
}
}
export interface GraphDriveItem {
id: string
webUrl?: string
webDavUrl?: string
eTag?: string
name?: string
size?: number
}
export interface GraphChatMessage {
id?: string
chatId?: string
channelIdentity?: { teamId?: string; channelId?: string }
body?: { content?: string }
createdDateTime?: string
webUrl?: string
}
export interface MicrosoftTeamsAttachment {
id: string
contentType: string
contentUrl?: string
content?: string
name?: string
thumbnailUrl?: string
size?: number
sourceUrl?: string
providerType?: string
item?: any
}
interface MicrosoftTeamsMetadata {
messageId?: string
channelId?: string
teamId?: string
chatId?: string
content?: string
createdTime?: string
url?: string
messageCount?: number
messages?: Array<{
id: string
content: string
sender: string
timestamp: string
messageType: string
attachments?: MicrosoftTeamsAttachment[]
uploadedFiles?: {
path: string
key: string
name: string
size: number
type: string
}[]
}>
// Global attachments summary
totalAttachments?: number
attachmentTypes?: string[]
}
export interface MicrosoftTeamsReadResponse extends ToolResponse {
output: {
content: string
metadata: MicrosoftTeamsMetadata
attachments?: Array<{
path: string
key: string
name: string
size: number
type: string
}>
}
}
export interface MicrosoftTeamsWriteResponse extends ToolResponse {
output: {
updatedContent: boolean
metadata: MicrosoftTeamsMetadata
files?: ToolFileData[]
}
}
export interface MicrosoftTeamsToolParams {
accessToken: string
messageId?: string
chatId?: string
channelId?: string
teamId?: string
content?: string
includeAttachments?: boolean
files?: UserFile[]
reactionType?: string // For reaction operations
}
// Update message params
export interface MicrosoftTeamsUpdateMessageParams extends MicrosoftTeamsToolParams {
messageId: string
content: string
}
// Delete message params
export interface MicrosoftTeamsDeleteMessageParams extends MicrosoftTeamsToolParams {
messageId: string
}
// Reply to message params
export interface MicrosoftTeamsReplyParams extends MicrosoftTeamsToolParams {
messageId: string
content: string
}
// Reaction params
export interface MicrosoftTeamsReactionParams extends MicrosoftTeamsToolParams {
messageId: string
reactionType: string
}
// Get message params
export interface MicrosoftTeamsGetMessageParams extends MicrosoftTeamsToolParams {
messageId: string
}
// Member list response
interface MicrosoftTeamsMember {
id: string
displayName: string
email?: string
userId?: string
roles?: string[]
}
export interface MicrosoftTeamsListMembersResponse extends ToolResponse {
output: {
members: MicrosoftTeamsMember[]
memberCount: number
hasMore?: boolean
metadata: {
teamId?: string
channelId?: string
chatId?: string
}
}
}
// Joined team summary (from GET /me/joinedTeams)
interface MicrosoftTeamsTeamSummary {
id: string
displayName: string
description?: string
isArchived?: boolean
}
export interface MicrosoftTeamsListTeamsResponse extends ToolResponse {
output: {
teams: MicrosoftTeamsTeamSummary[]
teamCount: number
hasMore: boolean
}
}
// Chat summary (from GET /me/chats)
interface MicrosoftTeamsChatSummary {
id: string
topic: string | null
chatType: string
webUrl?: string
createdDateTime?: string
lastUpdatedDateTime?: string
}
export interface MicrosoftTeamsListChatsResponse extends ToolResponse {
output: {
chats: MicrosoftTeamsChatSummary[]
chatCount: number
hasMore: boolean
}
}
// Channel summary (from GET /teams/{team-id}/channels)
interface MicrosoftTeamsChannelSummary {
id: string
displayName: string
description?: string | null
membershipType?: string
webUrl?: string
}
export interface MicrosoftTeamsListChannelsResponse extends ToolResponse {
output: {
channels: MicrosoftTeamsChannelSummary[]
channelCount: number
hasMore: boolean
metadata: {
teamId: string
}
}
}
// Delete response
export interface MicrosoftTeamsDeleteResponse extends ToolResponse {
output: {
deleted: boolean
messageId: string
metadata: MicrosoftTeamsMetadata
}
}
// Reaction response
export interface MicrosoftTeamsReactionResponse extends ToolResponse {
output: {
success: boolean
reactionType: string
messageId: string
metadata: MicrosoftTeamsMetadata
}
}
export type MicrosoftTeamsResponse =
| MicrosoftTeamsReadResponse
| MicrosoftTeamsWriteResponse
| MicrosoftTeamsDeleteResponse
| MicrosoftTeamsListMembersResponse
| MicrosoftTeamsReactionResponse
| MicrosoftTeamsListTeamsResponse
| MicrosoftTeamsListChatsResponse
| MicrosoftTeamsListChannelsResponse
@@ -0,0 +1,125 @@
import type {
MicrosoftTeamsReactionParams,
MicrosoftTeamsReactionResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const unsetReactionTool: ToolConfig<
MicrosoftTeamsReactionParams,
MicrosoftTeamsReactionResponse
> = {
id: 'microsoft_teams_unset_reaction',
name: 'Remove Reaction from Microsoft Teams Message',
description: 'Remove an emoji reaction from a message in Microsoft Teams',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the team for channel messages (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID)',
},
channelId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the channel for channel messages (e.g., "19:abc123def456@thread.tacv2")',
},
chatId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The ID of the chat for chat messages (e.g., "19:abc123def456@thread.v2")',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the message (e.g., "1234567890123" - a numeric string from message responses)',
},
reactionType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The emoji reaction to remove (e.g., ❤️, 👍, 😊)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the reaction was removed successfully' },
reactionType: { type: 'string', description: 'The emoji that was removed' },
messageId: { type: 'string', description: 'ID of the message' },
},
request: {
url: (params) => {
const messageId = params.messageId?.trim()
if (!messageId) {
throw new Error('Message ID is required')
}
// Check if it's a channel or chat message
if (params.teamId && params.channelId) {
const teamId = params.teamId.trim()
const channelId = params.channelId.trim()
return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/unsetReaction`
}
if (params.chatId) {
const chatId = params.chatId.trim()
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}/unsetReaction`
}
throw new Error('Either (teamId and channelId) or chatId is required')
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.reactionType) {
throw new Error('Reaction type is required')
}
return {
reactionType: params.reactionType,
}
},
},
transformResponse: async (_response: Response, params?: MicrosoftTeamsReactionParams) => {
// unsetReaction returns 204 No Content on success
return {
success: true,
output: {
success: true,
reactionType: params?.reactionType || '',
messageId: params?.messageId || '',
metadata: {
messageId: params?.messageId || '',
teamId: params?.teamId,
channelId: params?.channelId,
chatId: params?.chatId,
},
},
}
},
}
@@ -0,0 +1,121 @@
import type {
MicrosoftTeamsUpdateMessageParams,
MicrosoftTeamsWriteResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const updateChannelMessageTool: ToolConfig<
MicrosoftTeamsUpdateMessageParams,
MicrosoftTeamsWriteResponse
> = {
id: 'microsoft_teams_update_channel_message',
name: 'Update Microsoft Teams Channel Message',
description: 'Update an existing message in a Microsoft Teams channel',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the team (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings or channel info)',
},
channelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the channel containing the message (e.g., "19:abc123def456@thread.tacv2" - from channel listings)',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the message to update (e.g., "1234567890123" - a numeric string from message responses)',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The new content for the message (plain text or HTML formatted)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the update was successful' },
messageId: { type: 'string', description: 'ID of the updated message' },
updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' },
},
request: {
url: (params) => {
const teamId = params.teamId?.trim()
const channelId = params.channelId?.trim()
const messageId = params.messageId?.trim()
if (!teamId || !channelId || !messageId) {
throw new Error('Team ID, Channel ID, and Message ID are required')
}
return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}`
},
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.content) {
throw new Error('Content is required')
}
return {
body: {
contentType: 'text',
content: params.content,
},
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsUpdateMessageParams) => {
let data: any = {}
if (response.status !== 204 && response.headers.get('content-length') !== '0') {
const text = await response.text()
if (text) {
data = JSON.parse(text)
}
}
const metadata = {
messageId: data.id || params?.messageId || '',
teamId: params?.teamId || '',
channelId: params?.channelId || '',
content: data.body?.content || params?.content || '',
createdTime: data.createdDateTime || '',
url: data.webUrl || '',
}
return {
success: true,
output: {
updatedContent: true,
metadata,
},
}
},
}
@@ -0,0 +1,112 @@
import type {
MicrosoftTeamsUpdateMessageParams,
MicrosoftTeamsWriteResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const updateChatMessageTool: ToolConfig<
MicrosoftTeamsUpdateMessageParams,
MicrosoftTeamsWriteResponse
> = {
id: 'microsoft_teams_update_chat_message',
name: 'Update Microsoft Teams Chat Message',
description: 'Update an existing message in a Microsoft Teams chat',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
chatId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the chat containing the message (e.g., "19:abc123def456@thread.v2" - from chat listings)',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the message to update (e.g., "1234567890123" - a numeric string from message responses)',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The new content for the message (plain text or HTML formatted)',
},
},
outputs: {
success: { type: 'boolean', description: 'Whether the update was successful' },
messageId: { type: 'string', description: 'ID of the updated message' },
updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' },
},
request: {
url: (params) => {
const chatId = params.chatId?.trim()
const messageId = params.messageId?.trim()
if (!chatId || !messageId) {
throw new Error('Chat ID and Message ID are required')
}
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}`
},
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.content) {
throw new Error('Content is required')
}
return {
body: {
contentType: 'text',
content: params.content,
},
}
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsUpdateMessageParams) => {
let data: any = {}
if (response.status !== 204 && response.headers.get('content-length') !== '0') {
const text = await response.text()
if (text) {
data = JSON.parse(text)
}
}
const metadata = {
messageId: data.id || params?.messageId || '',
chatId: params?.chatId || '',
content: data.body?.content || params?.content || '',
createdTime: data.createdDateTime || '',
url: data.webUrl || '',
}
return {
success: true,
output: {
updatedContent: true,
metadata,
},
}
},
}
+450
View File
@@ -0,0 +1,450 @@
import { createLogger } from '@sim/logger'
import type { MicrosoftTeamsAttachment } from '@/tools/microsoft_teams/types'
import type { ToolFileData } from '@/tools/types'
const logger = createLogger('MicrosoftTeamsUtils')
interface ParsedMention {
name: string
fullTag: string
mentionId: number
}
interface TeamMember {
id: string
displayName: string
userIdentityType?: string
}
export interface TeamsMention {
id: number
mentionText: string
mentioned:
| {
user: {
id: string
displayName: string
userIdentityType?: string
}
}
| {
application: {
displayName: string
id: string
applicationIdentityType: 'bot'
}
}
}
/**
* Transform raw attachment data from Microsoft Graph API
*/
function transformAttachment(rawAttachment: any): MicrosoftTeamsAttachment {
return {
id: rawAttachment.id,
contentType: rawAttachment.contentType,
contentUrl: rawAttachment.contentUrl,
content: rawAttachment.content,
name: rawAttachment.name,
thumbnailUrl: rawAttachment.thumbnailUrl,
size: rawAttachment.size,
sourceUrl: rawAttachment.sourceUrl,
providerType: rawAttachment.providerType,
item: rawAttachment.item,
}
}
/**
* Extract attachments from message data
* Returns all attachments without any content processing
*/
export function extractMessageAttachments(message: any): MicrosoftTeamsAttachment[] {
const attachments = (message.attachments || []).map(transformAttachment)
return attachments
}
/**
* Fetch hostedContents for a chat message, upload each item to storage, and return uploaded file infos.
* Hosted contents expose base64 contentBytes via Microsoft Graph.
*/
export async function fetchHostedContentsForChatMessage(params: {
accessToken: string
chatId: string
messageId: string
}): Promise<ToolFileData[]> {
const { accessToken, chatId, messageId } = params
try {
const url = `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}/hostedContents`
const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } })
if (!res.ok) {
return []
}
const data = await res.json()
const items = Array.isArray(data.value) ? data.value : []
const results: ToolFileData[] = []
for (const item of items) {
const base64: string | undefined = item.contentBytes
if (!base64) continue
const contentType: string =
typeof item.contentType === 'string' ? item.contentType : 'application/octet-stream'
const name: string = item.id ? `teams-hosted-${item.id}` : 'teams-hosted-content'
results.push({ name, mimeType: contentType, data: base64 })
}
return results
} catch (error) {
logger.error('Error fetching/uploading hostedContents for chat message:', error)
return []
}
}
/**
* Fetch hostedContents for a channel message, upload each item to storage, and return uploaded file infos.
*/
export async function fetchHostedContentsForChannelMessage(params: {
accessToken: string
teamId: string
channelId: string
messageId: string
}): Promise<ToolFileData[]> {
const { accessToken, teamId, channelId, messageId } = params
try {
const url = `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/hostedContents`
const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } })
if (!res.ok) {
return []
}
const data = await res.json()
const items = Array.isArray(data.value) ? data.value : []
const results: ToolFileData[] = []
for (const item of items) {
const base64: string | undefined = item.contentBytes
if (!base64) continue
const contentType: string =
typeof item.contentType === 'string' ? item.contentType : 'application/octet-stream'
const name: string = item.id ? `teams-hosted-${item.id}` : 'teams-hosted-content'
results.push({ name, mimeType: contentType, data: base64 })
}
return results
} catch (error) {
logger.error('Error fetching/uploading hostedContents for channel message:', error)
return []
}
}
/**
* Download a reference-type attachment (SharePoint/OneDrive file) from Teams.
* These are files shared in Teams that are stored in SharePoint/OneDrive.
*
*/
async function downloadReferenceAttachment(params: {
accessToken: string
attachment: MicrosoftTeamsAttachment
}): Promise<ToolFileData | null> {
const { accessToken, attachment } = params
if (attachment.contentType !== 'reference') {
return null
}
const contentUrl = attachment.contentUrl
if (!contentUrl) {
logger.warn('Reference attachment has no contentUrl', { attachmentId: attachment.id })
return null
}
try {
const encodedUrl = Buffer.from(contentUrl)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
const shareId = `u!${encodedUrl}`
const metadataUrl = `https://graph.microsoft.com/v1.0/shares/${shareId}/driveItem`
const metadataRes = await fetch(metadataUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
})
if (!metadataRes.ok) {
const errorData = await metadataRes.json().catch(() => ({}))
logger.error('Failed to get driveItem metadata via shares API', {
status: metadataRes.status,
error: errorData,
attachmentName: attachment.name,
})
return null
}
const driveItem = await metadataRes.json()
const mimeType = driveItem.file?.mimeType || 'application/octet-stream'
const fileName = attachment.name || driveItem.name || 'attachment'
const downloadUrl = `https://graph.microsoft.com/v1.0/shares/${shareId}/driveItem/content`
const downloadRes = await fetch(downloadUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
})
if (!downloadRes.ok) {
logger.error('Failed to download file content', {
status: downloadRes.status,
fileName,
})
return null
}
const arrayBuffer = await downloadRes.arrayBuffer()
const base64Data = Buffer.from(arrayBuffer).toString('base64')
logger.info('Successfully downloaded reference attachment', {
fileName,
size: arrayBuffer.byteLength,
})
return {
name: fileName,
mimeType,
data: base64Data,
}
} catch (error) {
logger.error('Error downloading reference attachment:', {
error,
attachmentId: attachment.id,
attachmentName: attachment.name,
})
return null
}
}
export async function downloadAllReferenceAttachments(params: {
accessToken: string
attachments: MicrosoftTeamsAttachment[]
}): Promise<ToolFileData[]> {
const { accessToken, attachments } = params
const results: ToolFileData[] = []
const referenceAttachments = attachments.filter((att) => att.contentType === 'reference')
if (referenceAttachments.length === 0) {
return results
}
logger.info(`Downloading ${referenceAttachments.length} reference attachment(s)`)
for (const attachment of referenceAttachments) {
const file = await downloadReferenceAttachment({ accessToken, attachment })
if (file) {
results.push(file)
}
}
return results
}
function parseMentions(content: string): ParsedMention[] {
const mentions: ParsedMention[] = []
const mentionRegex = /<at>([^<]+)<\/at>/gi
let match: RegExpExecArray | null
let mentionId = 0
while ((match = mentionRegex.exec(content)) !== null) {
const name = match[1].trim()
if (name) {
mentions.push({
name,
fullTag: match[0],
mentionId: mentionId++,
})
}
}
return mentions
}
async function fetchChatMembers(chatId: string, accessToken: string): Promise<TeamMember[]> {
const response = await fetch(
`https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
return []
}
const data = await response.json()
return (data.value || []).map((member: TeamMember) => ({
id: member.id,
displayName: member.displayName || '',
userIdentityType: member.userIdentityType,
}))
}
async function fetchChannelMembers(
teamId: string,
channelId: string,
accessToken: string
): Promise<TeamMember[]> {
const response = await fetch(
`https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/members`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
return []
}
const data = await response.json()
return (data.value || []).map((member: TeamMember) => ({
id: member.id,
displayName: member.displayName || '',
userIdentityType: member.userIdentityType,
}))
}
function findMemberByName(members: TeamMember[], name: string): TeamMember | undefined {
const normalizedName = name.trim().toLowerCase()
return members.find((member) => member.displayName.toLowerCase() === normalizedName)
}
export async function resolveMentionsForChat(
content: string,
chatId: string,
accessToken: string
): Promise<{ mentions: TeamsMention[]; hasMentions: boolean; updatedContent: string }> {
const parsedMentions = parseMentions(content)
if (parsedMentions.length === 0) {
return { mentions: [], hasMentions: false, updatedContent: content }
}
const members = await fetchChatMembers(chatId, accessToken)
const mentions: TeamsMention[] = []
const resolvedTags = new Set<string>()
let updatedContent = content
for (const mention of parsedMentions) {
if (resolvedTags.has(mention.fullTag)) {
continue
}
const member = findMemberByName(members, mention.name)
if (member) {
const isBot = member.userIdentityType === 'bot'
if (isBot) {
mentions.push({
id: mention.mentionId,
mentionText: mention.name,
mentioned: {
application: {
displayName: member.displayName,
id: member.id,
applicationIdentityType: 'bot',
},
},
})
} else {
mentions.push({
id: mention.mentionId,
mentionText: mention.name,
mentioned: {
user: {
id: member.id,
displayName: member.displayName,
userIdentityType: member.userIdentityType || 'aadUser',
},
},
})
}
resolvedTags.add(mention.fullTag)
updatedContent = updatedContent.replaceAll(
mention.fullTag,
`<at id="${mention.mentionId}">${mention.name}</at>`
)
}
}
return {
mentions,
hasMentions: mentions.length > 0,
updatedContent,
}
}
export async function resolveMentionsForChannel(
content: string,
teamId: string,
channelId: string,
accessToken: string
): Promise<{ mentions: TeamsMention[]; hasMentions: boolean; updatedContent: string }> {
const parsedMentions = parseMentions(content)
if (parsedMentions.length === 0) {
return { mentions: [], hasMentions: false, updatedContent: content }
}
const members = await fetchChannelMembers(teamId, channelId, accessToken)
const mentions: TeamsMention[] = []
const resolvedTags = new Set<string>()
let updatedContent = content
for (const mention of parsedMentions) {
if (resolvedTags.has(mention.fullTag)) {
continue
}
const member = findMemberByName(members, mention.name)
if (member) {
const isBot = member.userIdentityType === 'bot'
if (isBot) {
mentions.push({
id: mention.mentionId,
mentionText: mention.name,
mentioned: {
application: {
displayName: member.displayName,
id: member.id,
applicationIdentityType: 'bot',
},
},
})
} else {
mentions.push({
id: mention.mentionId,
mentionText: mention.name,
mentioned: {
user: {
id: member.id,
displayName: member.displayName,
userIdentityType: member.userIdentityType || 'aadUser',
},
},
})
}
resolvedTags.add(mention.fullTag)
updatedContent = updatedContent.replaceAll(
mention.fullTag,
`<at id="${mention.mentionId}">${mention.name}</at>`
)
}
}
return {
mentions,
hasMentions: mentions.length > 0,
updatedContent,
}
}
@@ -0,0 +1,170 @@
import type {
MicrosoftTeamsToolParams,
MicrosoftTeamsWriteResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsWriteResponse> = {
id: 'microsoft_teams_write_channel',
name: 'Write to Microsoft Teams Channel',
description: 'Write or send a message to a Microsoft Teams channel',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the team to write to (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings)',
},
channelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the channel to write to (e.g., "19:abc123def456@thread.tacv2" - from channel listings)',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The content to write to the channel (plain text or HTML formatted, supports @mentions)',
},
files: {
type: 'file[]',
required: false,
visibility: 'user-only',
description: 'Files to attach to the message',
},
},
outputs: {
success: { type: 'boolean', description: 'Teams channel message send success status' },
messageId: { type: 'string', description: 'Unique identifier for the sent message' },
teamId: { type: 'string', description: 'ID of the team where message was sent' },
channelId: { type: 'string', description: 'ID of the channel where message was sent' },
createdTime: { type: 'string', description: 'Timestamp when message was created' },
url: { type: 'string', description: 'Web URL to the message' },
updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' },
files: { type: 'file[]', description: 'Files attached to the message' },
},
request: {
url: (params) => {
const teamId = params.teamId?.trim()
if (!teamId) {
throw new Error('Team ID is required')
}
const channelId = params.channelId?.trim()
if (!channelId) {
throw new Error('Channel ID is required')
}
// If files are provided, use custom API route for attachment handling
if (params.files && params.files.length > 0) {
return '/api/tools/microsoft_teams/write_channel'
}
// If content contains mentions, use custom API route for mention resolution
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
if (hasMentions) {
return '/api/tools/microsoft_teams/write_channel'
}
const encodedTeamId = encodeURIComponent(teamId)
const encodedChannelId = encodeURIComponent(channelId)
const url = `https://graph.microsoft.com/v1.0/teams/${encodedTeamId}/channels/${encodedChannelId}/messages`
return url
},
method: 'POST',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
// Validate content
if (!params.content) {
throw new Error('Content is required')
}
// If using custom API route (with files or mentions), pass all params
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
if (params.files && params.files.length > 0) {
return {
accessToken: params.accessToken,
teamId: params.teamId,
channelId: params.channelId,
content: params.content,
files: params.files,
}
}
if (hasMentions) {
return {
accessToken: params.accessToken,
teamId: params.teamId,
channelId: params.channelId,
content: params.content,
}
}
// Microsoft Teams API expects this specific format for channel messages
const requestBody = {
body: {
contentType: 'text',
content: params.content,
},
}
return requestBody
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => {
const data = await response.json()
// Handle custom API route response format
if (data.success !== undefined && data.output) {
return data
}
// Handle direct Graph API response format
const metadata = {
messageId: data.id || '',
teamId: data.channelIdentity?.teamId || '',
channelId: data.channelIdentity?.channelId || '',
content: data.body?.content || params?.content || '',
createdTime: data.createdDateTime || new Date().toISOString(),
url: data.webUrl || '',
}
return {
success: true,
output: {
updatedContent: true,
metadata,
},
}
},
}
@@ -0,0 +1,150 @@
import type {
MicrosoftTeamsToolParams,
MicrosoftTeamsWriteResponse,
} from '@/tools/microsoft_teams/types'
import type { ToolConfig } from '@/tools/types'
export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsWriteResponse> = {
id: 'microsoft_teams_write_chat',
name: 'Write to Microsoft Teams Chat',
description: 'Write or update content in a Microsoft Teams chat',
version: '1.0',
errorExtractor: 'nested-error-object',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Teams API',
},
chatId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the chat to write to (e.g., "19:abc123def456@thread.v2" - from chat listings)',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The content to write to the message (plain text or HTML formatted, supports @mentions)',
},
files: {
type: 'file[]',
required: false,
visibility: 'user-only',
description: 'Files to attach to the message',
},
},
outputs: {
success: { type: 'boolean', description: 'Teams chat message send success status' },
messageId: { type: 'string', description: 'Unique identifier for the sent message' },
chatId: { type: 'string', description: 'ID of the chat where message was sent' },
createdTime: { type: 'string', description: 'Timestamp when message was created' },
url: { type: 'string', description: 'Web URL to the message' },
updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' },
files: { type: 'file[]', description: 'Files attached to the message' },
},
request: {
url: (params) => {
// Ensure chatId is valid
const chatId = params.chatId?.trim()
if (!chatId) {
throw new Error('Chat ID is required')
}
// If files are provided, use custom API route for attachment handling
if (params.files && params.files.length > 0) {
return '/api/tools/microsoft_teams/write_chat'
}
// If content contains mentions, use custom API route for mention resolution
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
if (hasMentions) {
return '/api/tools/microsoft_teams/write_chat'
}
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages`
},
method: 'POST',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
// Validate content
if (!params.content) {
throw new Error('Content is required')
}
// If using custom API route (with files or mentions), pass all params
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
if (params.files && params.files.length > 0) {
return {
accessToken: params.accessToken,
chatId: params.chatId,
content: params.content,
files: params.files,
}
}
if (hasMentions) {
return {
accessToken: params.accessToken,
chatId: params.chatId,
content: params.content,
}
}
// Microsoft Teams API expects this specific format
const requestBody = {
body: {
contentType: 'text',
content: params.content,
},
}
return requestBody
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => {
const data = await response.json()
// Handle custom API route response format
if (data.success !== undefined && data.output) {
return data
}
// Handle direct Graph API response format
const metadata = {
messageId: data.id || '',
chatId: data.chatId || '',
content: data.body?.content || params?.content || '',
createdTime: data.createdDateTime || new Date().toISOString(),
url: data.webUrl || '',
}
return {
success: true,
output: {
updatedContent: true,
metadata,
},
}
},
}