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
16 changes: 15 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions packages/pieces/community/letmepost/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
17 changes: 17 additions & 0 deletions packages/pieces/community/letmepost/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@activepieces/piece-letmepost",
"version": "0.0.1",
"type": "commonjs",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.lib.json && cp package.json dist/",
"lint": "eslint 'src/**/*.ts'"
},
"dependencies": {
"@activepieces/pieces-common": "workspace:*",
"@activepieces/pieces-framework": "workspace:*",
"@activepieces/shared": "workspace:*",
"tslib": "^2.3.0"
}
}
35 changes: 35 additions & 0 deletions packages/pieces/community/letmepost/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createPiece } from '@activepieces/pieces-framework';
import { createCustomApiCallAction } from '@activepieces/pieces-common';
import { PieceCategory } from '@activepieces/shared';
import { letmepostAuth } from './lib/common/auth';
import { publishPost } from './lib/actions/publish-post';
import { getPost } from './lib/actions/get-post';
import { listAccounts } from './lib/actions/list-accounts';
import { listMedia } from './lib/actions/list-media';
import { newPublishedPost } from './lib/triggers/new-published-post';
import { postEvent } from './lib/triggers/post-event';

export const letmepost = createPiece({
displayName: 'Letmepost',
description:
'Publish and schedule posts to Bluesky, X, LinkedIn, Instagram, Threads, Facebook, Pinterest, and TikTok through one API',
minimumSupportedRelease: '0.36.1',
logoUrl: 'https://cdn.activepieces.com/pieces/letmepost.png',
categories: [PieceCategory.MARKETING],
auth: letmepostAuth,
authors: ['rosekamallove', 'sanket-a11y'],
actions: [
publishPost,
getPost,
listAccounts,
listMedia,
createCustomApiCallAction({
baseUrl: (auth) => auth?.props.base_url ?? 'https://api.letmepost.dev',
auth: letmepostAuth,
authMapping: async (auth) => ({
Authorization: `Bearer ${auth.props.api_key}`,
}),
}),
],
triggers: [newPublishedPost, postEvent],
});
35 changes: 35 additions & 0 deletions packages/pieces/community/letmepost/src/lib/actions/get-post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { letmepostAuth } from '../common/auth';
import { letmepostApiCall } from '../common';

export const getPost = createAction({
auth: letmepostAuth,
name: 'get_post',
displayName: 'Get a Post',
description: 'Retrieve a single post and its per-target results',
audience: 'both',
aiMetadata: {
description:
'Fetches one post by id, including its status, scheduled and published times, the connected account it went to, and the resulting platform URL. Use to check whether a post published or to read back its details. Idempotent, a read-only lookup.',
idempotent: true,
},
props: {
postId: Property.ShortText({
displayName: 'Post ID',
description: 'The ID of the post to retrieve.',
required: true,
}),
},
async run(context) {
const { postId } = context.propsValue;

const response = await letmepostApiCall<Record<string, unknown>>({
auth: context.auth,
method: HttpMethod.GET,
path: `/v1/posts/${encodeURIComponent(postId)}`,
});

return response.body;
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { letmepostAuth } from '../common/auth';
import { letmepostApiCall } from '../common';

export const listAccounts = createAction({
auth: letmepostAuth,
name: 'list_accounts',
displayName: 'List Accounts',
description: 'List the social accounts connected to your organization',
audience: 'both',
aiMetadata: {
description:
'Lists every connected account, returning each id, platform, and display name. Use to discover which accounts are available and to resolve the account id required by Publish a Post. Idempotent, a read-only lookup.',
idempotent: true,
},
props: {},
async run(context) {
const response = await letmepostApiCall<{
data: {
id: string;
platform: string;
displayName: string | null;
profileId: string;
platformAccountId: string;
tokenExpiresAt: string | null;
createdAt: string;
}[];
}>({
auth: context.auth,
method: HttpMethod.GET,
path: '/v1/accounts',
});

return response.body.data;
},
});
45 changes: 45 additions & 0 deletions packages/pieces/community/letmepost/src/lib/actions/list-media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { letmepostAuth } from '../common/auth';
import { letmepostApiCall } from '../common';

export const listMedia = createAction({
auth: letmepostAuth,
name: 'list_media',
displayName: 'List Media',
description: 'List previously uploaded media assets',
audience: 'both',
aiMetadata: {
description:
'Lists uploaded media assets, returning each id, URL, content type, and size. Use to find an existing media id to reuse in Publish a Post instead of re-uploading. Idempotent, a read-only lookup.',
idempotent: true,
},
props: {
limit: Property.Number({
displayName: 'Limit',
description: 'Maximum number of media assets to return.',
required: false,
defaultValue: 50,
}),
},
async run(context) {
const { limit } = context.propsValue;

const response = await letmepostApiCall<{
data: {
id: string;
url: string;
contentType: string;
sizeBytes: number;
createdAt: string;
}[];
}>({
auth: context.auth,
method: HttpMethod.GET,
path: '/v1/media',
queryParams: { limit: String(limit ?? 50) },
});

return response.body.data;
},
});
159 changes: 159 additions & 0 deletions packages/pieces/community/letmepost/src/lib/actions/publish-post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { letmepostAuth } from '../common/auth';
import { letmepostApiCall, letmepostCommon } from '../common';

export const publishPost = createAction({
auth: letmepostAuth,
name: 'publish_post',
displayName: 'Publish a Post',
description: 'Publish or schedule a post to one or more connected accounts',
audience: 'both',
aiMetadata: {
description:
'Sends one body of text and optional media to one or more connected accounts in a single call, either immediately or scheduled for a future time. Use to push content out through letmepost. A schedule time is required when Publish Now is off. Not idempotent unless an Idempotency Key is supplied, in which case replays within 24 hours return the original result.',
idempotent: false,
},
props: {
accounts: letmepostCommon.accountsDropdown,
text: Property.LongText({
displayName: 'Text',
description:
'The post body. Optional when the post is media-only on platforms that allow it.',
required: false,
}),
media: Property.Array({
displayName: 'Media',
description:
'Media attachments. Give each item a public URL or an existing letmepost media ID, not both.',
required: false,
properties: {
kind: Property.StaticDropdown({
displayName: 'Type',
required: true,
options: {
options: [
{ label: 'Image', value: 'image' },
{ label: 'Video', value: 'video' },
],
},
}),
url: Property.ShortText({
displayName: 'URL',
description: 'Public URL of the media. Use this or Media ID.',
required: false,
}),
mediaId: Property.ShortText({
displayName: 'Media ID',
description:
'ID of a media asset already uploaded to letmepost. Use this or URL.',
required: false,
}),
altText: Property.ShortText({
displayName: 'Alt text',
description: 'Accessibility description for the media.',
required: false,
}),
},
}),
firstComment: Property.LongText({
displayName: 'First comment',
description:
'Posted as the first comment under the published post, where the platform supports it.',
required: false,
}),
publishNow: Property.Checkbox({
displayName: 'Publish now',
description:
'Publish immediately. Turn off to schedule for a future time.',
required: false,
defaultValue: true,
}),
scheduledAt: Property.DateTime({
displayName: 'Schedule at',
description:
'When to publish the post (ISO 8601). Required when Publish Now is off.',
required: false,
}),
profileId: Property.ShortText({
displayName: 'Profile ID',
description: 'Optional profile to attribute this post to.',
required: false,
}),
idempotencyKey: Property.ShortText({
displayName: 'Idempotency key',
description:
'Reusing the same key never publishes twice, which makes retries safe.',
required: false,
}),
},
async run(context) {
const {
accounts,
text,
media,
firstComment,
publishNow,
scheduledAt,
profileId,
idempotencyKey,
} = context.propsValue;

const publishImmediately = publishNow ?? true;

if (!publishImmediately && !scheduledAt) {
throw new Error(
'A Schedule at time is required when Publish now is turned off.'
);
}

const body: Record<string, unknown> = {
targets: accounts.map((accountId) => ({ accountId })),
};
if (publishImmediately) {
body['publishNow'] = true;
} else {
body['scheduledAt'] = scheduledAt;
}

if (text) {
body['text'] = text;
}
if (media && media.length > 0) {
body['media'] = (media as Array<Record<string, unknown>>).map((item) => {
const cleaned: Record<string, unknown> = { kind: item['kind'] };
if (item['url']) {
cleaned['url'] = item['url'];
}
if (item['mediaId']) {
cleaned['mediaId'] = item['mediaId'];
}
if (item['altText']) {
cleaned['altText'] = item['altText'];
}
return cleaned;
});
}
if (firstComment) {
body['firstComment'] = { text: firstComment };
}
if (!publishImmediately && scheduledAt) {
body['scheduledAt'] = scheduledAt;
}
if (profileId) {
body['profileId'] = profileId;
}

const response = await letmepostApiCall<Record<string, unknown>>({
auth: context.auth,
method: HttpMethod.POST,
path: '/v1/posts',
body,
headers: idempotencyKey
? { 'Idempotency-Key': idempotencyKey }
: undefined,
});

return response.body;
},
});
Loading
Loading