Skip to content
Merged
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
6 changes: 4 additions & 2 deletions apps/docs/content/docs/en/integrations/buffer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Create a post in Buffer for a channel — add it to the queue, share it immediat
| `schedulingType` | string | No | How the post publishes: automatic \(Buffer publishes it, default\) or notification \(you get a mobile reminder\) |
| `dueAt` | string | No | Publish time as an ISO 8601 timestamp \(required when mode is customScheduled\) |
| `saveToDraft` | boolean | No | Save the post as a draft instead of scheduling it |
| `media` | file | No | Image or video to attach — an uploaded file, a file reference from a previous block, or a publicly accessible URL |
| `media` | file | No | Image or video to attach — an uploaded file, a file reference from a previous block, or a publicly accessible URL. Buffer downloads the media at publish time; uploaded files are shared via a link valid for 7 days, so use a public URL for posts scheduled further out |
| `mediaType` | string | No | Force the attachment type when it cannot be detected from the file or URL: image or video \(default auto\) |
| `mediaAltText` | string | No | Alt text for an attached image |

#### Output
Expand Down Expand Up @@ -82,7 +83,8 @@ Edit an existing Buffer post — update its text, schedule, or media. Attaching
| `schedulingType` | string | No | How the post publishes: automatic \(Buffer publishes it, default\) or notification \(you get a mobile reminder\) |
| `dueAt` | string | No | Publish time as an ISO 8601 timestamp \(required when mode is customScheduled\) |
| `saveToDraft` | boolean | No | Save the post as a draft instead of scheduling it |
| `media` | file | No | Image or video to attach — an uploaded file, a file reference from a previous block, or a publicly accessible URL. Replaces existing attachments |
| `media` | file | No | Image or video to attach — an uploaded file, a file reference from a previous block, or a publicly accessible URL. Buffer downloads the media at publish time; uploaded files are shared via a link valid for 7 days, so use a public URL for posts scheduled further out. Replaces existing attachments |
| `mediaType` | string | No | Force the attachment type when it cannot be detected from the file or URL: image or video \(default auto\) |
| `mediaAltText` | string | No | Alt text for an attached image |

#### Output
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/tools/buffer/create-post/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
dueAt: body.dueAt,
saveToDraft: body.saveToDraft,
media: body.media,
mediaType: body.mediaType,
mediaAltText: body.mediaAltText,
Comment thread
waleedlatif1 marked this conversation as resolved.
Comment thread
waleedlatif1 marked this conversation as resolved.
userId: authResult.userId,
requestId,
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/tools/buffer/edit-post/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
dueAt: body.dueAt,
saveToDraft: body.saveToDraft,
media: body.media,
mediaType: body.mediaType,
mediaAltText: body.mediaAltText,
Comment thread
waleedlatif1 marked this conversation as resolved.
Comment thread
waleedlatif1 marked this conversation as resolved.
userId: authResult.userId,
requestId,
Expand Down
46 changes: 39 additions & 7 deletions apps/sim/app/api/tools/buffer/server-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ import {
const VIDEO_EXTENSIONS = ['.mp4', '.mov', '.m4v', '.webm', '.avi']
const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp']
const MEDIA_PROBE_TIMEOUT_MS = 5000
/**
* Buffer fetches asset URLs at publish time, which for queued or scheduled
* posts can be days after createPost. Presign stored files for the S3 maximum
* of 7 days so scheduled posts within that window can still be published.
* The effective lifetime is additionally bounded by the signing credentials:
* Sim's storage clients sign with static keys (AWS_ACCESS_KEY_ID /
* AWS_SECRET_ACCESS_KEY), which support the full 7 days; deployments signing
* with temporary session credentials cap every presigned URL at the session
* lifetime platform-wide.
*/
const MEDIA_PRESIGN_EXPIRY_SECONDS = 7 * 24 * 60 * 60
Comment thread
waleedlatif1 marked this conversation as resolved.

const CREATE_POST_MUTATION = `
mutation CreatePost($input: CreatePostInput!) {
Expand Down Expand Up @@ -53,6 +64,7 @@ const EDIT_POST_MUTATION = `

interface ResolveMediaAssetOptions {
media: RawFileInput | string
mediaType?: 'auto' | 'image' | 'video' | null
mediaAltText?: string | null
userId: string
requestId: string
Expand All @@ -79,15 +91,16 @@ function mediaKindFromExtension(pathOrName: string): 'image' | 'video' | null {
* Determines whether media should be attached as a video or image asset.
* Prefers the file's MIME type, then the path/URL extension, and for
* extensionless URLs falls back to a DNS-pinned HEAD probe of the resolved
* URL's Content-Type. Defaults to image when nothing is conclusive.
* URL's Content-Type. Returns null when nothing is conclusive so the caller
* can ask for an explicit media type instead of guessing.
*/
async function resolveMediaKind(
mimeType: string | undefined,
pathOrName: string,
fileUrl: string,
requestId: string,
logger: Logger
): Promise<'image' | 'video'> {
): Promise<'image' | 'video' | null> {
if (mimeType?.startsWith('video/')) return 'video'
if (mimeType?.startsWith('image/')) return 'image'

Expand All @@ -106,11 +119,11 @@ async function resolveMediaKind(
if (contentType.startsWith('image/')) return 'image'
}
} catch (error) {
logger.warn(`[${requestId}] Media content-type probe failed, defaulting to image`, {
logger.warn(`[${requestId}] Media content-type probe was inconclusive`, {
error: getErrorMessage(error, 'probe failed'),
})
}
return 'image'
return null
}

/**
Expand All @@ -122,7 +135,7 @@ async function resolveMediaKind(
export async function resolveMediaAsset(
options: ResolveMediaAssetOptions
): Promise<ResolvedMediaAsset> {
const { media, mediaAltText, userId, requestId, logger } = options
const { media, mediaType, mediaAltText, userId, requestId, logger } = options

const isFileInput = typeof media === 'object'
const resolution = await resolveFileInputToUrl({
Expand All @@ -131,6 +144,7 @@ export async function resolveMediaAsset(
userId,
requestId,
logger,
presignExpirySeconds: MEDIA_PRESIGN_EXPIRY_SECONDS,
})
if (resolution.error || !resolution.fileUrl) {
return {
Expand All @@ -143,7 +157,22 @@ export async function resolveMediaAsset(

const mimeType = isFileInput ? media.type : undefined
const pathOrName = isFileInput ? media.name || '' : media
const kind = await resolveMediaKind(mimeType, pathOrName, resolution.fileUrl, requestId, logger)
const kind =
mediaType === 'image' || mediaType === 'video'
? mediaType
: await resolveMediaKind(mimeType, pathOrName, resolution.fileUrl, requestId, logger)
if (!kind) {
return {
errorResponse: NextResponse.json(
{
success: false,
error:
'Could not determine whether the media is an image or a video. Set mediaType to "image" or "video".',
},
{ status: 400 }
),
}
}
if (kind === 'video') {
return { asset: { video: { url: resolution.fileUrl } } }
}
Expand Down Expand Up @@ -210,6 +239,7 @@ interface ForwardPostMutationOptions {
dueAt?: string | null
saveToDraft?: boolean | null
media?: RawFileInput | string | null
mediaType?: 'auto' | 'image' | 'video' | null
mediaAltText?: string | null
userId: string
requestId: string
Expand All @@ -224,7 +254,8 @@ interface ForwardPostMutationOptions {
export async function forwardPostMutation(
options: ForwardPostMutationOptions
): Promise<NextResponse> {
const { apiKey, postId, channelId, media, mediaAltText, userId, requestId, logger } = options
const { apiKey, postId, channelId, media, mediaType, mediaAltText, userId, requestId, logger } =
options

const input: Record<string, unknown> = {
mode: options.mode,
Expand All @@ -243,6 +274,7 @@ export async function forwardPostMutation(
if (media) {
const { asset, errorResponse } = await resolveMediaAsset({
media,
mediaType,
mediaAltText,
userId,
requestId,
Expand Down
17 changes: 17 additions & 0 deletions apps/sim/blocks/blocks/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ export const BufferBlock: BlockConfig<BufferPostResponse> = {
mode: 'advanced',
condition: { field: 'operation', value: POST_EDIT_OPS },
},
{
id: 'mediaType',
title: 'Media Type',
type: 'dropdown',
options: [
{ label: 'Auto-detect', id: 'auto' },
{ label: 'Image', id: 'image' },
{ label: 'Video', id: 'video' },
],
value: () => 'auto',
mode: 'advanced',
condition: { field: 'operation', value: POST_EDIT_OPS },
},
{
id: 'mediaAltText',
title: 'Media Alt Text',
Expand Down Expand Up @@ -321,6 +334,10 @@ export const BufferBlock: BlockConfig<BufferPostResponse> = {
dueAt: { type: 'string', description: 'Publish time (ISO 8601)' },
saveToDraft: { type: 'boolean', description: 'Save the post as a draft' },
media: { type: 'string', description: 'Image or video attachment (file or public URL)' },
mediaType: {
type: 'string',
description: 'Attachment type override: auto, image, or video',
},
mediaAltText: { type: 'string', description: 'Alt text for an attached image' },
channelIds: { type: 'string', description: 'Comma-separated channel IDs filter' },
status: { type: 'string', description: 'Comma-separated post status filter' },
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/api/contracts/tools/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const postSharedFields = {
.nullable(),
saveToDraft: z.boolean().optional().nullable(),
media: FileInputSchema.optional().nullable(),
mediaType: z.enum(['auto', 'image', 'video']).default('auto'),
mediaAltText: z.string().max(1000, 'mediaAltText is too long').optional().nullable(),
}

Expand Down
68 changes: 50 additions & 18 deletions apps/sim/lib/uploads/utils/file-utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export interface ResolveFileInputOptions {
userId: string
requestId: string
logger: Logger
/**
* Expiry for presigned URLs minted for stored files, in seconds.
* Defaults to 5 minutes; raise it only when the external service fetches
* the URL later than the current request (e.g. scheduled publishing).
*/
presignExpirySeconds?: number
}

/**
Expand All @@ -62,7 +68,7 @@ export interface ResolveFileInputOptions {
export async function resolveFileInputToUrl(
options: ResolveFileInputOptions
): Promise<FileResolutionResult> {
const { file, filePath, userId, requestId, logger } = options
const { file, filePath, userId, requestId, logger, presignExpirySeconds = 5 * 60 } = options

if (file) {
let userFile: UserFile
Expand All @@ -77,19 +83,11 @@ export async function resolveFileInputToUrl(
}
}

let fileUrl = userFile.url || ''

// Handle internal URLs
if (fileUrl && isInternalFileUrl(fileUrl)) {
const resolution = await resolveInternalFileUrl(fileUrl, userId, requestId, logger)
if (resolution.error) {
return { error: resolution.error }
}
fileUrl = resolution.fileUrl || ''
}

// Generate presigned URL if we have a key but no URL
if (!fileUrl && userFile.key) {
// A stored file always gets a freshly minted presigned URL scoped to the
// requested expiry — an embedded url (internal serve path or a previously
// minted presigned link) may be stale, shorter-lived than required, or
// point at a different object than the verified key.
if (userFile.key) {
const context = resolveTrustedFileContext(userFile.key, userFile.context)
const hasAccess = await verifyFileAccess(userFile.key, userId, undefined, context, false)

Expand All @@ -102,7 +100,30 @@ export async function resolveFileInputToUrl(
return { error: { status: 404, message: 'File not found' } }
}

fileUrl = await StorageService.generatePresignedDownloadUrl(userFile.key, context, 5 * 60)
const fileUrl = await StorageService.generatePresignedDownloadUrl(
userFile.key,
context,
presignExpirySeconds
)
Comment thread
waleedlatif1 marked this conversation as resolved.
return { fileUrl }
}

let fileUrl = userFile.url || ''

// Without a key, the schema guarantees the url references an uploaded
// file, so resolve the internal serve path to a presigned URL.
if (fileUrl && isInternalFileUrl(fileUrl)) {
const resolution = await resolveInternalFileUrl(
fileUrl,
userId,
requestId,
logger,
presignExpirySeconds
)
if (resolution.error) {
return { error: resolution.error }
}
fileUrl = resolution.fileUrl || ''
}

return { fileUrl }
Expand All @@ -112,7 +133,13 @@ export async function resolveFileInputToUrl(
let fileUrl = filePath

if (isInternalFileUrl(filePath)) {
const resolution = await resolveInternalFileUrl(filePath, userId, requestId, logger)
const resolution = await resolveInternalFileUrl(
filePath,
userId,
requestId,
logger,
presignExpirySeconds
)
if (resolution.error) {
return { error: resolution.error }
}
Expand Down Expand Up @@ -227,7 +254,8 @@ export async function resolveInternalFileUrl(
filePath: string,
userId: string,
requestId: string,
logger: Logger
logger: Logger,
presignExpirySeconds = 5 * 60
): Promise<{ fileUrl?: string; error?: { status: number; message: string } }> {
if (!isInternalFileUrl(filePath)) {
return { fileUrl: filePath }
Expand All @@ -247,7 +275,11 @@ export async function resolveInternalFileUrl(
return { error: { status: 404, message: 'File not found' } }
}

const fileUrl = await StorageService.generatePresignedDownloadUrl(storageKey, context, 5 * 60)
const fileUrl = await StorageService.generatePresignedDownloadUrl(
storageKey,
context,
presignExpirySeconds
)
logger.info(`[${requestId}] Generated presigned URL for ${context} file`)
return { fileUrl }
} catch (error) {
Expand Down
10 changes: 9 additions & 1 deletion apps/sim/tools/buffer/create_post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ export const bufferCreatePostTool: ToolConfig<BufferCreatePostParams, BufferPost
required: false,
visibility: 'user-or-llm',
description:
'Image or video to attach — an uploaded file, a file reference from a previous block, or a publicly accessible URL',
'Image or video to attach — an uploaded file, a file reference from a previous block, or a publicly accessible URL. Buffer downloads the media at publish time; uploaded files are shared via a link valid for 7 days, so use a public URL for posts scheduled further out',
},
mediaType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Force the attachment type when it cannot be detected from the file or URL: image or video (default auto)',
},
mediaAltText: {
type: 'string',
Expand All @@ -85,6 +92,7 @@ export const bufferCreatePostTool: ToolConfig<BufferCreatePostParams, BufferPost
dueAt: params.dueAt,
saveToDraft: params.saveToDraft,
media: params.media,
mediaType: params.mediaType,
mediaAltText: params.mediaAltText,
}),
},
Expand Down
10 changes: 9 additions & 1 deletion apps/sim/tools/buffer/edit_post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ export const bufferEditPostTool: ToolConfig<BufferEditPostParams, BufferPostResp
required: false,
visibility: 'user-or-llm',
description:
'Image or video to attach — an uploaded file, a file reference from a previous block, or a publicly accessible URL. Replaces existing attachments',
'Image or video to attach — an uploaded file, a file reference from a previous block, or a publicly accessible URL. Buffer downloads the media at publish time; uploaded files are shared via a link valid for 7 days, so use a public URL for posts scheduled further out. Replaces existing attachments',
},
mediaType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Force the attachment type when it cannot be detected from the file or URL: image or video (default auto)',
},
mediaAltText: {
type: 'string',
Expand All @@ -85,6 +92,7 @@ export const bufferEditPostTool: ToolConfig<BufferEditPostParams, BufferPostResp
dueAt: params.dueAt,
saveToDraft: params.saveToDraft,
media: params.media,
mediaType: params.mediaType,
mediaAltText: params.mediaAltText,
}),
},
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/tools/buffer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ export interface BufferCreatePostParams extends BufferBaseParams {
dueAt?: string
saveToDraft?: boolean
media?: unknown
mediaType?: 'auto' | 'image' | 'video'
mediaAltText?: string
}

Expand All @@ -400,6 +401,7 @@ export interface BufferEditPostParams extends BufferBaseParams {
dueAt?: string
saveToDraft?: boolean
media?: unknown
mediaType?: 'auto' | 'image' | 'video'
mediaAltText?: string
}

Expand Down
Loading