Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions apps/sim/app/api/tools/pinterest/boards/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('PinterestBoardsAPI')

interface PinterestBoard {
id: string
name: string
description?: string
privacy?: string
owner?: {
username: string
}
}

export async function POST(request: Request) {
try {
const requestId = generateRequestId()
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 })
}

const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})

if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)

if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}

logger.info('Fetching Pinterest boards', { requestId })

const response = await fetch('https://api.pinterest.com/v5/boards', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})

if (!response.ok) {
const errorText = await response.text()
logger.error('Pinterest API error', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{ error: `Pinterest API error: ${response.status} - ${response.statusText}` },
{ status: response.status }
)
}

const data = await response.json()
const boards = (data.items || []).map((board: PinterestBoard) => ({
id: board.id,
name: board.name,
description: board.description,
privacy: board.privacy,
}))

logger.info(`Successfully fetched ${boards.length} Pinterest boards`, { requestId })
return NextResponse.json({ items: boards })
} catch (error) {
logger.error('Error processing Pinterest boards request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Pinterest boards', details: (error as Error).message },
{ status: 500 }
)
}
}
107 changes: 107 additions & 0 deletions apps/sim/blocks/blocks/pinterest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { PinterestIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { PinterestResponse } from '@/tools/pinterest/types'

export const PinterestBlock: BlockConfig<PinterestResponse> = {
type: 'pinterest',
name: 'Pinterest',
description: 'Create pins on your Pinterest boards',
authMode: AuthMode.OAuth,
longDescription: 'Create and share pins on Pinterest. Post images with titles, descriptions, and links to your boards.',
docsLink: 'https://docs.sim.ai/tools/pinterest',
category: 'tools',
bgColor: '#E60023',
icon: PinterestIcon,
subBlocks: [
{
id: 'credential',
title: 'Pinterest Account',
type: 'oauth-input',
serviceId: 'pinterest',
requiredScopes: ['boards:read', 'boards:write', 'pins:read', 'pins:write'],
placeholder: 'Select Pinterest account',
required: true,
},
{
id: 'board_id',
title: 'Board',
type: 'file-selector',
canonicalParamId: 'board_id',
serviceId: 'pinterest',
placeholder: 'Select a Pinterest board',
dependsOn: ['credential'],
required: true,
},
{
id: 'title',
title: 'Pin Title',
type: 'short-input',
placeholder: 'Enter pin title',
required: true,
},
{
id: 'description',
title: 'Pin Description',
type: 'long-input',
placeholder: 'Enter pin description',
required: true,
},
{
id: 'media_url',
title: 'Image URL',
type: 'short-input',
placeholder: 'Enter image URL',
required: true,
},
{
id: 'link',
title: 'Destination Link',
type: 'short-input',
placeholder: 'Enter destination URL (optional)',
required: false,
},
{
id: 'alt_text',
title: 'Alt Text',
type: 'short-input',
placeholder: 'Enter alt text for accessibility (optional)',
required: false,
},
],
tools: {
access: ['pinterest_create_pin'],
config: {
tool: () => 'pinterest_create_pin',
params: (inputs) => {
const { credential, ...rest } = inputs

return {
accessToken: credential,
board_id: rest.board_id,
title: rest.title,
description: rest.description,
media_url: rest.media_url,
link: rest.link,
alt_text: rest.alt_text,
}
},
},
},
inputs: {
credential: { type: 'string', description: 'Pinterest access token' },
board_id: { type: 'string', description: 'Board ID where the pin will be created' },
title: { type: 'string', description: 'Pin title' },
description: { type: 'string', description: 'Pin description' },
media_url: { type: 'string', description: 'Image URL for the pin' },
link: { type: 'string', description: 'Destination link when pin is clicked' },
alt_text: { type: 'string', description: 'Alt text for accessibility' },
},
outputs: {
success: { type: 'boolean', description: 'Whether the pin was created successfully' },
pin: { type: 'json', description: 'Full pin object' },
pin_id: { type: 'string', description: 'ID of the created pin' },
pin_url: { type: 'string', description: 'URL of the created pin' },
error: { type: 'string', description: 'Error message if operation failed' },
},
}
2 changes: 2 additions & 0 deletions apps/sim/blocks/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
import { LinearBlock } from '@/blocks/blocks/linear'
import { LinkedInBlock } from '@/blocks/blocks/linkedin'
import { LinkupBlock } from '@/blocks/blocks/linkup'
import { PinterestBlock } from '@/blocks/blocks/pinterest'
import { MailchimpBlock } from '@/blocks/blocks/mailchimp'
import { MailgunBlock } from '@/blocks/blocks/mailgun'
import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger'
Expand Down Expand Up @@ -205,6 +206,7 @@ export const registry: Record<string, BlockConfig> = {
linear: LinearBlock,
linkedin: LinkedInBlock,
linkup: LinkupBlock,
pinterest: PinterestBlock,
mailchimp: MailchimpBlock,
mailgun: MailgunBlock,
manual_trigger: ManualTriggerBlock,
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4302,6 +4302,12 @@ export function SpotifyIcon(props: SVGProps<SVGSVGElement>) {
)
}

export const PinterestIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} viewBox='0 0 24 24' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
<path d='M12 0C5.373 0 0 5.372 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738.098.119.112.224.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.631-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12 0-6.628-5.373-12-12-12z' />
</svg>
)

export function GrainIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 34' fill='none'>
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export default {
dbCredentials: {
url: env.DATABASE_URL,
},
} satisfies Config
} satisfies Config
31 changes: 31 additions & 0 deletions apps/sim/hooks/selectors/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,37 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
}))
},
},
'pinterest.boards': {
key: 'pinterest.boards',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'pinterest.boards',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const body = JSON.stringify({
credential: context.credentialId,
workflowId: context.workflowId,
})
const data = await fetchJson<{ items: { id: string; name: string; description?: string; privacy?: string }[] }>(
'/api/tools/pinterest/boards',
{
method: 'POST',
body,
}
)
return (data.items || []).map((board) => ({
id: board.id,
label: board.name,
meta: {
description: board.description,
privacy: board.privacy,
},
}))
},
},
}

export function getSelectorDefinition(key: SelectorKey): SelectorDefinition {
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/hooks/selectors/resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ function resolveFileSelector(
return { key: 'webflow.items', context, allowSearch: true }
}
return { key: null, context, allowSearch: true }
case 'pinterest':
return { key: 'pinterest.boards', context, allowSearch: true }
default:
return { key: null, context, allowSearch: true }
}
Expand Down
1 change: 1 addition & 0 deletions apps/sim/hooks/selectors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type SelectorKey =
| 'webflow.sites'
| 'webflow.collections'
| 'webflow.items'
| 'pinterest.boards'

export interface SelectorOption {
id: string
Expand Down
Loading