mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
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:
@@ -24,6 +24,7 @@
|
||||
"linkup",
|
||||
"mem0",
|
||||
"memory",
|
||||
"microsoft_teams",
|
||||
"notion",
|
||||
"openai",
|
||||
"perplexity",
|
||||
|
||||
100
apps/docs/content/docs/tools/microsoft_teams.mdx
Normal file
100
apps/docs/content/docs/tools/microsoft_teams.mdx
Normal 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`
|
||||
121
apps/sim/app/api/auth/oauth/microsoft-teams/channels/route.ts
Normal file
121
apps/sim/app/api/auth/oauth/microsoft-teams/channels/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
221
apps/sim/app/api/auth/oauth/microsoft-teams/chats/route.ts
Normal file
221
apps/sim/app/api/auth/oauth/microsoft-teams/chats/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
113
apps/sim/app/api/auth/oauth/microsoft-teams/teams/route.ts
Normal file
113
apps/sim/app/api/auth/oauth/microsoft-teams/teams/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
180
apps/sim/blocks/blocks/microsoft_teams.ts
Normal file
180
apps/sim/blocks/blocks/microsoft_teams.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
9
apps/sim/tools/microsoft_teams/index.ts
Normal file
9
apps/sim/tools/microsoft_teams/index.ts
Normal 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
|
||||
143
apps/sim/tools/microsoft_teams/read_channel.ts
Normal file
143
apps/sim/tools/microsoft_teams/read_channel.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
125
apps/sim/tools/microsoft_teams/read_chat.ts
Normal file
125
apps/sim/tools/microsoft_teams/read_chat.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
43
apps/sim/tools/microsoft_teams/types.ts
Normal file
43
apps/sim/tools/microsoft_teams/types.ts
Normal 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
|
||||
}
|
||||
130
apps/sim/tools/microsoft_teams/write_channel.ts
Normal file
130
apps/sim/tools/microsoft_teams/write_channel.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
112
apps/sim/tools/microsoft_teams/write_chat.ts
Normal file
112
apps/sim/tools/microsoft_teams/write_chat.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user