Skip to content

Commit

Permalink
feat(linkedIn): LinkedIn post video action in public template (#104)
Browse files Browse the repository at this point in the history
## Describe your changes

## Issue ticket number and link

## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [ ] I added tests, otherwise the reason is:
- [ ] External API requests have `retries`
- [ ] Pagination is used where appropriate
- [ ] The built in `nango.paginate` call is used instead of a `while
(true)` loop
- [ ] Third party requests are NOT parallelized (this can cause issues
with rate limits)
- [ ] If a sync requires metadata the `nango.yaml` has `auto_start:
false`
- [ ] If the sync is a `full` sync then `track_deletes: true` is set
- [ ] I followed the best practices and guidelines from the [Writing
Integration
Scripts](/NangoHQ/integration-templates/blob/main/WRITING_INTEGRATION_SCRIPTS.md)
doc

---------

Co-authored-by: Khaliq <khaliqgant@gmail.com>
  • Loading branch information
dannylwe and khaliqgant authored Nov 18, 2024
1 parent 290a958 commit 95ceb42
Show file tree
Hide file tree
Showing 11 changed files with 425 additions and 46 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ models.ts
vite.config.ts
vitest.setup.ts
globals.d.ts
integrations/linkedin/proxy/video-upload.ts
integrations/linkedin/nango-integrations/linkedin/proxy/video-upload.ts
23 changes: 23 additions & 0 deletions flows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5668,6 +5668,29 @@ integrations:
updatedAt: date
teamId: string
projectIds: string
linkedin:
actions:
post:
description: Create a linkedin post with an optional video
input: LinkedinVideoPost
output: CreateLinkedInPostWithVideoResponse
endpoint: POST /videos
scopes:
- openid
- profile
- r_basicprofile
- w_member_social
- email
- w_organization_social
- r_organization_social
models:
LinkedinVideoPost:
text: string
videoURN: string
videoTitle: string
ownerId: string
CreateLinkedInPostWithVideoResponse:
succcess: boolean
luma:
syncs:
list-events:
Expand Down
79 changes: 39 additions & 40 deletions integrations/gorgias/tests/gorgias-tickets.test.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,52 @@
import { vi, expect, it, describe } from "vitest";
import { vi, expect, it, describe } from 'vitest';

import fetchData from "../syncs/tickets.js";
import fetchData from '../syncs/tickets.js';

describe("gorgias tickets tests", () => {
const nangoMock = new global.vitest.NangoSyncMock({
dirname: __dirname,
name: "tickets",
Model: "Ticket"
});
describe('gorgias tickets tests', () => {
const nangoMock = new global.vitest.NangoSyncMock({
dirname: __dirname,
name: 'tickets',
Model: 'Ticket'
});

const models = "Ticket".split(',');
const batchSaveSpy = vi.spyOn(nangoMock, 'batchSave');
const models = 'Ticket'.split(',');
const batchSaveSpy = vi.spyOn(nangoMock, 'batchSave');

it("should get, map correctly the data and batchSave the result", async () => {
await fetchData(nangoMock);
it('should get, map correctly the data and batchSave the result', async () => {
await fetchData(nangoMock);

for (const model of models) {
const batchSaveData = await nangoMock.getBatchSaveData(model);
for (const model of models) {
const batchSaveData = await nangoMock.getBatchSaveData(model);

const totalCalls = batchSaveSpy.mock.calls.length;
const totalCalls = batchSaveSpy.mock.calls.length;

if (totalCalls > models.length) {
const splitSize = Math.ceil(batchSaveData.length / totalCalls);
if (totalCalls > models.length) {
const splitSize = Math.ceil(batchSaveData.length / totalCalls);

const splitBatchSaveData = [];
for (let i = 0; i < totalCalls; i++) {
const chunk = batchSaveData.slice(i * splitSize, (i + 1) * splitSize);
splitBatchSaveData.push(chunk);
const splitBatchSaveData = [];
for (let i = 0; i < totalCalls; i++) {
const chunk = batchSaveData.slice(i * splitSize, (i + 1) * splitSize);
splitBatchSaveData.push(chunk);
}

splitBatchSaveData.forEach((data, index) => {
// @ts-ignore
expect(batchSaveSpy?.mock.calls[index][0]).toEqual(data);
});
} else {
expect(nangoMock.batchSave).toHaveBeenCalledWith(batchSaveData, model);
}
}
});

splitBatchSaveData.forEach((data, index) => {
// @ts-ignore
expect(batchSaveSpy?.mock.calls[index][0]).toEqual(data);
});
it('should get, map correctly the data and batchDelete the result', async () => {
await fetchData(nangoMock);

} else {
expect(nangoMock.batchSave).toHaveBeenCalledWith(batchSaveData, model);
for (const model of models) {
const batchDeleteData = await nangoMock.getBatchDeleteData(model);
if (batchDeleteData && batchDeleteData.length > 0) {
expect(nangoMock.batchDelete).toHaveBeenCalledWith(batchDeleteData, model);
}
}
}
});

it('should get, map correctly the data and batchDelete the result', async () => {
await fetchData(nangoMock);

for (const model of models) {
const batchDeleteData = await nangoMock.getBatchDeleteData(model);
if (batchDeleteData && batchDeleteData.length > 0) {
expect(nangoMock.batchDelete).toHaveBeenCalledWith(batchDeleteData, model);
}
}
});
});
});
12 changes: 6 additions & 6 deletions integrations/gorgias/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ export interface GorgiasTicketResponse {
customer: Customer;
assignee_user: AssigneeUser | null;
assignee_user_id: number;
assignee_team: any | null;
assignee_team: any;
assignee_team_id: number | null;
language: string;
subject: string;
excerpt: string;
meta: any | null;
meta: any;
tags: Tag[];
integrations: [];
messages_count: number;
Expand All @@ -83,7 +83,7 @@ export interface GorgiasTicketResponse {
closed_datetime: string | null;
trashed_datetime: string | null;
snooze_datetime: string | null;
satisfaction_survey: any | null;
satisfaction_survey: any;
reply_options: ReplyOptions;
requester?: Customer;
is_unread: boolean;
Expand All @@ -97,8 +97,8 @@ interface Customer {
lastname: string;
meta: { name_set_via: string };
channels: Channel[];
data: any | null;
customer: any | null;
data: any;
customer: any;
integrations: Record<string, any>;
external_id: string | null;
note: string | null;
Expand Down Expand Up @@ -213,7 +213,7 @@ export interface GorgiasMessageResponse {
attachments: GorgiasAttachementResponse[] | null;
actions: MessageAction[];
headers: null;
meta: any | null;
meta: any;
created_datetime: string;
sent_datetime: string;
failed_datetime: string;
Expand Down
23 changes: 23 additions & 0 deletions integrations/linkedin/actions/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { CreateLinkedInPostWithVideoResponse, NangoAction, LinkedinVideoPost } from '../../models';
import { createPostWithVideo } from '../helpers/post-video.js';
import { userInfo } from '../helpers/user-info.js';

export default async function runAction(nango: NangoAction, input: LinkedinVideoPost): Promise<CreateLinkedInPostWithVideoResponse> {
const videoURN = input?.videoURN;
let ownerId = input?.ownerId;

if (!ownerId) {
const me = await userInfo(nango);
ownerId = me.sub;
}

if (videoURN && !videoURN.startsWith('urn')) {
throw new nango.ActionError({
message: `invalid video urn`
});
}

const resp = await createPostWithVideo(nango, ownerId, input.text, input.videoTitle, videoURN);

return resp;
}
3 changes: 3 additions & 0 deletions integrations/linkedin/fixtures/video.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"text": "test post 123"
}
55 changes: 55 additions & 0 deletions integrations/linkedin/helpers/post-video.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { NangoAction, CreateLinkedInPostWithVideoResponse, ProxyConfiguration } from '../../models';
import type { LinkedinCreatePost } from '../types.js';

export async function createPostWithVideo(
nango: NangoAction,
author: string,
postText: string,
videoTitle: string,
videoURN?: string
): Promise<CreateLinkedInPostWithVideoResponse> {
const postData: LinkedinCreatePost = {
author: `urn:li:person:${author}`,
commentary: postText,
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: []
},
lifecycleState: 'PUBLISHED',
isReshareDisabledByAuthor: false
};

if (videoURN) {
postData.content = {
media: {
title: videoTitle,
// video that is already uploaded to linkedin api. this id can be video, image or document urn.
id: videoURN
}
};
}

const config: ProxyConfiguration = {
// https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api?view=li-lms-2024-10&tabs=http
endpoint: `/rest/posts`,
retries: 10,
data: postData,
headers: {
'LinkedIn-Version': '202405'
}
};

const response = await nango.post(config);

if (response.status !== 200) {
throw new nango.ActionError({
message: `failed to create post with video urn ${videoURN}`
});
}

return {
succcess: response.status == 200 ? true : false
};
}
19 changes: 19 additions & 0 deletions integrations/linkedin/helpers/user-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NangoAction, ProxyConfiguration } from '../../models';
import type { LinkedInUserInfo } from '../types.js';

export async function userInfo(nango: NangoAction): Promise<LinkedInUserInfo> {
const config: ProxyConfiguration = {
// https://learn.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api
endpoint: '/v2/userinfo',
retries: 10,
headers: {
'LinkedIn-Version': '202405'
}
};

const response = await nango.get(config);

const { data } = response;

return data;
}
26 changes: 26 additions & 0 deletions integrations/linkedin/nango.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
integrations:
linkedin:
actions:
post:
description: Create a linkedin post with an optional video
input: LinkedinVideoPost
output: CreateLinkedInPostWithVideoResponse
endpoint: POST /videos
scopes:
- openid
- profile
- r_basicprofile
- w_member_social
- email
- w_organization_social
- r_organization_social

models:
LinkedinVideoPost:
text: string
videoURN: string
videoTitle: string
ownerId: string

CreateLinkedInPostWithVideoResponse:
succcess: boolean
Loading

0 comments on commit 95ceb42

Please sign in to comment.