Skip to content

Commit b041e61

Browse files
committed
added pinterest block and tool
1 parent b304233 commit b041e61

File tree

17 files changed

+609
-1
lines changed

17 files changed

+609
-1
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { NextResponse } from 'next/server'
2+
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
3+
import { generateRequestId } from '@/lib/core/utils/request'
4+
import { createLogger } from '@/lib/logs/console/logger'
5+
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
6+
7+
export const dynamic = 'force-dynamic'
8+
9+
const logger = createLogger('PinterestBoardsAPI')
10+
11+
interface PinterestBoard {
12+
id: string
13+
name: string
14+
description?: string
15+
privacy?: string
16+
owner?: {
17+
username: string
18+
}
19+
}
20+
21+
export async function POST(request: Request) {
22+
try {
23+
const requestId = generateRequestId()
24+
const body = await request.json()
25+
const { credential, workflowId } = body
26+
27+
if (!credential) {
28+
logger.error('Missing credential in request')
29+
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
30+
}
31+
32+
const authz = await authorizeCredentialUse(request as any, {
33+
credentialId: credential,
34+
workflowId,
35+
})
36+
37+
if (!authz.ok || !authz.credentialOwnerUserId) {
38+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
39+
}
40+
41+
const accessToken = await refreshAccessTokenIfNeeded(
42+
credential,
43+
authz.credentialOwnerUserId,
44+
requestId
45+
)
46+
47+
if (!accessToken) {
48+
logger.error('Failed to get access token', {
49+
credentialId: credential,
50+
userId: authz.credentialOwnerUserId,
51+
})
52+
return NextResponse.json(
53+
{
54+
error: 'Could not retrieve access token',
55+
authRequired: true,
56+
},
57+
{ status: 401 }
58+
)
59+
}
60+
61+
logger.info('Fetching Pinterest boards', { requestId })
62+
63+
const response = await fetch('https://api.pinterest.com/v5/boards', {
64+
headers: {
65+
Authorization: `Bearer ${accessToken}`,
66+
'Content-Type': 'application/json',
67+
},
68+
})
69+
70+
if (!response.ok) {
71+
const errorText = await response.text()
72+
logger.error('Pinterest API error', {
73+
status: response.status,
74+
statusText: response.statusText,
75+
error: errorText,
76+
})
77+
return NextResponse.json(
78+
{ error: `Pinterest API error: ${response.status} - ${response.statusText}` },
79+
{ status: response.status }
80+
)
81+
}
82+
83+
const data = await response.json()
84+
const boards = (data.items || []).map((board: PinterestBoard) => ({
85+
id: board.id,
86+
name: board.name,
87+
description: board.description,
88+
privacy: board.privacy,
89+
}))
90+
91+
logger.info(`Successfully fetched ${boards.length} Pinterest boards`, { requestId })
92+
return NextResponse.json({ items: boards })
93+
} catch (error) {
94+
logger.error('Error processing Pinterest boards request:', error)
95+
return NextResponse.json(
96+
{ error: 'Failed to retrieve Pinterest boards', details: (error as Error).message },
97+
{ status: 500 }
98+
)
99+
}
100+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { PinterestIcon } from '@/components/icons'
2+
import type { BlockConfig } from '@/blocks/types'
3+
import { AuthMode } from '@/blocks/types'
4+
import type { PinterestResponse } from '@/tools/pinterest/types'
5+
6+
export const PinterestBlock: BlockConfig<PinterestResponse> = {
7+
type: 'pinterest',
8+
name: 'Pinterest',
9+
description: 'Create pins on your Pinterest boards',
10+
authMode: AuthMode.OAuth,
11+
longDescription: 'Create and share pins on Pinterest. Post images with titles, descriptions, and links to your boards.',
12+
docsLink: 'https://docs.sim.ai/tools/pinterest',
13+
category: 'tools',
14+
bgColor: '#E60023',
15+
icon: PinterestIcon,
16+
subBlocks: [
17+
{
18+
id: 'credential',
19+
title: 'Pinterest Account',
20+
type: 'oauth-input',
21+
serviceId: 'pinterest',
22+
requiredScopes: ['boards:read', 'boards:write', 'pins:read', 'pins:write'],
23+
placeholder: 'Select Pinterest account',
24+
required: true,
25+
},
26+
{
27+
id: 'board_id',
28+
title: 'Board ID',
29+
type: 'short-input',
30+
placeholder: 'Enter board ID (e.g., 1234567890)',
31+
required: true,
32+
},
33+
{
34+
id: 'title',
35+
title: 'Pin Title',
36+
type: 'short-input',
37+
placeholder: 'Enter pin title',
38+
required: true,
39+
},
40+
{
41+
id: 'description',
42+
title: 'Pin Description',
43+
type: 'long-input',
44+
placeholder: 'Enter pin description',
45+
required: true,
46+
},
47+
{
48+
id: 'media_url',
49+
title: 'Image URL',
50+
type: 'short-input',
51+
placeholder: 'Enter image URL',
52+
required: true,
53+
},
54+
{
55+
id: 'link',
56+
title: 'Destination Link',
57+
type: 'short-input',
58+
placeholder: 'Enter destination URL (optional)',
59+
required: false,
60+
},
61+
{
62+
id: 'alt_text',
63+
title: 'Alt Text',
64+
type: 'short-input',
65+
placeholder: 'Enter alt text for accessibility (optional)',
66+
required: false,
67+
},
68+
],
69+
tools: {
70+
access: ['pinterest_create_pin'],
71+
config: {
72+
tool: () => 'pinterest_create_pin',
73+
params: (inputs) => {
74+
const { credential, ...rest } = inputs
75+
76+
return {
77+
accessToken: credential,
78+
board_id: rest.board_id,
79+
title: rest.title,
80+
description: rest.description,
81+
media_url: rest.media_url,
82+
link: rest.link,
83+
alt_text: rest.alt_text,
84+
}
85+
},
86+
},
87+
},
88+
inputs: {
89+
credential: { type: 'string', description: 'Pinterest access token' },
90+
board_id: { type: 'string', description: 'Board ID where the pin will be created' },
91+
title: { type: 'string', description: 'Pin title' },
92+
description: { type: 'string', description: 'Pin description' },
93+
media_url: { type: 'string', description: 'Image URL for the pin' },
94+
link: { type: 'string', description: 'Destination link when pin is clicked' },
95+
alt_text: { type: 'string', description: 'Alt text for accessibility' },
96+
},
97+
outputs: {
98+
success: { type: 'boolean', description: 'Whether the pin was created successfully' },
99+
pin: { type: 'json', description: 'Full pin object' },
100+
pin_id: { type: 'string', description: 'ID of the created pin' },
101+
pin_url: { type: 'string', description: 'URL of the created pin' },
102+
error: { type: 'string', description: 'Error message if operation failed' },
103+
},
104+
}

apps/sim/blocks/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
5959
import { LinearBlock } from '@/blocks/blocks/linear'
6060
import { LinkedInBlock } from '@/blocks/blocks/linkedin'
6161
import { LinkupBlock } from '@/blocks/blocks/linkup'
62+
import { PinterestBlock } from '@/blocks/blocks/pinterest'
6263
import { MailchimpBlock } from '@/blocks/blocks/mailchimp'
6364
import { MailgunBlock } from '@/blocks/blocks/mailgun'
6465
import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger'
@@ -203,6 +204,7 @@ export const registry: Record<string, BlockConfig> = {
203204
linear: LinearBlock,
204205
linkedin: LinkedInBlock,
205206
linkup: LinkupBlock,
207+
pinterest: PinterestBlock,
206208
mailchimp: MailchimpBlock,
207209
mailgun: MailgunBlock,
208210
manual_trigger: ManualTriggerBlock,

apps/sim/components/icons.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4289,6 +4289,12 @@ export function SpotifyIcon(props: SVGProps<SVGSVGElement>) {
42894289
)
42904290
}
42914291

4292+
export const PinterestIcon = (props: SVGProps<SVGSVGElement>) => (
4293+
<svg {...props} viewBox='0 0 24 24' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
4294+
<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' />
4295+
</svg>
4296+
)
4297+
42924298
export function GrainIcon(props: SVGProps<SVGSVGElement>) {
42934299
return (
42944300
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 34' fill='none'>

apps/sim/drizzle.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ export default {
88
dbCredentials: {
99
url: env.DATABASE_URL,
1010
},
11-
} satisfies Config
11+
} satisfies Config

apps/sim/hooks/selectors/registry.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,37 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
791791
}))
792792
},
793793
},
794+
'pinterest.boards': {
795+
key: 'pinterest.boards',
796+
staleTime: SELECTOR_STALE,
797+
getQueryKey: ({ context }: SelectorQueryArgs) => [
798+
'selectors',
799+
'pinterest.boards',
800+
context.credentialId ?? 'none',
801+
],
802+
enabled: ({ context }) => Boolean(context.credentialId),
803+
fetchList: async ({ context }: SelectorQueryArgs) => {
804+
const body = JSON.stringify({
805+
credential: context.credentialId,
806+
workflowId: context.workflowId,
807+
})
808+
const data = await fetchJson<{ items: { id: string; name: string; description?: string; privacy?: string }[] }>(
809+
'/api/tools/pinterest/boards',
810+
{
811+
method: 'POST',
812+
body,
813+
}
814+
)
815+
return (data.items || []).map((board) => ({
816+
id: board.id,
817+
label: board.name,
818+
meta: {
819+
description: board.description,
820+
privacy: board.privacy,
821+
},
822+
}))
823+
},
824+
},
794825
}
795826

796827
export function getSelectorDefinition(key: SelectorKey): SelectorDefinition {

apps/sim/hooks/selectors/resolution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ function resolveFileSelector(
120120
return { key: 'webflow.items', context, allowSearch: true }
121121
}
122122
return { key: null, context, allowSearch: true }
123+
case 'pinterest':
124+
return { key: 'pinterest.boards', context, allowSearch: true }
123125
default:
124126
return { key: null, context, allowSearch: true }
125127
}

apps/sim/hooks/selectors/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type SelectorKey =
2727
| 'webflow.sites'
2828
| 'webflow.collections'
2929
| 'webflow.items'
30+
| 'pinterest.boards'
3031

3132
export interface SelectorOption {
3233
id: string

apps/sim/lib/auth/auth.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export const auth = betterAuth({
223223
'hubspot',
224224
'linkedin',
225225
'spotify',
226+
'pinterest',
226227

227228
// Common SSO provider patterns
228229
...SSO_TRUSTED_PROVIDERS,
@@ -1704,6 +1705,77 @@ export const auth = betterAuth({
17041705
},
17051706
},
17061707

1708+
// Pinterest provider
1709+
{
1710+
providerId: 'pinterest',
1711+
clientId: env.PINTEREST_CLIENT_ID as string,
1712+
clientSecret: env.PINTEREST_CLIENT_SECRET as string,
1713+
authorizationUrl: 'https://www.pinterest.com/oauth/',
1714+
tokenUrl: 'https://api.pinterest.com/v5/oauth/token',
1715+
userInfoUrl: 'https://api.pinterest.com/v5/user_account',
1716+
scopes: ['boards:read', 'boards:write', 'pins:read', 'pins:write'],
1717+
responseType: 'code',
1718+
authentication: 'basic',
1719+
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/pinterest`,
1720+
getUserInfo: async (tokens) => {
1721+
try {
1722+
logger.info('Fetching Pinterest user profile', {
1723+
hasAccessToken: !!tokens.accessToken,
1724+
})
1725+
1726+
const response = await fetch('https://api.pinterest.com/v5/user_account', {
1727+
headers: {
1728+
Authorization: `Bearer ${tokens.accessToken}`,
1729+
'Content-Type': 'application/json',
1730+
},
1731+
})
1732+
1733+
if (!response.ok) {
1734+
const errorBody = await response.text()
1735+
logger.error('Failed to fetch Pinterest user info', {
1736+
status: response.status,
1737+
statusText: response.statusText,
1738+
body: errorBody,
1739+
})
1740+
1741+
// Pinterest might not require user info - return minimal data
1742+
return {
1743+
id: `pinterest_${Date.now()}`,
1744+
name: 'Pinterest User',
1745+
email: `pinterest_${Date.now()}@pinterest.user`,
1746+
emailVerified: true,
1747+
createdAt: new Date(),
1748+
updatedAt: new Date(),
1749+
}
1750+
}
1751+
1752+
const profile = await response.json()
1753+
logger.info('Pinterest profile fetched successfully', { profile })
1754+
1755+
return {
1756+
id: profile.username || profile.id || `pinterest_${Date.now()}`,
1757+
name: profile.username || profile.business_name || 'Pinterest User',
1758+
email: `${profile.username || profile.id}@pinterest.user`,
1759+
emailVerified: true,
1760+
image: profile.profile_image || undefined,
1761+
createdAt: new Date(),
1762+
updatedAt: new Date(),
1763+
}
1764+
} catch (error) {
1765+
logger.error('Error in Pinterest getUserInfo:', { error })
1766+
// Return fallback user info instead of null
1767+
return {
1768+
id: `pinterest_${Date.now()}`,
1769+
name: 'Pinterest User',
1770+
email: `pinterest_${Date.now()}@pinterest.user`,
1771+
emailVerified: true,
1772+
createdAt: new Date(),
1773+
updatedAt: new Date(),
1774+
}
1775+
}
1776+
},
1777+
},
1778+
17071779
// Zoom provider
17081780
{
17091781
providerId: 'zoom',

apps/sim/lib/core/config/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ export const env = createEnv({
239239
WORDPRESS_CLIENT_SECRET: z.string().optional(), // WordPress.com OAuth client secret
240240
SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID
241241
SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret
242+
PINTEREST_CLIENT_ID: z.string().optional(), // Pinterest OAuth client ID
243+
PINTEREST_CLIENT_SECRET: z.string().optional(), // Pinterest OAuth client secret
242244

243245
// E2B Remote Code Execution
244246
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution

0 commit comments

Comments
 (0)