feat(tools): added microsoft teams tools/block (#405)

* feat: microsoft teams block added

* updated name

* oauth working

* fixing accessToken

* saving accessToken spot

* all four tools are working

* display name better

* finished teams tool

* Remove package-lock.json from PR

* added greptile comments

* added scopes, removed ;, removed loggers

* solved credential bug

* added docs and rebased

* fixed lint checks

* more bun lint

* bun lint errors solved

---------

Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
This commit is contained in:
Adam Gough
2025-05-24 14:02:37 -07:00
committed by GitHub
parent 8c268e23dd
commit 45c92067e2
21 changed files with 2469 additions and 13 deletions

View File

@@ -24,6 +24,7 @@
"linkup",
"mem0",
"memory",
"microsoft_teams",
"notion",
"openai",
"perplexity",

View File

@@ -0,0 +1,100 @@
---
title: Microsoft Teams
description: Read, write, and create messages
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="microsoft_teams"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2228.833 2073.333"
>
<path fill="#5059C9" d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"/>
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25"/>
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917"/>
<path fill="#7B83EB" d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"/>
<path opacity=".1" d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"/>
<path opacity=".2" d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
<path opacity=".2" d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
<path opacity=".2" d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"/>
<path opacity=".1" d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"/>
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
<path opacity=".2" d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"/>
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="198.099" y1="1683.0726" x2="942.2344" y2="394.2607" gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
<stop offset="0" stop-color="#5a62c3"/>
<stop offset=".5" stop-color="#4d55bd"/>
<stop offset="1" stop-color="#3940ab"/>
</linearGradient>
<path fill="url(#a)" d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"/>
<path fill="#FFF" d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Microsoft Teams](https://teams.microsoft.com) is a robust communication and collaboration platform that enables users to engage in real-time messaging, meetings, and content sharing within teams and organizations. As part of Microsoft's productivity ecosystem, Microsoft Teams offers seamless chat functionality integrated with Office 365, allowing users to post messages, coordinate work, and stay connected across devices and workflows.
With Microsoft Teams, you can:
- **Send and receive messages**: Communicate instantly with individuals or groups in chat threads
- **Collaborate in real-time**: Share updates and information across teams within channels and chats
- **Organize conversations**: Maintain context with threaded discussions and persistent chat history
- **Share files and content**: Attach and view documents, images, and links directly in chat
- **Integrate with Microsoft 365**: Seamlessly connect with Outlook, SharePoint, OneDrive, and more
- **Access across devices**: Use Teams on desktop, web, and mobile with cloud-synced conversations
- **Secure communication**: Leverage enterprise-grade security and compliance features
In Sim Studio, the Microsoft Teams integration enables your agents to interact directly with chat messages programmatically. This allows for powerful automation scenarios such as sending updates, posting alerts, coordinating tasks, and responding to conversations in real time. Your agents can write new messages to chats or channels, update content based on workflow data, and engage with users where collaboration happens. By integrating Sim Studio with Microsoft Teams, you bridge the gap between intelligent workflows and team communication — empowering your agents to streamline collaboration, automate communication tasks, and keep your teams aligned.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Microsoft Teams functionality to manage messages. Read content from existing messages and write to messages using OAuth authentication. Supports text content manipulation for message creation and editing.
## Tools
### `microsoft_teams_read_chat`
### `microsoft_teams_write_chat`
### `microsoft_teams_read_channel`
### `microsoft_teams_write_channel`
## Block Configuration
### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `operation` | string | Yes | Operation |
### Outputs
| Output | Type | Description |
| ------ | ---- | ----------- |
| `response` | object | Output from response |
| ↳ `content` | string | content of the response |
| ↳ `metadata` | json | metadata of the response |
| ↳ `updatedContent` | boolean | updatedContent of the response |
## Notes
- Category: `tools`
- Type: `microsoft_teams`

View File

@@ -0,0 +1,121 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('TeamsChannelsAPI')
export async function POST(request: Request) {
try {
const session = await getSession()
const body = await request.json()
const { credential, teamId, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!teamId) {
logger.error('Missing team ID in request')
return NextResponse.json({ error: 'Team ID is required' }, { status: 400 })
}
try {
// Get the userId either from the session or from the workflowId
const userId = session?.user?.id || ''
if (!userId) {
logger.error('No user ID found in session')
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}
const accessToken = await refreshAccessTokenIfNeeded(credential, userId, workflowId)
if (!accessToken) {
logger.error('Failed to get access token', { credentialId: credential, userId })
return NextResponse.json(
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}
const response = await fetch(
`https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
const errorData = await response.json()
logger.error('Microsoft Graph API error getting channels', {
status: response.status,
error: errorData,
endpoint: `https://graph.microsoft.com/v1.0/teams/${teamId}/channels`,
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
},
{ status: 401 }
)
}
throw new Error(`Microsoft Graph API error: ${JSON.stringify(errorData)}`)
}
const data = await response.json()
const channels = data.value
return NextResponse.json({
channels: channels,
})
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||
errorMessage.includes('token') ||
errorMessage.includes('unauthorized') ||
errorMessage.includes('unauthenticated')
) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
details: errorMessage,
},
{ status: 401 }
)
}
throw innerError
}
} catch (error) {
logger.error('Error processing Channels request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Microsoft Teams channels',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,221 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('teams-chats')
// Helper function to get chat members and create a meaningful name
const getChatDisplayName = async (
chatId: string,
accessToken: string,
chatTopic?: string
): Promise<string> => {
try {
// If the chat already has a topic, use it
if (chatTopic?.trim() && chatTopic !== 'null') {
return chatTopic
}
// Fetch chat members to create a meaningful name
const membersResponse = await fetch(
`https://graph.microsoft.com/v1.0/chats/${chatId}/members`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (membersResponse.ok) {
const membersData = await membersResponse.json()
const members = membersData.value || []
// Filter out the current user and get display names
const memberNames = members
.filter((member: any) => member.displayName && member.displayName !== 'Unknown')
.map((member: any) => member.displayName)
.slice(0, 3) // Limit to first 3 names to avoid very long names
if (memberNames.length > 0) {
if (memberNames.length === 1) {
return memberNames[0] // 1:1 chat
}
if (memberNames.length === 2) {
return memberNames.join(' & ') // 2-person group
}
return `${memberNames.slice(0, 2).join(', ')} & ${memberNames.length - 2} more` // Larger group
}
}
// Fallback: try to get a better name from recent messages
try {
const messagesResponse = await fetch(
`https://graph.microsoft.com/v1.0/chats/${chatId}/messages?$top=10&$orderby=createdDateTime desc`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (messagesResponse.ok) {
const messagesData = await messagesResponse.json()
const messages = messagesData.value || []
// Look for chat rename events
for (const message of messages) {
if (message.eventDetail?.chatDisplayName) {
return message.eventDetail.chatDisplayName
}
}
// Get unique sender names from recent messages as last resort
const senderNames = [
...new Set(
messages
.filter(
(msg: any) => msg.from?.user?.displayName && msg.from.user.displayName !== 'Unknown'
)
.map((msg: any) => msg.from.user.displayName)
),
].slice(0, 3)
if (senderNames.length > 0) {
if (senderNames.length === 1) {
return senderNames[0] as string
}
if (senderNames.length === 2) {
return senderNames.join(' & ')
}
return `${senderNames.slice(0, 2).join(', ')} & ${senderNames.length - 2} more`
}
}
} catch (error) {
logger.warn(
`Failed to get better name from messages for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`
)
}
// Final fallback
return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...`
} catch (error) {
logger.warn(
`Failed to get display name for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`
)
return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...`
}
}
export async function POST(request: Request) {
try {
const session = await getSession()
const body = await request.json()
const { credential } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
try {
// Get the userId either from the session or from the workflowId
const userId = session?.user?.id || ''
if (!userId) {
logger.error('No user ID found in session')
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}
const accessToken = await refreshAccessTokenIfNeeded(credential, userId, body.workflowId)
if (!accessToken) {
logger.error('Failed to get access token', { credentialId: credential, userId })
return NextResponse.json({ error: 'Could not retrieve access token' }, { status: 401 })
}
// Now try to fetch the chats
const response = await fetch('https://graph.microsoft.com/v1.0/me/chats', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json()
logger.error('Microsoft Graph API error getting chats', {
status: response.status,
error: errorData,
endpoint: 'https://graph.microsoft.com/v1.0/me/chats',
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
},
{ status: 401 }
)
}
throw new Error(`Microsoft Graph API error: ${JSON.stringify(errorData)}`)
}
const data = await response.json()
// Process chats with enhanced display names
const chats = await Promise.all(
data.value.map(async (chat: any) => ({
id: chat.id,
displayName: await getChatDisplayName(chat.id, accessToken, chat.topic),
}))
)
return NextResponse.json({
chats: chats,
})
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||
errorMessage.includes('token') ||
errorMessage.includes('unauthorized') ||
errorMessage.includes('unauthenticated')
) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
details: errorMessage,
},
{ status: 401 }
)
}
throw innerError
}
} catch (error) {
logger.error('Error processing Chats request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Microsoft Teams chats',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,113 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('teams-teams')
export async function POST(request: Request) {
try {
const session = await getSession()
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
try {
// Get the userId either from the session or from the workflowId
const userId = session?.user?.id || ''
if (!userId) {
logger.error('No user ID found in session')
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}
const accessToken = await refreshAccessTokenIfNeeded(credential, userId, workflowId)
if (!accessToken) {
logger.error('Failed to get access token', { credentialId: credential, userId })
return NextResponse.json(
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}
const response = await fetch('https://graph.microsoft.com/v1.0/me/joinedTeams', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json()
logger.error('Microsoft Graph API error getting teams', {
status: response.status,
error: errorData,
endpoint: 'https://graph.microsoft.com/v1.0/me/joinedTeams',
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
},
{ status: 401 }
)
}
throw new Error(`Microsoft Graph API error: ${JSON.stringify(errorData)}`)
}
const data = await response.json()
const teams = data.value
return NextResponse.json({
teams: teams,
})
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||
errorMessage.includes('token') ||
errorMessage.includes('unauthorized') ||
errorMessage.includes('unauthenticated')
) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
details: errorMessage,
},
{ status: 401 }
)
}
throw innerError
}
} catch (error) {
logger.error('Error processing Teams request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Microsoft Teams teams',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -87,6 +87,16 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'read:user:jira': 'Read your Jira user',
'read:field-configuration:jira': 'Read your Jira field configuration',
'read:issue-details:jira': 'Read your Jira issue details',
'User.Read': 'Read your Microsoft user',
'Chat.Read': 'Read your Microsoft chats',
'Chat.ReadWrite': 'Write to your Microsoft chats',
'Chat.ReadBasic': 'Read your Microsoft chats',
'Channel.ReadBasic.All': 'Read your Microsoft channels',
'ChannelMessage.Send': 'Write to your Microsoft channels',
'ChannelMessage.Read.All': 'Read your Microsoft channels',
'Group.Read.All': 'Read your Microsoft groups',
'Group.ReadWrite.All': 'Write to your Microsoft groups',
'Team.ReadBasic.All': 'Read your Microsoft teams',
identify: 'Read your Discord user',
bot: 'Read your Discord bot',
'messages.read': 'Read your Discord messages',

View File

@@ -0,0 +1,907 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Check, ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-react'
import { MicrosoftTeamsIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Logger } from '@/lib/logs/console-logger'
import {
type Credential,
getProviderIdFromServiceId,
getServiceIdFromScopes,
type OAuthProvider,
} from '@/lib/oauth'
import { saveToStorage } from '@/stores/workflows/persistence'
import { OAuthRequiredModal } from '../../credential-selector/components/oauth-required-modal'
const logger = new Logger('teams_message_selector')
export interface TeamsMessageInfo {
id: string
displayName: string
type: 'team' | 'channel' | 'chat'
teamId?: string
channelId?: string
chatId?: string
webViewLink?: string
}
interface TeamsMessageSelectorProps {
value: string
onChange: (value: string, messageInfo?: TeamsMessageInfo) => void
provider: OAuthProvider
requiredScopes?: string[]
label?: string
disabled?: boolean
serviceId?: string
showPreview?: boolean
onMessageInfoChange?: (messageInfo: TeamsMessageInfo | null) => void
credential: string
selectionType?: 'team' | 'channel' | 'chat'
initialTeamId?: string
workflowId: string
}
export function TeamsMessageSelector({
value,
onChange,
provider,
requiredScopes = [],
label = 'Select Teams message location',
disabled = false,
serviceId,
showPreview = true,
onMessageInfoChange,
selectionType = 'team',
initialTeamId,
workflowId,
}: TeamsMessageSelectorProps) {
const [open, setOpen] = useState(false)
const [credentials, setCredentials] = useState<Credential[]>([])
const [teams, setTeams] = useState<TeamsMessageInfo[]>([])
const [channels, setChannels] = useState<TeamsMessageInfo[]>([])
const [chats, setChats] = useState<TeamsMessageInfo[]>([])
const [selectedCredentialId, setSelectedCredentialId] = useState<string>('')
const [selectedTeamId, setSelectedTeamId] = useState<string>('')
const [selectedChannelId, setSelectedChannelId] = useState<string>('')
const [selectedChatId, setSelectedChatId] = useState<string>('')
const [selectedMessageId, setSelectedMessageId] = useState(value)
const [selectedMessage, setSelectedMessage] = useState<TeamsMessageInfo | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [showOAuthModal, setShowOAuthModal] = useState(false)
const initialFetchRef = useRef(false)
const [error, setError] = useState<string | null>(null)
const [selectionStage, setSelectionStage] = useState<'team' | 'channel' | 'chat'>(selectionType)
// Determine the appropriate service ID based on provider and scopes
const getServiceId = (): string => {
if (serviceId) return serviceId
return getServiceIdFromScopes(provider, requiredScopes)
}
// Determine the appropriate provider ID based on service and scopes
const getProviderId = (): string => {
const effectiveServiceId = getServiceId()
return getProviderIdFromServiceId(effectiveServiceId)
}
const fetchCredentials = useCallback(async () => {
setIsLoading(true)
try {
const providerId = getProviderId()
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
if (response.ok) {
const data = await response.json()
setCredentials(data.credentials)
// Auto-select logic for credentials
if (data.credentials.length > 0) {
// If we already have a selected credential ID, check if it's valid
if (
selectedCredentialId &&
data.credentials.some((cred: Credential) => cred.id === selectedCredentialId)
) {
// Keep the current selection
} else {
// Otherwise, select the default or first credential
const defaultCred = data.credentials.find((cred: Credential) => cred.isDefault)
if (defaultCred) {
setSelectedCredentialId(defaultCred.id)
} else if (data.credentials.length === 1) {
setSelectedCredentialId(data.credentials[0].id)
}
}
}
}
} catch (error) {
logger.error('Error fetching credentials:', error)
} finally {
setIsLoading(false)
}
}, [provider, getProviderId, selectedCredentialId])
// Fetch teams
const fetchTeams = useCallback(async () => {
if (!selectedCredentialId) return
setIsLoading(true)
setError(null)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/teams', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
credential: selectedCredentialId,
}),
})
if (!response.ok) {
const errorData = await response.json()
// If server indicates auth is required, show the auth modal
if (response.status === 401 && errorData.authRequired) {
logger.warn('Authentication required for Microsoft Teams')
setShowOAuthModal(true)
throw new Error('Microsoft Teams authentication required')
}
throw new Error(errorData.error || 'Failed to fetch teams')
}
const data = await response.json()
const teamsData = data.teams.map((team: { id: string; displayName: string }) => ({
id: team.id,
displayName: team.displayName,
type: 'team' as const,
teamId: team.id,
webViewLink: `https://teams.microsoft.com/l/team/${team.id}`,
}))
setTeams(teamsData)
// If we have a selected team ID, find it in the list
if (selectedTeamId) {
const team = teamsData.find((t: TeamsMessageInfo) => t.teamId === selectedTeamId)
if (team) {
setSelectedMessage(team)
onMessageInfoChange?.(team)
}
}
} catch (error) {
logger.error('Error fetching teams:', error)
setError((error as Error).message)
setTeams([])
} finally {
setIsLoading(false)
}
}, [selectedCredentialId, selectedTeamId, onMessageInfoChange, workflowId])
// Fetch channels for a selected team
const fetchChannels = useCallback(
async (teamId: string) => {
if (!selectedCredentialId || !teamId) return
setIsLoading(true)
setError(null)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/channels', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
credential: selectedCredentialId,
teamId,
}),
})
if (!response.ok) {
const errorData = await response.json()
// If server indicates auth is required, show the auth modal
if (response.status === 401 && errorData.authRequired) {
logger.warn('Authentication required for Microsoft Teams')
setShowOAuthModal(true)
throw new Error('Microsoft Teams authentication required')
}
throw new Error(errorData.error || 'Failed to fetch channels')
}
const data = await response.json()
const channelsData = data.channels.map((channel: { id: string; displayName: string }) => ({
id: `${teamId}-${channel.id}`,
displayName: channel.displayName,
type: 'channel' as const,
teamId,
channelId: channel.id,
webViewLink: `https://teams.microsoft.com/l/channel/${teamId}/${encodeURIComponent(channel.displayName)}/${channel.id}`,
}))
setChannels(channelsData)
// If we have a selected channel ID, find it in the list
if (selectedChannelId) {
const channel = channelsData.find(
(c: TeamsMessageInfo) => c.channelId === selectedChannelId
)
if (channel) {
setSelectedMessage(channel)
onMessageInfoChange?.(channel)
}
}
} catch (error) {
logger.error('Error fetching channels:', error)
setError((error as Error).message)
setChannels([])
} finally {
setIsLoading(false)
}
},
[selectedCredentialId, selectedChannelId, onMessageInfoChange, workflowId]
)
// Fetch chats
const fetchChats = useCallback(async () => {
if (!selectedCredentialId) return
setIsLoading(true)
setError(null)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/chats', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
credential: selectedCredentialId,
workflowId: workflowId, // Pass the workflowId for server-side authentication
}),
})
if (!response.ok) {
const errorData = await response.json()
// If server indicates auth is required, show the auth modal
if (response.status === 401 && errorData.authRequired) {
logger.warn('Authentication required for Microsoft Teams')
setShowOAuthModal(true)
throw new Error('Microsoft Teams authentication required')
}
throw new Error(errorData.error || 'Failed to fetch chats')
}
const data = await response.json()
const chatsData = data.chats.map((chat: { id: string; displayName: string }) => ({
id: chat.id,
displayName: chat.displayName,
type: 'chat' as const,
chatId: chat.id,
webViewLink: `https://teams.microsoft.com/l/chat/${chat.id}`,
}))
setChats(chatsData)
// If we have a selected chat ID, find it in the list
if (selectedChatId) {
const chat = chatsData.find((c: TeamsMessageInfo) => c.chatId === selectedChatId)
if (chat) {
setSelectedMessage(chat)
onMessageInfoChange?.(chat)
}
}
} catch (error) {
logger.error('Error fetching chats:', error)
setError((error as Error).message)
setChats([])
} finally {
setIsLoading(false)
}
}, [selectedCredentialId, selectedChatId, onMessageInfoChange, workflowId])
// Update selection stage based on selected values and selectionType
useEffect(() => {
// If we have explicit values selected, use those to determine the stage
if (selectedChatId) {
setSelectionStage('chat')
} else if (selectedChannelId) {
setSelectionStage('channel')
} else if (selectionType === 'channel' && selectedTeamId) {
// If we're in channel mode and have a team selected, go to channel selection
setSelectionStage('channel')
} else if (selectionType !== 'team' && !selectedTeamId) {
// If no selections but we have a specific selection type, use that
// But for channel selection, start with team selection if no team is selected
if (selectionType === 'channel') {
setSelectionStage('team')
} else {
setSelectionStage(selectionType)
}
} else {
// Default to team selection
setSelectionStage('team')
}
}, [selectedTeamId, selectedChannelId, selectedChatId, selectionType])
// Handle open change
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen)
// Only fetch data when opening the dropdown
if (isOpen && selectedCredentialId) {
if (selectionStage === 'team') {
fetchTeams()
} else if (selectionStage === 'channel' && selectedTeamId) {
fetchChannels(selectedTeamId)
} else if (selectionStage === 'chat') {
fetchChats()
}
}
}
// Keep internal selectedMessageId in sync with the value prop
useEffect(() => {
if (value !== selectedMessageId) {
setSelectedMessageId(value)
}
}, [value])
// Handle team selection
const handleSelectTeam = (team: TeamsMessageInfo) => {
setSelectedTeamId(team.teamId || '')
setSelectedChannelId('')
setSelectedChatId('')
setSelectedMessage(team)
setSelectedMessageId(team.id)
onChange(team.id, team)
onMessageInfoChange?.(team)
setSelectionStage('channel')
fetchChannels(team.teamId || '')
setOpen(false)
}
// Handle channel selection
const handleSelectChannel = (channel: TeamsMessageInfo) => {
setSelectedChannelId(channel.channelId || '')
setSelectedChatId('')
setSelectedMessage(channel)
setSelectedMessageId(channel.id)
onChange(channel.id, channel)
onMessageInfoChange?.(channel)
setOpen(false)
}
// Handle chat selection
const handleSelectChat = (chat: TeamsMessageInfo) => {
setSelectedChatId(chat.chatId || '')
setSelectedMessage(chat)
setSelectedMessageId(chat.id)
onChange(chat.id, chat)
onMessageInfoChange?.(chat)
setOpen(false)
}
// Handle adding a new credential
const handleAddCredential = () => {
const effectiveServiceId = getServiceId()
const providerId = getProviderId()
// Store information about the required connection
saveToStorage<string>('pending_service_id', effectiveServiceId)
saveToStorage<string[]>('pending_oauth_scopes', requiredScopes)
saveToStorage<string>('pending_oauth_return_url', window.location.href)
saveToStorage<string>('pending_oauth_provider_id', providerId)
// Show the OAuth modal
setShowOAuthModal(true)
setOpen(false)
}
// Clear selection
const handleClearSelection = () => {
setSelectedMessageId('')
setSelectedTeamId('')
setSelectedChannelId('')
setSelectedChatId('')
setSelectedMessage(null)
setError(null)
onChange('', undefined)
onMessageInfoChange?.(null)
setSelectionStage(selectionType) // Reset to the initial selection type
}
// Render dropdown options based on the current selection stage
const renderSelectionOptions = () => {
if (selectionStage === 'team' && teams.length > 0) {
return (
<CommandGroup>
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>Teams</div>
{teams.map((team) => (
<CommandItem
key={team.id}
value={`team-${team.id}-${team.displayName}`}
onSelect={() => handleSelectTeam(team)}
>
<div className='flex items-center gap-2 overflow-hidden'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='truncate font-normal'>{team.displayName}</span>
</div>
{team.teamId === selectedTeamId && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
))}
</CommandGroup>
)
}
if (selectionStage === 'channel' && channels.length > 0) {
return (
<CommandGroup>
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>Channels</div>
{channels.map((channel) => (
<CommandItem
key={channel.id}
value={`channel-${channel.id}-${channel.displayName}`}
onSelect={() => handleSelectChannel(channel)}
>
<div className='flex items-center gap-2 overflow-hidden'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='truncate font-normal'>{channel.displayName}</span>
</div>
{channel.channelId === selectedChannelId && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
))}
</CommandGroup>
)
}
if (selectionStage === 'chat' && chats.length > 0) {
return (
<CommandGroup>
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>Chats</div>
{chats.map((chat) => (
<CommandItem
key={chat.id}
value={`chat-${chat.id}-${chat.displayName}`}
onSelect={() => handleSelectChat(chat)}
>
<div className='flex items-center gap-2 overflow-hidden'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='truncate font-normal'>{chat.displayName}</span>
</div>
{chat.chatId === selectedChatId && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
))}
</CommandGroup>
)
}
return null
}
// Restore team selection on page refresh
const restoreTeamSelection = useCallback(
async (teamId: string) => {
if (!selectedCredentialId || !teamId || selectionType !== 'team') return
setIsLoading(true)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/teams', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: selectedCredentialId, workflowId }),
})
if (response.ok) {
const data = await response.json()
const team = data.teams.find((t: { id: string; displayName: string }) => t.id === teamId)
if (team) {
const teamInfo: TeamsMessageInfo = {
id: team.id,
displayName: team.displayName,
type: 'team',
teamId: team.id,
webViewLink: `https://teams.microsoft.com/l/team/${team.id}`,
}
setSelectedTeamId(team.id)
setSelectedMessage(teamInfo)
onMessageInfoChange?.(teamInfo)
}
}
} catch (error) {
logger.error('Error restoring team selection:', error)
} finally {
setIsLoading(false)
}
},
[selectedCredentialId, selectionType, onMessageInfoChange, workflowId]
)
// Restore chat selection on page refresh
const restoreChatSelection = useCallback(
async (chatId: string) => {
if (!selectedCredentialId || !chatId || selectionType !== 'chat') return
setIsLoading(true)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/chats', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: selectedCredentialId, workflowId }),
})
if (response.ok) {
const data = await response.json()
const chat = data.chats.find((c: { id: string; displayName: string }) => c.id === chatId)
if (chat) {
const chatInfo: TeamsMessageInfo = {
id: chat.id,
displayName: chat.displayName,
type: 'chat',
chatId: chat.id,
webViewLink: `https://teams.microsoft.com/l/chat/${chat.id}`,
}
setSelectedChatId(chat.id)
setSelectedMessage(chatInfo)
onMessageInfoChange?.(chatInfo)
}
}
} catch (error) {
logger.error('Error restoring chat selection:', error)
} finally {
setIsLoading(false)
}
},
[selectedCredentialId, selectionType, onMessageInfoChange, workflowId]
)
// Restore channel selection on page refresh
const restoreChannelSelection = useCallback(
async (channelId: string) => {
if (!selectedCredentialId || !channelId || selectionType !== 'channel') return
setIsLoading(true)
try {
// First fetch teams to search through them
const teamsResponse = await fetch('/api/auth/oauth/microsoft-teams/teams', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: selectedCredentialId, workflowId }),
})
if (teamsResponse.ok) {
const teamsData = await teamsResponse.json()
// Create parallel promises for all teams to search for the channel
const channelSearchPromises = teamsData.teams.map(
async (team: { id: string; displayName: string }) => {
try {
const channelsResponse = await fetch('/api/auth/oauth/microsoft-teams/channels', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
credential: selectedCredentialId,
teamId: team.id,
workflowId,
}),
})
if (channelsResponse.ok) {
const channelsData = await channelsResponse.json()
const channel = channelsData.channels.find(
(c: { id: string; displayName: string }) => c.id === channelId
)
if (channel) {
return {
team,
channel,
channelInfo: {
id: `${team.id}-${channel.id}`,
displayName: channel.displayName,
type: 'channel' as const,
teamId: team.id,
channelId: channel.id,
webViewLink: `https://teams.microsoft.com/l/channel/${team.id}/${encodeURIComponent(channel.displayName)}/${channel.id}`,
},
}
}
}
} catch (error) {
logger.warn(
`Error searching for channel in team ${team.id}:`,
error instanceof Error ? error.message : String(error)
)
}
return null
}
)
// Wait for all parallel requests to complete (or fail)
const results = await Promise.allSettled(channelSearchPromises)
// Find the first successful result that contains our channel
for (const result of results) {
if (result.status === 'fulfilled' && result.value) {
const { channelInfo } = result.value
setSelectedTeamId(channelInfo.teamId!)
setSelectedChannelId(channelInfo.channelId!)
setSelectedMessage(channelInfo)
onMessageInfoChange?.(channelInfo)
return // Found the channel, exit successfully
}
}
// If we get here, the channel wasn't found in any team
logger.warn(`Channel ${channelId} not found in any accessible team`)
}
} catch (error) {
logger.error('Error restoring channel selection:', error)
} finally {
setIsLoading(false)
}
},
[selectedCredentialId, selectionType, onMessageInfoChange, workflowId]
)
// Set initial team ID if provided
useEffect(() => {
if (initialTeamId && !selectedTeamId && selectionType === 'channel') {
setSelectedTeamId(initialTeamId)
}
}, [initialTeamId, selectedTeamId, selectionType])
// Clear selection when selectionType changes to allow proper restoration
useEffect(() => {
setSelectedMessage(null)
setSelectedTeamId('')
setSelectedChannelId('')
setSelectedChatId('')
}, [selectionType])
// Fetch appropriate data on initial mount based on selectionType
useEffect(() => {
if (!initialFetchRef.current) {
fetchCredentials()
initialFetchRef.current = true
}
}, [fetchCredentials])
// Restore selection based on selectionType and value
useEffect(() => {
if (value && selectedCredentialId && !selectedMessage) {
if (selectionType === 'team') {
restoreTeamSelection(value)
} else if (selectionType === 'chat') {
restoreChatSelection(value)
} else if (selectionType === 'channel') {
restoreChannelSelection(value)
}
}
}, [
value,
selectedCredentialId,
selectedMessage,
selectionType,
restoreTeamSelection,
restoreChatSelection,
restoreChannelSelection,
])
return (
<>
<div className='space-y-2'>
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<Button
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
disabled={disabled}
>
{selectedMessage ? (
<div className='flex items-center gap-2 overflow-hidden'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='truncate font-normal'>{selectedMessage.displayName}</span>
</div>
) : (
<div className='flex items-center gap-2'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='text-muted-foreground'>
{selectionType === 'channel' && selectionStage === 'team'
? 'Select a team first'
: label}
</span>
</div>
)}
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>
<PopoverContent className='w-[300px] p-0' align='start'>
{/* Current account indicator */}
{selectedCredentialId && credentials.length > 0 && (
<div className='flex items-center justify-between border-b px-3 py-2'>
<div className='flex items-center gap-2'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='text-muted-foreground text-xs'>
{credentials.find((cred) => cred.id === selectedCredentialId)?.name ||
'Unknown'}
</span>
</div>
{credentials.length > 1 && (
<Button
variant='ghost'
size='sm'
className='h-6 px-2 text-xs'
onClick={() => setOpen(true)}
>
Switch
</Button>
)}
</div>
)}
<Command>
<CommandInput placeholder={`Search ${selectionStage}s...`} />
<CommandList>
<CommandEmpty>
{isLoading ? (
<div className='flex items-center justify-center p-4'>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='ml-2'>Loading {selectionStage}s...</span>
</div>
) : error ? (
<div className='p-4 text-center'>
<p className='text-destructive text-sm'>{error}</p>
{selectionStage === 'chat' && error.includes('teams') && (
<p className='mt-1 text-muted-foreground text-xs'>
There was an issue fetching chats. Please try again or connect a different
account.
</p>
)}
</div>
) : credentials.length === 0 ? (
<div className='p-4 text-center'>
<p className='font-medium text-sm'>No accounts connected.</p>
<p className='text-muted-foreground text-xs'>
Connect a Microsoft Teams account to{' '}
{selectionStage === 'chat'
? 'access your chats'
: selectionStage === 'channel'
? 'see your channels'
: 'continue'}
.
</p>
</div>
) : (
<div className='p-4 text-center'>
<p className='font-medium text-sm'>No {selectionStage}s found.</p>
<p className='text-muted-foreground text-xs'>
{selectionStage === 'team'
? 'Try a different account.'
: selectionStage === 'channel'
? selectedTeamId
? 'This team has no channels or you may not have access.'
: 'Please select a team first to see its channels.'
: 'Try a different account or check if you have any active chats.'}
</p>
</div>
)}
</CommandEmpty>
{/* Account selection - only show if we have multiple accounts */}
{credentials.length > 1 && (
<CommandGroup>
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
Switch Account
</div>
{credentials.map((cred) => (
<CommandItem
key={cred.id}
value={`account-${cred.id}`}
onSelect={() => {
setSelectedCredentialId(cred.id)
setOpen(false)
}}
>
<div className='flex items-center gap-2'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='font-normal'>{cred.name}</span>
</div>
{cred.id === selectedCredentialId && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
))}
</CommandGroup>
)}
{/* Display appropriate options based on selection stage */}
{renderSelectionOptions()}
{/* Connect account option - only show if no credentials */}
{credentials.length === 0 && (
<CommandGroup>
<CommandItem onSelect={handleAddCredential}>
<div className='flex items-center gap-2 text-primary'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span>Connect Microsoft Teams account</span>
</div>
</CommandItem>
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* Selection preview */}
{showPreview && selectedMessage && (
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
<div className='absolute top-2 right-2'>
<Button
variant='ghost'
size='icon'
className='h-5 w-5 hover:bg-muted'
onClick={handleClearSelection}
>
<X className='h-3 w-3' />
</Button>
</div>
<div className='flex items-center gap-3 pr-4'>
<div className='flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-muted/20'>
<MicrosoftTeamsIcon className='h-4 w-4' />
</div>
<div className='min-w-0 flex-1 overflow-hidden'>
<div className='flex items-center gap-2'>
<h4 className='truncate font-medium text-xs'>{selectedMessage.displayName}</h4>
<span className='whitespace-nowrap text-muted-foreground text-xs'>
{selectedMessage.type}
</span>
</div>
{selectedMessage.webViewLink ? (
<a
href={selectedMessage.webViewLink}
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-1 text-primary text-xs hover:underline'
onClick={(e) => e.stopPropagation()}
>
<span>Open in Microsoft Teams</span>
<ExternalLink className='h-3 w-3' />
</a>
) : (
<></>
)}
</div>
</div>
</div>
)}
</div>
{showOAuthModal && (
<OAuthRequiredModal
isOpen={showOAuthModal}
onClose={() => setShowOAuthModal(false)}
provider={provider}
toolName='Microsoft Teams'
requiredScopes={requiredScopes}
serviceId={getServiceId()}
/>
)}
</>
)
}

View File

@@ -4,17 +4,18 @@ import { useEffect, useState } from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { env } from '@/lib/env'
import type { SubBlockConfig } from '@/blocks/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import {
type ConfluenceFileInfo,
ConfluenceFileSelector,
} from './components/confluence-file-selector'
import {
type DiscordChannelInfo,
DiscordChannelSelector,
} from './components/discord-channel-selector'
import { type FileInfo, GoogleDrivePicker } from './components/google-drive-picker'
import { type JiraIssueInfo, JiraIssueSelector } from './components/jira-issue-selector'
import type { ConfluenceFileInfo } from './components/confluence-file-selector'
import { ConfluenceFileSelector } from './components/confluence-file-selector'
import type { DiscordChannelInfo } from './components/discord-channel-selector'
import { DiscordChannelSelector } from './components/discord-channel-selector'
import type { FileInfo } from './components/google-drive-picker'
import { GoogleDrivePicker } from './components/google-drive-picker'
import type { JiraIssueInfo } from './components/jira-issue-selector'
import { JiraIssueSelector } from './components/jira-issue-selector'
import type { TeamsMessageInfo } from './components/teams-message-selector'
import { TeamsMessageSelector } from './components/teams-message-selector'
interface FileSelectorInputProps {
blockId: string
@@ -24,18 +25,22 @@ interface FileSelectorInputProps {
export function FileSelectorInput({ blockId, subBlock, disabled = false }: FileSelectorInputProps) {
const { getValue, setValue } = useSubBlockStore()
const { activeWorkflowId } = useWorkflowRegistry()
const [selectedFileId, setSelectedFileId] = useState<string>('')
const [_fileInfo, setFileInfo] = useState<FileInfo | ConfluenceFileInfo | null>(null)
const [selectedIssueId, setSelectedIssueId] = useState<string>('')
const [_issueInfo, setIssueInfo] = useState<JiraIssueInfo | null>(null)
const [selectedChannelId, setSelectedChannelId] = useState<string>('')
const [_channelInfo, setChannelInfo] = useState<DiscordChannelInfo | null>(null)
const [channelInfo, setChannelInfo] = useState<DiscordChannelInfo | null>(null)
const [selectedMessageId, setSelectedMessageId] = useState<string>('')
const [messageInfo, setMessageInfo] = useState<TeamsMessageInfo | null>(null)
// Get provider-specific values
const provider = subBlock.provider || 'google-drive'
const isConfluence = provider === 'confluence'
const isJira = provider === 'jira'
const isDiscord = provider === 'discord'
const isMicrosoftTeams = provider === 'microsoft-teams'
// For Confluence and Jira, we need the domain and credentials
const domain = isConfluence || isJira ? (getValue(blockId, 'domain') as string) || '' : ''
@@ -53,11 +58,13 @@ export function FileSelectorInput({ blockId, subBlock, disabled = false }: FileS
setSelectedIssueId(value)
} else if (isDiscord) {
setSelectedChannelId(value)
} else if (isMicrosoftTeams) {
setSelectedMessageId(value)
} else {
setSelectedFileId(value)
}
}
}, [blockId, subBlock.id, getValue, isJira, isDiscord])
}, [blockId, subBlock.id, getValue, isJira, isDiscord, isMicrosoftTeams])
// Handle file selection
const handleFileChange = (fileId: string, info?: any) => {
@@ -179,6 +186,69 @@ export function FileSelectorInput({ blockId, subBlock, disabled = false }: FileS
)
}
// Handle Microsoft Teams selector
if (isMicrosoftTeams) {
// Get credential using the same pattern as other tools
const credential = (getValue(blockId, 'credential') as string) || ''
// Determine the selector type based on the subBlock ID
let selectionType: 'team' | 'channel' | 'chat' = 'team'
if (subBlock.id === 'teamId') {
selectionType = 'team'
} else if (subBlock.id === 'channelId') {
selectionType = 'channel'
} else if (subBlock.id === 'chatId') {
selectionType = 'chat'
} else {
// Fallback: look at the operation to determine the selection type
const operation = (getValue(blockId, 'operation') as string) || ''
if (operation.includes('chat')) {
selectionType = 'chat'
} else if (operation.includes('channel')) {
selectionType = 'channel'
}
}
// Get the teamId from workflow parameters for channel selector
const selectedTeamId = (getValue(blockId, 'teamId') as string) || ''
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full'>
<TeamsMessageSelector
value={selectedMessageId}
onChange={(value, info) => {
setSelectedMessageId(value)
setMessageInfo(info || null)
setValue(blockId, subBlock.id, value)
}}
provider='microsoft-teams'
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select Teams message location'}
disabled={disabled || !credential}
showPreview={true}
onMessageInfoChange={setMessageInfo}
credential={credential}
selectionType={selectionType}
initialTeamId={selectedTeamId}
workflowId={activeWorkflowId || ''}
/>
</div>
</TooltipTrigger>
{!credential && (
<TooltipContent side='top'>
<p>Please select Microsoft Teams credentials first</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
}
// Default to Google Drive picker
return (
<GoogleDrivePicker

View File

@@ -0,0 +1,180 @@
import { MicrosoftTeamsIcon } from '@/components/icons'
import type {
MicrosoftTeamsReadResponse,
MicrosoftTeamsWriteResponse,
} from '@/tools/microsoft_teams/types'
import type { BlockConfig } from '../types'
type MicrosoftTeamsResponse = MicrosoftTeamsReadResponse | MicrosoftTeamsWriteResponse
export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
type: 'microsoft_teams',
name: 'Microsoft Teams',
description: 'Read, write, and create messages',
longDescription:
'Integrate Microsoft Teams functionality to manage messages. Read content from existing messages and write to messages using OAuth authentication. Supports text content manipulation for message creation and editing.',
docsLink: 'https://docs.simstudio.ai/tools/microsoft_teams',
category: 'tools',
bgColor: '#E0E0E0',
icon: MicrosoftTeamsIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Read Chat Messages', id: 'read_chat' },
{ label: 'Write Chat Message', id: 'write_chat' },
{ label: 'Read Channel Messages', id: 'read_channel' },
{ label: 'Write Channel Message', id: 'write_channel' },
],
},
{
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
layout: 'full',
provider: 'microsoft-teams',
serviceId: 'microsoft-teams',
requiredScopes: [
'openid',
'profile',
'email',
'User.Read',
'Chat.Read',
'Chat.ReadWrite',
'Chat.ReadBasic',
'Channel.ReadBasic.All',
'ChannelMessage.Send',
'ChannelMessage.Read.All',
'Group.Read.All',
'Group.ReadWrite.All',
'Team.ReadBasic.All',
'offline_access',
],
placeholder: 'Select Microsoft account',
},
{
id: 'teamId',
title: 'Select Team',
type: 'file-selector',
layout: 'full',
provider: 'microsoft-teams',
serviceId: 'microsoft-teams',
requiredScopes: [],
placeholder: 'Select a team',
condition: { field: 'operation', value: ['read_channel', 'write_channel'] },
},
{
id: 'chatId',
title: 'Select Chat',
type: 'file-selector',
layout: 'full',
provider: 'microsoft-teams',
serviceId: 'microsoft-teams',
requiredScopes: [],
placeholder: 'Select a chat',
condition: { field: 'operation', value: ['read_chat', 'write_chat'] },
},
{
id: 'channelId',
title: 'Select Channel',
type: 'file-selector',
layout: 'full',
provider: 'microsoft-teams',
serviceId: 'microsoft-teams',
requiredScopes: [],
placeholder: 'Select a channel',
condition: { field: 'operation', value: ['read_channel', 'write_channel'] },
},
// Create-specific Fields
{
id: 'content',
title: 'Message',
type: 'long-input',
layout: 'full',
placeholder: 'Enter message content',
condition: { field: 'operation', value: ['write_chat', 'write_channel'] },
},
],
tools: {
access: [
'microsoft_teams_read_chat',
'microsoft_teams_write_chat',
'microsoft_teams_read_channel',
'microsoft_teams_write_channel',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'read_chat':
return 'microsoft_teams_read_chat'
case 'write_chat':
return 'microsoft_teams_write_chat'
case 'read_channel':
return 'microsoft_teams_read_channel'
case 'write_channel':
return 'microsoft_teams_write_channel'
default:
return 'microsoft_teams_read_chat'
}
},
params: (params) => {
const { credential, operation, ...rest } = params
// Build the parameters based on operation type
const baseParams = {
...rest,
credential,
}
// For chat operations, we need chatId
if (operation === 'read_chat' || operation === 'write_chat') {
if (!params.chatId) {
throw new Error('Chat ID is required for chat operations')
}
return {
...baseParams,
chatId: params.chatId,
}
}
// For channel operations, we need teamId and channelId
if (operation === 'read_channel' || operation === 'write_channel') {
if (!params.teamId) {
throw new Error('Team ID is required for channel operations')
}
if (!params.channelId) {
throw new Error('Channel ID is required for channel operations')
}
return {
...baseParams,
teamId: params.teamId,
channelId: params.channelId,
}
}
return baseParams
},
},
},
inputs: {
operation: { type: 'string', required: true },
credential: { type: 'string', required: true },
messageId: { type: 'string', required: true },
chatId: { type: 'string', required: true },
channelId: { type: 'string', required: true },
teamId: { type: 'string', required: true },
content: { type: 'string', required: true },
},
outputs: {
response: {
type: {
content: 'string',
metadata: 'json',
updatedContent: 'boolean',
},
},
},
}

View File

@@ -24,6 +24,7 @@ import { GoogleSearchBlock } from './blocks/google'
import { GoogleDocsBlock } from './blocks/google_docs'
import { GoogleDriveBlock } from './blocks/google_drive'
import { GoogleSheetsBlock } from './blocks/google_sheets'
// import { GuestyBlock } from './blocks/guesty'
import { ImageGeneratorBlock } from './blocks/image_generator'
import { JinaBlock } from './blocks/jina'
import { JiraBlock } from './blocks/jira'
@@ -31,6 +32,7 @@ import { LinkupBlock } from './blocks/linkup'
import { Mem0Block } from './blocks/mem0'
// import { GuestyBlock } from './blocks/guesty'
import { MemoryBlock } from './blocks/memory'
import { MicrosoftTeamsBlock } from './blocks/microsoft_teams'
import { MistralParseBlock } from './blocks/mistral_parse'
import { NotionBlock } from './blocks/notion'
import { OpenAIBlock } from './blocks/openai'
@@ -80,6 +82,7 @@ export const registry: Record<string, BlockConfig> = {
google_drive: GoogleDriveBlock,
google_search: GoogleSearchBlock,
google_sheets: GoogleSheetsBlock,
microsoft_teams: MicrosoftTeamsBlock,
// guesty: GuestyBlock,
image_generator: ImageGeneratorBlock,
jina: JinaBlock,

View File

@@ -2474,3 +2474,85 @@ export function ClayIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 23 23' {...props}>
<path fill='#f3f3f3' d='M0 0h23v23H0z' />
<path fill='#f35325' d='M1 1h10v10H1z' />
<path fill='#81bc06' d='M12 1h10v10H12z' />
<path fill='#05a6f0' d='M1 12h10v10H1z' />
<path fill='#ffba08' d='M12 12h10v10H12z' />
</svg>
)
}
export function MicrosoftTeamsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2228.833 2073.333'>
<path
fill='#5059C9'
d='M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z'
/>
<circle fill='#5059C9' cx='1943.75' cy='440.583' r='233.25' />
<circle fill='#7B83EB' cx='1218.083' cy='336.917' r='336.917' />
<path
fill='#7B83EB'
d='M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z'
/>
<path
opacity='.1'
d='M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z'
/>
<path
opacity='.2'
d='M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z'
/>
<path
opacity='.2'
d='M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z'
/>
<path
opacity='.2'
d='M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z'
/>
<path
opacity='.1'
d='M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z'
/>
<path
opacity='.2'
d='M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z'
/>
<path
opacity='.2'
d='M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z'
/>
<path
opacity='.2'
d='M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z'
/>
<linearGradient
id='a'
gradientUnits='userSpaceOnUse'
x1='198.099'
y1='1683.0726'
x2='942.2344'
y2='394.2607'
gradientTransform='matrix(1 0 0 -1 0 2075.3333)'
>
<stop offset='0' stop-color='#5a62c3' />
<stop offset='.5' stop-color='#4d55bd' />
<stop offset='1' stop-color='#3940ab' />
</linearGradient>
<path
fill='url(#a)'
d='M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z'
/>
<path
fill='#FFF'
d='M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z'
/>
</svg>
)
}

View File

@@ -114,6 +114,7 @@ export const auth = betterAuth({
'supabase',
'x',
'notion',
'microsoft',
],
},
},
@@ -365,6 +366,37 @@ export const auth = betterAuth({
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-sheets`,
},
{
providerId: 'microsoft-teams',
clientId: env.MICROSOFT_CLIENT_ID as string,
clientSecret: env.MICROSOFT_CLIENT_SECRET as string,
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
scopes: [
'openid',
'profile',
'email',
'User.Read',
'Chat.Read',
'Chat.ReadWrite',
'Chat.ReadBasic',
'Channel.ReadBasic.All',
'ChannelMessage.Send',
'ChannelMessage.Read.All',
'Group.Read.All',
'Group.ReadWrite.All',
'Team.ReadBasic.All',
'offline_access',
],
responseType: 'code',
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/microsoft-teams`,
},
// Supabase provider
{
providerId: 'supabase',

View File

@@ -93,6 +93,8 @@ export const env = createEnv({
NOTION_CLIENT_SECRET: z.string().optional(),
DISCORD_CLIENT_ID: z.string().optional(),
DISCORD_CLIENT_SECRET: z.string().optional(),
MICROSOFT_CLIENT_ID: z.string().optional(),
MICROSOFT_CLIENT_SECRET: z.string().optional(),
HUBSPOT_CLIENT_ID: z.string().optional(),
HUBSPOT_CLIENT_SECRET: z.string().optional(),
DOCKER_BUILD: z.boolean().optional(),

View File

@@ -11,6 +11,8 @@ import {
GoogleIcon,
GoogleSheetsIcon,
JiraIcon,
MicrosoftIcon,
MicrosoftTeamsIcon,
NotionIcon,
SupabaseIcon,
xIcon,
@@ -31,6 +33,7 @@ export type OAuthProvider =
| 'notion'
| 'jira'
| 'discord'
| 'microsoft'
| string
export type OAuthService =
@@ -47,7 +50,7 @@ export type OAuthService =
| 'notion'
| 'jira'
| 'discord'
| 'microsoft-teams'
// Define the interface for OAuth provider configuration
export interface OAuthProviderConfig {
id: OAuthProvider
@@ -131,6 +134,38 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'gmail',
},
microsoft: {
id: 'microsoft',
name: 'Microsoft',
icon: (props) => MicrosoftIcon(props),
services: {
'microsoft-teams': {
id: 'microsoft-teams',
name: 'Microsoft Teams',
description: 'Connect to Microsoft Teams and manage messages.',
providerId: 'microsoft-teams',
icon: (props) => MicrosoftTeamsIcon(props),
baseProviderIcon: (props) => MicrosoftIcon(props),
scopes: [
'openid',
'profile',
'email',
'User.Read',
'Chat.Read',
'Chat.ReadWrite',
'Chat.ReadBasic',
'Channel.ReadBasic.All',
'ChannelMessage.Send',
'ChannelMessage.Read.All',
'Group.Read.All',
'Group.ReadWrite.All',
'Team.ReadBasic.All',
'offline_access',
],
},
},
defaultService: 'microsoft',
},
github: {
id: 'github',
name: 'GitHub',
@@ -319,6 +354,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]
if (scopes.some((scope) => scope.includes('calendar'))) {
return 'google-calendar'
}
} else if (provider === 'microsoft-teams') {
return 'microsoft-teams'
} else if (provider === 'github') {
return 'github'
} else if (provider === 'supabase') {
@@ -464,6 +501,11 @@ export async function refreshOAuthToken(
clientSecret = env.DISCORD_CLIENT_SECRET
useBasicAuth = true
break
case 'microsoft':
tokenEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
clientId = env.MICROSOFT_CLIENT_ID
clientSecret = env.MICROSOFT_CLIENT_SECRET
break
default:
throw new Error(`Unsupported provider: ${provider}`)
}

View File

@@ -0,0 +1,9 @@
import { readChannelTool } from './read_channel'
import { readChatTool } from './read_chat'
import { writeChannelTool } from './write_channel'
import { writeChatTool } from './write_chat'
export const microsoftTeamsReadChannelTool = readChannelTool
export const microsoftTeamsWriteChannelTool = writeChannelTool
export const microsoftTeamsReadChatTool = readChatTool
export const microsoftTeamsWriteChatTool = writeChatTool

View File

@@ -0,0 +1,143 @@
import type { ToolConfig } from '../types'
import type { MicrosoftTeamsReadResponse, MicrosoftTeamsToolParams } from './types'
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',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
description: 'The ID of the team to read from',
},
channelId: {
type: 'string',
required: true,
description: 'The ID of the channel to read from',
},
},
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')
}
// URL encode the IDs to handle special characters
const encodedTeamId = encodeURIComponent(teamId)
const encodedChannelId = encodeURIComponent(channelId)
// Fetch the most recent messages from the channel
const url = `https://graph.microsoft.com/v1.0/teams/${encodedTeamId}/channels/${encodedChannelId}/messages`
return url
},
method: 'GET',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to read Microsoft Teams channel: ${errorText}`)
}
const data = await response.json()
// Microsoft Graph API returns messages in a 'value' array
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: [],
},
},
}
}
// Format the messages into a readable text
const formattedMessages = messages
.map((message: any) => {
const content = message.body?.content || 'No content'
const sender = message.from?.user?.displayName || 'Unknown sender'
const timestamp = message.createdDateTime
? new Date(message.createdDateTime).toLocaleString()
: 'Unknown time'
return `[${timestamp}] ${sender}: ${content}`
})
.join('\n\n')
// Create document metadata
const metadata = {
teamId: messages[0]?.channelIdentity?.teamId || '',
channelId: messages[0]?.channelIdentity?.channelId || '',
messageCount: messages.length,
messages: messages.map((msg: any) => ({
id: msg.id,
content: msg.body?.content || '',
sender: msg.from?.user?.displayName || 'Unknown',
timestamp: msg.createdDateTime,
messageType: msg.messageType || 'message',
})),
}
return {
success: true,
output: {
content: formattedMessages,
metadata,
},
}
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while reading Microsoft Teams channel'
},
}

View File

@@ -0,0 +1,125 @@
import type { ToolConfig } from '../types'
import type { MicrosoftTeamsReadResponse, MicrosoftTeamsToolParams } from './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',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'The access token for the Microsoft Teams API',
},
chatId: {
type: 'string',
required: true,
description: 'The ID of the chat to read from',
},
},
request: {
url: (params) => {
// Ensure chatId is valid
const chatId = params.chatId?.trim()
if (!chatId) {
throw new Error('Chat ID is required')
}
// Fetch the most recent messages from the chat
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages?$top=50&$orderby=createdDateTime desc`
},
method: 'GET',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to read Microsoft Teams chat: ${errorText}`)
}
const data = await response.json()
// Microsoft Graph API returns messages in a 'value' array
const messages = data.value || []
if (messages.length === 0) {
return {
success: true,
output: {
content: 'No messages found in this chat.',
metadata: {
chatId: '',
messageCount: 0,
messages: [],
},
},
}
}
// Format the messages into a readable text
const formattedMessages = messages
.map((message: any) => {
const content = message.body?.content || 'No content'
const sender = message.from?.user?.displayName || 'Unknown sender'
const timestamp = message.createdDateTime
? new Date(message.createdDateTime).toLocaleString()
: 'Unknown time'
return `[${timestamp}] ${sender}: ${content}`
})
.join('\n\n')
// Create document metadata
const metadata = {
chatId: messages[0]?.chatId || '',
messageCount: messages.length,
messages: messages.map((msg: any) => ({
id: msg.id,
content: msg.body?.content || '',
sender: msg.from?.user?.displayName || 'Unknown',
timestamp: msg.createdDateTime,
messageType: msg.messageType || 'message',
})),
}
return {
success: true,
output: {
content: formattedMessages,
metadata,
},
}
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while reading Microsoft Teams chat'
},
}

View File

@@ -0,0 +1,43 @@
import type { ToolResponse } from '../types'
export interface MicrosoftTeamsMetadata {
messageId?: string
channelId?: string
teamId?: string
chatId?: string
content?: string
createdTime?: string
modifiedTime?: string
url?: string
messageCount?: number
messages?: Array<{
id: string
content: string
sender: string
timestamp: string
messageType: string
}>
}
export interface MicrosoftTeamsReadResponse extends ToolResponse {
output: {
content: string
metadata: MicrosoftTeamsMetadata
}
}
export interface MicrosoftTeamsWriteResponse extends ToolResponse {
output: {
updatedContent: boolean
metadata: MicrosoftTeamsMetadata
}
}
export interface MicrosoftTeamsToolParams {
accessToken: string
messageId?: string
chatId?: string
channelId?: string
teamId?: string
content?: string
}

View File

@@ -0,0 +1,130 @@
import type { ToolConfig } from '../types'
import type { MicrosoftTeamsToolParams, MicrosoftTeamsWriteResponse } from './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',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'The access token for the Microsoft Teams API',
},
teamId: {
type: 'string',
required: true,
description: 'The ID of the team to write to',
},
channelId: {
type: 'string',
required: true,
description: 'The ID of the channel to write to',
},
content: {
type: 'string',
required: true,
description: 'The content to write to the channel',
},
},
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')
}
// URL encode the IDs to handle special characters
const encodedTeamId = encodeURIComponent(teamId)
const encodedChannelId = encodeURIComponent(channelId)
// Send a message to a channel
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')
}
// 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) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to write Microsoft Teams channel message: ${errorText}`)
}
const data = await response.json()
// Create document metadata from the response
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,
},
}
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while writing Microsoft Teams channel message'
},
}

View File

@@ -0,0 +1,112 @@
import type { ToolConfig } from '../types'
import type { MicrosoftTeamsToolParams, MicrosoftTeamsWriteResponse } from './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',
oauth: {
required: true,
provider: 'microsoft-teams',
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'The access token for the Microsoft Teams API',
},
chatId: {
type: 'string',
required: true,
description: 'The ID of the chat to write to',
},
content: {
type: 'string',
required: true,
description: 'The content to write to the message',
},
},
request: {
url: (params) => {
// Ensure chatId is valid
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`
},
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')
}
// Microsoft Teams API expects this specific format
const requestBody = {
body: {
contentType: 'text',
content: params.content,
},
}
return requestBody
},
},
transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to write Microsoft Teams message: ${errorText}`)
}
const data = await response.json()
// Create document metadata from the response
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,
},
}
},
transformError: (error) => {
// If it's an Error instance with a message, use that
if (error instanceof Error) {
return error.message
}
// If it's an object with an error or message property
if (typeof error === 'object' && error !== null) {
if (error.error) {
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while writing Microsoft Teams message'
},
}

View File

@@ -48,6 +48,12 @@ import { jiraBulkRetrieveTool, jiraRetrieveTool, jiraUpdateTool, jiraWriteTool }
import { linkupSearchTool } from './linkup'
import { mem0AddMemoriesTool, mem0GetMemoriesTool, mem0SearchMemoriesTool } from './mem0'
import { memoryAddTool, memoryDeleteTool, memoryGetAllTool, memoryGetTool } from './memory'
import {
microsoftTeamsReadChannelTool,
microsoftTeamsReadChatTool,
microsoftTeamsWriteChannelTool,
microsoftTeamsWriteChatTool,
} from './microsoft_teams'
import { mistralParserTool } from './mistral'
import { notionReadTool, notionWriteTool } from './notion'
import { imageTool, embeddingsTool as openAIEmbeddings } from './openai'
@@ -173,4 +179,8 @@ export const tools: Record<string, ToolConfig> = {
discord_get_server: discordGetServerTool,
discord_get_user: discordGetUserTool,
openai_image: imageTool,
microsoft_teams_read_chat: microsoftTeamsReadChatTool,
microsoft_teams_write_chat: microsoftTeamsWriteChatTool,
microsoft_teams_read_channel: microsoftTeamsReadChannelTool,
microsoft_teams_write_channel: microsoftTeamsWriteChannelTool,
}