Skip to content

Commit 4d105c9

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
refactor(instagram): align publishing with file patterns
1 parent bf5abc9 commit 4d105c9

24 files changed

Lines changed: 361 additions & 673 deletions

File tree

apps/sim/app/api/tools/instagram/download-media/route.test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -303,11 +303,6 @@ describe('POST /api/tools/instagram/download-media', () => {
303303
})
304304

305305
describe('instagramDownloadMediaTool', () => {
306-
it('documents the canonical User File size limit', () => {
307-
expect(instagramDownloadMediaTool.description).toContain('100 MB max per file')
308-
expect(instagramDownloadMediaTool.outputs?.files.description).toContain('100 MB max each')
309-
})
310-
311306
it('forwards execution context and returns canonical file-array output', async () => {
312307
const body = instagramDownloadMediaTool.request.body?.({
313308
accessToken: 'instagram-token',
@@ -354,6 +349,5 @@ describe('instagramDownloadMediaTool', () => {
354349
downloadedCount: 1,
355350
},
356351
})
357-
expect(instagramDownloadMediaTool.outputs?.files.type).toBe('file[]')
358352
})
359353
})

apps/sim/app/api/tools/instagram/publish-carousel/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { instagramPublishCarouselContract } from '@/lib/api/contracts/tools/inst
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8-
import { resolveInstagramCarouselMedia } from '@/app/api/tools/instagram/resolve-media'
98
import {
109
createMediaContainer,
1110
publishMediaContainer,
1211
resolveIgUserId,
12+
resolveInstagramCarouselMedia,
1313
waitForContainerReady,
14-
} from '@/tools/instagram/utils'
14+
} from '@/app/api/tools/instagram/server-utils'
1515

1616
export const dynamic = 'force-dynamic'
1717
/**

apps/sim/app/api/tools/instagram/publish-image/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { instagramPublishImageContract } from '@/lib/api/contracts/tools/instagr
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8-
import { resolveInstagramMedia } from '@/app/api/tools/instagram/resolve-media'
98
import {
109
createMediaContainer,
1110
publishMediaContainer,
1211
resolveIgUserId,
12+
resolveInstagramMedia,
1313
waitForContainerReady,
14-
} from '@/tools/instagram/utils'
14+
} from '@/app/api/tools/instagram/server-utils'
1515

1616
export const dynamic = 'force-dynamic'
1717
/** Meta may poll container status once per minute for up to five minutes. */

apps/sim/app/api/tools/instagram/publish-reel/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { instagramPublishReelContract } from '@/lib/api/contracts/tools/instagra
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8-
import { resolveInstagramMedia } from '@/app/api/tools/instagram/resolve-media'
98
import {
109
createMediaContainer,
1110
publishMediaContainer,
1211
resolveIgUserId,
12+
resolveInstagramMedia,
1313
waitForContainerReady,
14-
} from '@/tools/instagram/utils'
14+
} from '@/app/api/tools/instagram/server-utils'
1515

1616
export const dynamic = 'force-dynamic'
1717
/** Meta may poll container status once per minute for up to five minutes. */

apps/sim/app/api/tools/instagram/publish-story/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { instagramPublishStoryContract } from '@/lib/api/contracts/tools/instagr
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8-
import { resolveInstagramMedia } from '@/app/api/tools/instagram/resolve-media'
98
import {
109
createMediaContainer,
1110
publishMediaContainer,
1211
resolveIgUserId,
12+
resolveInstagramMedia,
1313
waitForContainerReady,
14-
} from '@/tools/instagram/utils'
14+
} from '@/app/api/tools/instagram/server-utils'
1515

1616
export const dynamic = 'force-dynamic'
1717
/** Meta may poll container status once per minute for up to five minutes. */

apps/sim/app/api/tools/instagram/publish-video/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { instagramPublishVideoContract } from '@/lib/api/contracts/tools/instagr
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8-
import { resolveInstagramMedia } from '@/app/api/tools/instagram/resolve-media'
98
import {
109
createMediaContainer,
1110
publishMediaContainer,
1211
resolveIgUserId,
12+
resolveInstagramMedia,
1313
waitForContainerReady,
14-
} from '@/tools/instagram/utils'
14+
} from '@/app/api/tools/instagram/server-utils'
1515

1616
export const dynamic = 'force-dynamic'
1717
/** Meta may poll container status once per minute for up to five minutes. */

apps/sim/app/api/tools/instagram/resolve-media.test.ts renamed to apps/sim/app/api/tools/instagram/server-utils.test.ts

Lines changed: 66 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @vitest-environment node
33
*/
44
import type { Logger } from '@sim/logger'
5-
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
66

77
const { mockHasCloudStorage, mockResolveFileInputToUrl } = vi.hoisted(() => ({
88
mockHasCloudStorage: vi.fn(),
@@ -18,10 +18,13 @@ vi.mock('@/lib/uploads/utils/file-utils.server', () => ({
1818
}))
1919

2020
import {
21+
createMediaContainer,
2122
INSTAGRAM_MEDIA_URL_TTL_SECONDS,
23+
publishMediaContainer,
24+
resolveIgUserId,
2225
resolveInstagramCarouselMedia,
2326
resolveInstagramMedia,
24-
} from '@/app/api/tools/instagram/resolve-media'
27+
} from '@/app/api/tools/instagram/server-utils'
2528

2629
const logger = {} as Logger
2730
const context = {
@@ -44,40 +47,26 @@ function uploadedFile(overrides: Record<string, unknown> = {}) {
4447
beforeEach(() => {
4548
vi.clearAllMocks()
4649
mockHasCloudStorage.mockReturnValue(true)
47-
mockResolveFileInputToUrl.mockImplementation(
48-
async ({ file, filePath }: { file?: { name?: string }; filePath?: string }) => ({
49-
fileUrl: filePath || `https://signed.example.com/${file?.name || 'media'}`,
50-
})
51-
)
50+
mockResolveFileInputToUrl.mockImplementation(async ({ file }: { file?: { name?: string } }) => ({
51+
fileUrl: `https://signed.example.com/${file?.name || 'media'}`,
52+
}))
53+
})
54+
55+
afterEach(() => {
56+
vi.unstubAllGlobals()
5257
})
5358

5459
describe('resolveInstagramMedia', () => {
55-
it('accepts a public HTTPS media URL', async () => {
60+
it('rejects non-file inputs before resolving them', async () => {
5661
const result = await resolveInstagramMedia({
5762
...context,
5863
input: 'https://cdn.example.com/photo.jpg',
5964
role: 'image',
6065
})
6166

62-
expect(result).toEqual({
63-
media: expect.objectContaining({
64-
url: 'https://cdn.example.com/photo.jpg',
65-
kind: 'image',
66-
mimeType: 'image/jpeg',
67-
}),
68-
})
69-
})
70-
71-
it('rejects a public HTTP media URL before resolving it', async () => {
72-
const result = await resolveInstagramMedia({
73-
...context,
74-
input: 'http://cdn.example.com/photo.jpg',
75-
role: 'image',
76-
})
77-
7867
expect(result.error).toEqual({
7968
status: 400,
80-
message: 'Instagram media URLs must use HTTPS so Meta can download them',
69+
message: 'Media must be a Sim file',
8170
})
8271
expect(mockResolveFileInputToUrl).not.toHaveBeenCalled()
8372
})
@@ -95,22 +84,19 @@ describe('resolveInstagramMedia', () => {
9584
})
9685
expect(mockResolveFileInputToUrl).toHaveBeenCalledWith({
9786
file,
98-
filePath: undefined,
9987
...context,
10088
presignExpirySeconds: INSTAGRAM_MEDIA_URL_TTL_SECONDS,
10189
})
10290
})
10391

104-
it('requires cloud storage for uploaded files and internal URLs', async () => {
92+
it('requires cloud storage for publishing files', async () => {
10593
mockHasCloudStorage.mockReturnValue(false)
10694

107-
for (const input of [uploadedFile(), '/api/files/serve/execution/photo.jpg']) {
108-
const result = await resolveInstagramMedia({ ...context, input, role: 'image' })
109-
expect(result.error).toEqual({
110-
status: 400,
111-
message: expect.stringContaining('Cloud storage is required'),
112-
})
113-
}
95+
const result = await resolveInstagramMedia({ ...context, input: uploadedFile(), role: 'image' })
96+
expect(result.error).toEqual({
97+
status: 400,
98+
message: expect.stringContaining('Cloud storage is required'),
99+
})
114100
expect(mockResolveFileInputToUrl).not.toHaveBeenCalled()
115101
})
116102

@@ -164,71 +150,40 @@ describe('resolveInstagramMedia', () => {
164150
})
165151

166152
describe('resolveInstagramCarouselMedia', () => {
167-
it('parses JSON input and infers image and video item types sequentially', async () => {
153+
it('resolves canonical files in order and infers image and video types sequentially', async () => {
168154
let activeResolutions = 0
169155
let maxActiveResolutions = 0
170-
mockResolveFileInputToUrl.mockImplementation(async ({ filePath }: { filePath?: string }) => {
156+
mockResolveFileInputToUrl.mockImplementation(async ({ file }: { file?: { name?: string } }) => {
171157
activeResolutions += 1
172158
maxActiveResolutions = Math.max(maxActiveResolutions, activeResolutions)
173159
await Promise.resolve()
174160
activeResolutions -= 1
175-
return { fileUrl: filePath }
161+
return { fileUrl: `https://signed.example.com/${file?.name}` }
176162
})
177163

178164
const result = await resolveInstagramCarouselMedia(
179-
JSON.stringify([
180-
'https://cdn.example.com/carousel-1.jpg',
181-
'https://cdn.example.com/carousel-2.mp4',
182-
]),
165+
[
166+
uploadedFile({ name: 'carousel-1.jpg' }),
167+
uploadedFile({ name: 'carousel-2.mp4', type: 'video/mp4' }),
168+
],
183169
context.userId,
184170
context.requestId,
185171
logger
186172
)
187173

188174
expect(result.items?.map(({ url, kind }) => ({ url, kind }))).toEqual([
189-
{ url: 'https://cdn.example.com/carousel-1.jpg', kind: 'image' },
190-
{ url: 'https://cdn.example.com/carousel-2.mp4', kind: 'video' },
175+
{ url: 'https://signed.example.com/carousel-1.jpg', kind: 'image' },
176+
{ url: 'https://signed.example.com/carousel-2.mp4', kind: 'video' },
191177
])
192178
expect(maxActiveResolutions).toBe(1)
193179
})
194180

195-
it('supports legacy comma-separated URLs with an explicit video prefix', async () => {
196-
const result = await resolveInstagramCarouselMedia(
197-
'https://cdn.example.com/carousel-1.jpg, video:https://cdn.example.com/carousel-2.mp4',
198-
context.userId,
199-
context.requestId,
200-
logger
201-
)
202-
203-
expect(result.items?.map(({ kind }) => kind)).toEqual(['image', 'video'])
204-
})
205-
206-
it('infers media types for uploaded file arrays', async () => {
207-
const result = await resolveInstagramCarouselMedia(
208-
[
209-
uploadedFile(),
210-
uploadedFile({
211-
id: 'file-2',
212-
key: 'execution/workflow-1/execution-1/video.mov',
213-
name: 'video.mov',
214-
type: 'video/quicktime',
215-
}),
216-
],
217-
context.userId,
218-
context.requestId,
219-
logger
220-
)
221-
222-
expect(result.items?.map(({ kind }) => kind)).toEqual(['image', 'video'])
223-
})
224-
225181
it.each([
226182
{ count: 1, label: 'too few' },
227183
{ count: 11, label: 'too many' },
228184
])('rejects $label carousel items before resolving them', async ({ count }) => {
229-
const input = Array.from(
230-
{ length: count },
231-
(_, index) => `https://cdn.example.com/carousel-${index + 1}.jpg`
185+
const input = Array.from({ length: count }, (_, index) =>
186+
uploadedFile({ id: `file-${index + 1}`, name: `carousel-${index + 1}.jpg` })
232187
)
233188

234189
const result = await resolveInstagramCarouselMedia(
@@ -245,14 +200,46 @@ describe('resolveInstagramCarouselMedia', () => {
245200
expect(mockResolveFileInputToUrl).not.toHaveBeenCalled()
246201
})
247202

248-
it('rejects malformed carousel JSON', async () => {
203+
it('rejects non-file string inputs', async () => {
249204
const result = await resolveInstagramCarouselMedia(
250-
'[not-json',
205+
'https://example.com/one.jpg,https://example.com/two.jpg',
251206
context.userId,
252207
context.requestId,
253208
logger
254209
)
255210

256-
expect(result.error).toEqual({ status: 400, message: 'Carousel media JSON is invalid' })
211+
expect(result.error).toEqual({ status: 400, message: 'Carousel media is required' })
212+
})
213+
})
214+
215+
describe('Instagram publishing requests', () => {
216+
it('resolves the connected account when no override is supplied', async () => {
217+
const fetchMock = vi.fn().mockResolvedValue(Response.json({ user_id: 123 }, { status: 200 }))
218+
vi.stubGlobal('fetch', fetchMock)
219+
220+
await expect(resolveIgUserId('token')).resolves.toBe('123')
221+
expect(fetchMock).toHaveBeenCalledOnce()
222+
})
223+
224+
it('creates and publishes form-encoded containers', async () => {
225+
const fetchMock = vi
226+
.fn()
227+
.mockResolvedValueOnce(Response.json({ id: 'container-1' }, { status: 200 }))
228+
.mockResolvedValueOnce(Response.json({ id: 'media-1' }, { status: 200 }))
229+
vi.stubGlobal('fetch', fetchMock)
230+
231+
await expect(
232+
createMediaContainer('token', 'user-1', { image_url: 'https://signed.example/image.jpg' })
233+
).resolves.toBe('container-1')
234+
await expect(publishMediaContainer('token', 'user-1', 'container-1')).resolves.toBe('media-1')
235+
236+
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
237+
method: 'POST',
238+
body: 'image_url=https%3A%2F%2Fsigned.example%2Fimage.jpg',
239+
})
240+
expect(fetchMock.mock.calls[1]?.[1]).toMatchObject({
241+
method: 'POST',
242+
body: 'creation_id=container-1',
243+
})
257244
})
258245
})

0 commit comments

Comments
 (0)