Skip to content
Merged
16 changes: 3 additions & 13 deletions apps/sim/app/(landing)/contributors/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ export default function ContributorsPage() {
<ResponsiveContainer width='100%' height={300} className='sm:!h-[400px]'>
<BarChart
data={filteredContributors?.slice(0, showAllContributors ? undefined : 10)}
margin={{ top: 10, right: 5, bottom: 50, left: 5 }}
margin={{ top: 10, right: 5, bottom: 45, left: 5 }}
className='sm:!mx-2.5 sm:!mb-2.5'
>
<XAxis
Expand Down Expand Up @@ -461,21 +461,11 @@ export default function ContributorsPage() {
</AvatarFallback>
</Avatar>
</foreignObject>
<text
x='0'
y='40'
textAnchor='middle'
className='fill-neutral-400 text-[10px] sm:text-xs'
>
{payload.value.length > 6
? `${payload.value.slice(0, 6)}...`
: payload.value}
</text>
</g>
)
}}
height={60}
className='sm:!h-[80px] text-neutral-400'
height={50}
className='sm:!h-[60px] text-neutral-400'
/>
<YAxis
stroke='currentColor'
Expand Down
12 changes: 9 additions & 3 deletions apps/sim/app/api/chat/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { NextRequest } from 'next/server'
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { env } from '@/lib/env'

describe('Chat API Route', () => {
const mockSelect = vi.fn()
Expand Down Expand Up @@ -270,12 +269,19 @@ describe('Chat API Route', () => {
}),
}))

// Mock environment variables
vi.doMock('@/lib/env', () => ({
env: {
NODE_ENV: 'development',
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
},
}))

vi.stubGlobal('process', {
...process,
env: {
...env,
...process.env,
NODE_ENV: 'development',
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
},
})

Expand Down
20 changes: 17 additions & 3 deletions apps/sim/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,23 @@ export async function POST(request: NextRequest) {
// Return successful response with chat URL
// Check if we're in development or production
const isDevelopment = env.NODE_ENV === 'development'
const chatUrl = isDevelopment
? `http://${subdomain}.localhost:3000`
: `https://${subdomain}.simstudio.ai`
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'

let chatUrl: string
if (isDevelopment) {
try {
const url = new URL(baseUrl)
chatUrl = `${url.protocol}//${subdomain}.${url.host}`
} catch (error) {
logger.warn('Failed to parse baseUrl, falling back to localhost:', {
baseUrl,
error: error instanceof Error ? error.message : 'Unknown error',
})
chatUrl = `http://${subdomain}.localhost:3000`
}
} else {
chatUrl = `https://${subdomain}.simstudio.ai`
}

logger.info(`Chat "${title}" deployed successfully at ${chatUrl}`)

Expand Down
153 changes: 153 additions & 0 deletions apps/sim/app/api/tools/slack/channels/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('SlackChannelsAPI')

interface SlackChannel {
id: string
name: string
is_private: boolean
is_archived: boolean
is_member: boolean
}

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 })
}

let accessToken: string
let isBotToken = false

if (credential.startsWith('xoxb-')) {
accessToken = credential
isBotToken = true
logger.info('Using direct bot token for Slack API')
} else {
const userId = session?.user?.id || ''
Comment thread
waleedlatif1 marked this conversation as resolved.
if (!userId) {
logger.error('No user ID found in session')
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}

const resolvedToken = await refreshAccessTokenIfNeeded(credential, userId, workflowId)
if (!resolvedToken) {
logger.error('Failed to get access token', { credentialId: credential, userId })
return NextResponse.json(
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}
accessToken = resolvedToken
logger.info('Using OAuth token for Slack API')
}

let data
try {
data = await fetchSlackChannels(accessToken, true)
logger.info('Successfully fetched channels including private channels')
} catch (error) {
if (isBotToken) {
logger.warn(
'Failed to fetch private channels with bot token, falling back to public channels only:',
(error as Error).message
)
try {
data = await fetchSlackChannels(accessToken, false)
logger.info('Successfully fetched public channels only')
} catch (fallbackError) {
logger.error('Failed to fetch channels even with public-only fallback:', fallbackError)
return NextResponse.json(
{ error: `Slack API error: ${(fallbackError as Error).message}` },
{ status: 400 }
)
}
} else {
logger.error('Slack API error with OAuth token:', error)
return NextResponse.json(
{ error: `Slack API error: ${(error as Error).message}` },
{ status: 400 }
)
}
}

// Filter to channels the bot can access and format the response
const channels = (data.channels || [])
.filter((channel: SlackChannel) => {
const canAccess = !channel.is_archived && (channel.is_member || !channel.is_private)

if (!canAccess) {
logger.debug(
`Filtering out channel: ${channel.name} (archived: ${channel.is_archived}, private: ${channel.is_private}, member: ${channel.is_member})`
)
}

return canAccess
})
.map((channel: SlackChannel) => ({
id: channel.id,
name: channel.name,
isPrivate: channel.is_private,
}))

logger.info(`Successfully fetched ${channels.length} Slack channels`, {
total: data.channels?.length || 0,
private: channels.filter((c: { isPrivate: boolean }) => c.isPrivate).length,
public: channels.filter((c: { isPrivate: boolean }) => !c.isPrivate).length,
tokenType: isBotToken ? 'bot_token' : 'oauth',
})
return NextResponse.json({ channels })
} catch (error) {
logger.error('Error processing Slack channels request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Slack channels', details: (error as Error).message },
{ status: 500 }
Comment thread
waleedlatif1 marked this conversation as resolved.
)
}
}

async function fetchSlackChannels(accessToken: string, includePrivate = true) {
const url = new URL('https://slack.com/api/conversations.list')

if (includePrivate) {
url.searchParams.append('types', 'public_channel,private_channel')
} else {
url.searchParams.append('types', 'public_channel')
}

url.searchParams.append('exclude_archived', 'true')
url.searchParams.append('limit', '200')

const response = await fetch(url.toString(), {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})

if (!response.ok) {
throw new Error(`Slack API error: ${response.status} ${response.statusText}`)
}

const data = await response.json()

if (!data.ok) {
throw new Error(data.error || 'Failed to fetch channels')
}

return data
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client'

import { useEffect, useState } from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import type { SubBlockConfig } from '@/blocks/types'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { type SlackChannelInfo, SlackChannelSelector } from './components/slack-channel-selector'

interface ChannelSelectorInputProps {
blockId: string
subBlock: SubBlockConfig
disabled?: boolean
onChannelSelect?: (channelId: string) => void
credential?: string // Optional credential override
}

export function ChannelSelectorInput({
blockId,
subBlock,
disabled = false,
onChannelSelect,
credential: providedCredential,
}: ChannelSelectorInputProps) {
const { getValue, setValue } = useSubBlockStore()
const [selectedChannelId, setSelectedChannelId] = useState<string>('')
const [_channelInfo, setChannelInfo] = useState<SlackChannelInfo | null>(null)

// Get provider-specific values
const provider = subBlock.provider || 'slack'
const isSlack = provider === 'slack'

// Get the credential for the provider - use provided credential or fall back to store
const authMethod = getValue(blockId, 'authMethod') as string
const botToken = getValue(blockId, 'botToken') as string

let credential: string
if (providedCredential) {
credential = providedCredential
} else if (authMethod === 'bot_token' && botToken) {
credential = botToken
} else {
credential = (getValue(blockId, 'credential') as string) || ''
}

// Get the current value from the store
useEffect(() => {
const value = getValue(blockId, subBlock.id)
if (value && typeof value === 'string') {
setSelectedChannelId(value)
}
}, [blockId, subBlock.id, getValue])

// Handle channel selection
const handleChannelChange = (channelId: string, info?: SlackChannelInfo) => {
setSelectedChannelId(channelId)
setChannelInfo(info || null)
setValue(blockId, subBlock.id, channelId)
onChannelSelect?.(channelId)
}

// Render Slack channel selector
if (isSlack) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full'>
<SlackChannelSelector
value={selectedChannelId}
onChange={(channelId: string, channelInfo?: SlackChannelInfo) => {
handleChannelChange(channelId, channelInfo)
}}
credential={credential}
label={subBlock.placeholder || 'Select Slack channel'}
disabled={disabled || !credential}
/>
</div>
</TooltipTrigger>
{!credential && (
<TooltipContent side='top'>
<p>Please select a Slack account or enter a bot token first</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
}

// Default fallback for unsupported providers
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full rounded border border-dashed p-4 text-center text-muted-foreground text-sm'>
Channel selector not supported for provider: {provider}
</div>
</TooltipTrigger>
<TooltipContent side='top'>
<p>This channel selector is not yet implemented for {provider}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
Loading