@agnocon/piece-buffer 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.MIT-AP +24 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/actions/create-idea.d.ts +8 -0
- package/dist/lib/actions/create-idea.d.ts.map +1 -0
- package/dist/lib/actions/create-idea.js +102 -0
- package/dist/lib/actions/create-idea.js.map +1 -0
- package/dist/lib/actions/create-post.d.ts +12 -0
- package/dist/lib/actions/create-post.d.ts.map +1 -0
- package/dist/lib/actions/create-post.js +141 -0
- package/dist/lib/actions/create-post.js.map +1 -0
- package/dist/lib/common/auth.d.ts +2 -0
- package/dist/lib/common/auth.d.ts.map +1 -0
- package/dist/lib/common/auth.js +37 -0
- package/dist/lib/common/auth.js.map +1 -0
- package/dist/lib/common/client.d.ts +11 -0
- package/dist/lib/common/client.d.ts.map +1 -0
- package/dist/lib/common/client.js +34 -0
- package/dist/lib/common/client.js.map +1 -0
- package/dist/lib/common/props.d.ts +52 -0
- package/dist/lib/common/props.d.ts.map +1 -0
- package/dist/lib/common/props.js +181 -0
- package/dist/lib/common/props.js.map +1 -0
- package/dist/lib/triggers/new-channel.d.ts +11 -0
- package/dist/lib/triggers/new-channel.d.ts.map +1 -0
- package/dist/lib/triggers/new-channel.js +73 -0
- package/dist/lib/triggers/new-channel.js.map +1 -0
- package/dist/lib/triggers/new-queue-item.d.ts +15 -0
- package/dist/lib/triggers/new-queue-item.d.ts.map +1 -0
- package/dist/lib/triggers/new-queue-item.js +118 -0
- package/dist/lib/triggers/new-queue-item.js.map +1 -0
- package/dist/lib/triggers/new-sent-item.d.ts +15 -0
- package/dist/lib/triggers/new-sent-item.d.ts.map +1 -0
- package/dist/lib/triggers/new-sent-item.js +117 -0
- package/dist/lib/triggers/new-sent-item.js.map +1 -0
- package/package.json +46 -0
- package/src/index.ts +33 -0
- package/src/lib/actions/create-idea.ts +123 -0
- package/src/lib/actions/create-post.ts +173 -0
- package/src/lib/common/auth.ts +31 -0
- package/src/lib/common/client.ts +44 -0
- package/src/lib/common/props.ts +224 -0
- package/src/lib/triggers/new-channel.ts +73 -0
- package/src/lib/triggers/new-queue-item.ts +122 -0
- package/src/lib/triggers/new-sent-item.ts +121 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createPiece } from '@agnocon/pieces-framework';
|
|
2
|
+
import { createCustomApiCallAction } from '@agnocon/pieces-common';
|
|
3
|
+
import { PieceCategory } from '@agnocon/pieces-framework';
|
|
4
|
+
import { bufferAuth } from './lib/common/auth';
|
|
5
|
+
import { bufferClient } from './lib/common/client';
|
|
6
|
+
import { createPost } from './lib/actions/create-post';
|
|
7
|
+
import { createIdea } from './lib/actions/create-idea';
|
|
8
|
+
import { newChannel } from './lib/triggers/new-channel';
|
|
9
|
+
import { newQueueItem } from './lib/triggers/new-queue-item';
|
|
10
|
+
import { newSentItem } from './lib/triggers/new-sent-item';
|
|
11
|
+
|
|
12
|
+
export const buffer = createPiece({
|
|
13
|
+
displayName: 'Buffer',
|
|
14
|
+
description:
|
|
15
|
+
'Schedule, publish and analyze social media posts across multiple channels with Buffer.',
|
|
16
|
+
auth: bufferAuth,
|
|
17
|
+
minimumSupportedRelease: '0.36.1',
|
|
18
|
+
logoUrl: 'https://cdn.activepieces.com/pieces/buffer.png',
|
|
19
|
+
categories: [PieceCategory.MARKETING],
|
|
20
|
+
authors: ['sanket-a11y'],
|
|
21
|
+
actions: [
|
|
22
|
+
createPost,
|
|
23
|
+
createIdea,
|
|
24
|
+
createCustomApiCallAction({
|
|
25
|
+
baseUrl: () => bufferClient.apiUrl,
|
|
26
|
+
auth: bufferAuth,
|
|
27
|
+
authMapping: async (auth) => ({
|
|
28
|
+
Authorization: `Bearer ${auth.secret_text}`,
|
|
29
|
+
}),
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
triggers: [newChannel, newQueueItem, newSentItem],
|
|
33
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createAction, Property } from '@agnocon/pieces-framework';
|
|
2
|
+
import { bufferAuth } from '../common/auth';
|
|
3
|
+
import { bufferClient } from '../common/client';
|
|
4
|
+
import { bufferProps } from '../common/props';
|
|
5
|
+
|
|
6
|
+
type CreateIdeaResponse = {
|
|
7
|
+
createIdea: {
|
|
8
|
+
__typename?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
idea?: {
|
|
11
|
+
id: string;
|
|
12
|
+
organizationId: string;
|
|
13
|
+
groupId?: string;
|
|
14
|
+
createdAt?: number;
|
|
15
|
+
updatedAt?: number;
|
|
16
|
+
content?: {
|
|
17
|
+
title?: string;
|
|
18
|
+
text?: string;
|
|
19
|
+
media?: Array<{ url: string; type: string; alt?: string }>;
|
|
20
|
+
tags?: Array<{ id: string; name: string; color?: string }>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
refreshIdeas?: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CREATE_IDEA_MUTATION = `
|
|
28
|
+
mutation CreateIdea($input: CreateIdeaInput!) {
|
|
29
|
+
createIdea(input: $input) {
|
|
30
|
+
__typename
|
|
31
|
+
... on IdeaResponse {
|
|
32
|
+
idea {
|
|
33
|
+
id
|
|
34
|
+
organizationId
|
|
35
|
+
groupId
|
|
36
|
+
createdAt
|
|
37
|
+
updatedAt
|
|
38
|
+
content {
|
|
39
|
+
title
|
|
40
|
+
text
|
|
41
|
+
media { url type alt }
|
|
42
|
+
tags { id name color }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
refreshIdeas
|
|
46
|
+
}
|
|
47
|
+
... on MutationError {
|
|
48
|
+
message
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
export const createIdea = createAction({
|
|
55
|
+
auth: bufferAuth,
|
|
56
|
+
name: 'create_idea',
|
|
57
|
+
displayName: 'Create Idea',
|
|
58
|
+
description: "Save a new idea to your Buffer Idea Bank.",
|
|
59
|
+
audience: 'both',
|
|
60
|
+
aiMetadata: {
|
|
61
|
+
description:
|
|
62
|
+
'Saves a new draft idea (title, body text, and/or attached images) to a Buffer organization\'s Idea Bank for later use. Choose it to stash content concepts without scheduling them to a channel. The idea must include at least a title, text, or one image. Not idempotent — each call creates a new idea.',
|
|
63
|
+
idempotent: false,
|
|
64
|
+
},
|
|
65
|
+
props: {
|
|
66
|
+
organizationId: bufferProps.organizationId(),
|
|
67
|
+
title: Property.ShortText({
|
|
68
|
+
displayName: 'Title',
|
|
69
|
+
description: 'A short title or headline for the idea.',
|
|
70
|
+
required: false,
|
|
71
|
+
}),
|
|
72
|
+
text: Property.LongText({
|
|
73
|
+
displayName: 'Text',
|
|
74
|
+
description: 'The main body of the idea.',
|
|
75
|
+
required: false,
|
|
76
|
+
}),
|
|
77
|
+
imageUrls: Property.Array({
|
|
78
|
+
displayName: 'Image URLs',
|
|
79
|
+
description: 'Public URLs of images to attach to the idea.',
|
|
80
|
+
required: false,
|
|
81
|
+
}),
|
|
82
|
+
aiAssisted: Property.Checkbox({
|
|
83
|
+
displayName: 'AI Assisted',
|
|
84
|
+
description: 'Mark this idea as created with AI assistance.',
|
|
85
|
+
required: false,
|
|
86
|
+
defaultValue: false,
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
async run(context) {
|
|
90
|
+
const { organizationId, title, text, imageUrls, aiAssisted } =
|
|
91
|
+
context.propsValue;
|
|
92
|
+
|
|
93
|
+
if (!title && !text && (!imageUrls || imageUrls.length === 0)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
'An idea must include at least a title, text, or one image.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const media = (imageUrls ?? []).map((url) => ({
|
|
100
|
+
url: url as string,
|
|
101
|
+
type: 'image',
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
const content: Record<string, unknown> = {};
|
|
105
|
+
if (title) content['title'] = title;
|
|
106
|
+
if (text) content['text'] = text;
|
|
107
|
+
if (media.length > 0) content['media'] = media;
|
|
108
|
+
if (aiAssisted) content['aiAssisted'] = true;
|
|
109
|
+
|
|
110
|
+
const data = await bufferClient.graphql<CreateIdeaResponse>({
|
|
111
|
+
accessToken: context.auth.secret_text,
|
|
112
|
+
query: CREATE_IDEA_MUTATION,
|
|
113
|
+
variables: { input: { organizationId, content } },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (data.createIdea.message) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Buffer rejected the idea: ${data.createIdea.message}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return data;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createAction, Property } from '@agnocon/pieces-framework';
|
|
2
|
+
import { bufferAuth } from '../common/auth';
|
|
3
|
+
import { bufferClient } from '../common/client';
|
|
4
|
+
import { bufferProps } from '../common/props';
|
|
5
|
+
|
|
6
|
+
type CreatePostResponse = {
|
|
7
|
+
createPost: {
|
|
8
|
+
__typename?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
post?: {
|
|
11
|
+
id: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
status?: string;
|
|
14
|
+
dueAt?: string;
|
|
15
|
+
sentAt?: string;
|
|
16
|
+
channelId?: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const CREATE_POST_MUTATION = `
|
|
22
|
+
mutation CreatePost($input: CreatePostInput!) {
|
|
23
|
+
createPost(input: $input) {
|
|
24
|
+
__typename
|
|
25
|
+
... on PostActionSuccess {
|
|
26
|
+
post {
|
|
27
|
+
id
|
|
28
|
+
text
|
|
29
|
+
status
|
|
30
|
+
dueAt
|
|
31
|
+
sentAt
|
|
32
|
+
channelId
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
... on MutationError {
|
|
36
|
+
message
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export const createPost = createAction({
|
|
43
|
+
auth: bufferAuth,
|
|
44
|
+
name: 'create_post',
|
|
45
|
+
displayName: 'Create Post',
|
|
46
|
+
description:
|
|
47
|
+
'Create a post in Buffer and add it to the queue, share it next, share it now, or schedule it for a custom time.',
|
|
48
|
+
audience: 'both',
|
|
49
|
+
aiMetadata: {
|
|
50
|
+
description:
|
|
51
|
+
'Creates a social media post in Buffer for one or more channels, where the share mode controls timing: add to the channel queue, share next (skip the queue), share now, or schedule for a custom time. Choose it to draft or publish content to connected social accounts. A custom-scheduled post requires the scheduled time; posting to multiple channels creates a separate post per channel. Not idempotent — each call creates new posts.',
|
|
52
|
+
idempotent: false,
|
|
53
|
+
},
|
|
54
|
+
props: {
|
|
55
|
+
organizationId: bufferProps.organizationId(),
|
|
56
|
+
channelIds: bufferProps.channelIds(true),
|
|
57
|
+
text: Property.LongText({
|
|
58
|
+
displayName: 'Text',
|
|
59
|
+
description: 'The text content of the post.',
|
|
60
|
+
required: true,
|
|
61
|
+
}),
|
|
62
|
+
mode: Property.StaticDropdown({
|
|
63
|
+
displayName: 'Share Mode',
|
|
64
|
+
description: 'When and how the post should be published.',
|
|
65
|
+
required: true,
|
|
66
|
+
defaultValue: 'addToQueue',
|
|
67
|
+
options: {
|
|
68
|
+
disabled: false,
|
|
69
|
+
options: [
|
|
70
|
+
{ label: 'Add to Queue', value: 'addToQueue' },
|
|
71
|
+
{ label: 'Share Next (skip the queue)', value: 'shareNext' },
|
|
72
|
+
{ label: 'Share Now', value: 'shareNow' },
|
|
73
|
+
{ label: 'Schedule for Custom Time', value: 'customScheduled' },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
dueAt: Property.DateTime({
|
|
78
|
+
displayName: 'Scheduled Time',
|
|
79
|
+
description:
|
|
80
|
+
'When to publish the post. Required when Share Mode is "Schedule for Custom Time". ISO 8601 (UTC).',
|
|
81
|
+
required: false,
|
|
82
|
+
}),
|
|
83
|
+
schedulingType: Property.StaticDropdown({
|
|
84
|
+
displayName: 'Scheduling Type',
|
|
85
|
+
description:
|
|
86
|
+
'Use "Automatic" for channels Buffer can publish to directly. Use "Notification" for channels that require manual publishing via the Buffer mobile app (e.g. Instagram personal accounts).',
|
|
87
|
+
required: true,
|
|
88
|
+
defaultValue: 'automatic',
|
|
89
|
+
options: {
|
|
90
|
+
disabled: false,
|
|
91
|
+
options: [
|
|
92
|
+
{ label: 'Automatic', value: 'automatic' },
|
|
93
|
+
{ label: 'Notification (manual publish)', value: 'notification' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
saveToDraft: Property.Checkbox({
|
|
98
|
+
displayName: 'Save as Draft',
|
|
99
|
+
description:
|
|
100
|
+
'Save the post as a draft instead of scheduling it. Drafts are not published until explicitly scheduled.',
|
|
101
|
+
required: false,
|
|
102
|
+
defaultValue: false,
|
|
103
|
+
}),
|
|
104
|
+
imageUrls: Property.Array({
|
|
105
|
+
displayName: 'Image URLs',
|
|
106
|
+
description: 'Public URLs of images to attach to the post.',
|
|
107
|
+
required: false,
|
|
108
|
+
}),
|
|
109
|
+
linkUrl: Property.ShortText({
|
|
110
|
+
displayName: 'Link URL',
|
|
111
|
+
description: 'A link to attach to the post (used as a link preview).',
|
|
112
|
+
required: false,
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
async run(context) {
|
|
116
|
+
const {
|
|
117
|
+
channelIds,
|
|
118
|
+
text,
|
|
119
|
+
mode,
|
|
120
|
+
dueAt,
|
|
121
|
+
schedulingType,
|
|
122
|
+
saveToDraft,
|
|
123
|
+
imageUrls,
|
|
124
|
+
linkUrl,
|
|
125
|
+
} = context.propsValue;
|
|
126
|
+
|
|
127
|
+
if (mode === 'customScheduled' && !dueAt) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'Scheduled Time is required when Share Mode is "Schedule for Custom Time".',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const assets: Array<Record<string, unknown>> = [];
|
|
134
|
+
for (const url of imageUrls ?? []) {
|
|
135
|
+
assets.push({ image: { url: url as string } });
|
|
136
|
+
}
|
|
137
|
+
if (linkUrl) {
|
|
138
|
+
assets.push({ link: { url: linkUrl } });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const results: CreatePostResponse['createPost'][] = [];
|
|
142
|
+
for (const channelId of channelIds ?? []) {
|
|
143
|
+
const input: Record<string, unknown> = {
|
|
144
|
+
channelId,
|
|
145
|
+
text,
|
|
146
|
+
mode,
|
|
147
|
+
schedulingType,
|
|
148
|
+
assets,
|
|
149
|
+
};
|
|
150
|
+
if (mode === 'customScheduled' && dueAt) {
|
|
151
|
+
input['dueAt'] = dueAt;
|
|
152
|
+
}
|
|
153
|
+
if (saveToDraft) {
|
|
154
|
+
input['saveToDraft'] = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const data = await bufferClient.graphql<CreatePostResponse>({
|
|
158
|
+
accessToken: context.auth.secret_text,
|
|
159
|
+
query: CREATE_POST_MUTATION,
|
|
160
|
+
variables: { input },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (data.createPost.message) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Buffer rejected the post for channel ${channelId}: ${data.createPost.message}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
results.push(data.createPost);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { posts: results };
|
|
172
|
+
},
|
|
173
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PieceAuth } from '@agnocon/pieces-framework';
|
|
2
|
+
import { bufferClient } from './client';
|
|
3
|
+
|
|
4
|
+
export const bufferAuth = PieceAuth.SecretText({
|
|
5
|
+
displayName: 'Access Token',
|
|
6
|
+
description: `
|
|
7
|
+
**How to get your Buffer access token:**
|
|
8
|
+
|
|
9
|
+
1. Sign in to Buffer at https://buffer.com and open the [Developer Apps](https://publish.buffer.com/developers/apps) page.
|
|
10
|
+
2. Click **Create New App** and fill in the basic details (callback URL can be any placeholder).
|
|
11
|
+
3. After the app is created, copy the **Access Token** shown on the app's page.
|
|
12
|
+
4. Paste it below.
|
|
13
|
+
|
|
14
|
+
The token is sent as a Bearer token to the Buffer GraphQL API (https://api.buffer.com).`,
|
|
15
|
+
required: true,
|
|
16
|
+
validate: async ({ auth }) => {
|
|
17
|
+
try {
|
|
18
|
+
await bufferClient.graphql<{ account: { id: string } }>({
|
|
19
|
+
accessToken: auth,
|
|
20
|
+
query: `query { account { id } }`,
|
|
21
|
+
});
|
|
22
|
+
return { valid: true };
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return {
|
|
25
|
+
valid: false,
|
|
26
|
+
error:
|
|
27
|
+
'Invalid Buffer access token. Make sure the token has the required scopes and try again.',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { httpClient, HttpMethod } from '@agnocon/pieces-common';
|
|
2
|
+
|
|
3
|
+
const BUFFER_API_URL = 'https://api.buffer.com';
|
|
4
|
+
|
|
5
|
+
type GraphQLResponse<T> = {
|
|
6
|
+
data?: T;
|
|
7
|
+
errors?: Array<{ message: string; path?: string[]; extensions?: unknown }>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
async function graphqlRequest<T>({
|
|
11
|
+
accessToken,
|
|
12
|
+
query,
|
|
13
|
+
variables,
|
|
14
|
+
}: {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
query: string;
|
|
17
|
+
variables?: Record<string, unknown>;
|
|
18
|
+
}): Promise<T> {
|
|
19
|
+
const response = await httpClient.sendRequest<GraphQLResponse<T>>({
|
|
20
|
+
method: HttpMethod.POST,
|
|
21
|
+
url: BUFFER_API_URL,
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${accessToken}`,
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
},
|
|
26
|
+
body: { query, variables: variables ?? {} },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const body = response.body;
|
|
30
|
+
if (body.errors && body.errors.length > 0) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Buffer API error: ${body.errors.map((e) => e.message).join('; ')}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!body.data) {
|
|
36
|
+
throw new Error('Buffer API returned no data');
|
|
37
|
+
}
|
|
38
|
+
return body.data;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const bufferClient = {
|
|
42
|
+
graphql: graphqlRequest,
|
|
43
|
+
apiUrl: BUFFER_API_URL,
|
|
44
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Property } from '@agnocon/pieces-framework';
|
|
2
|
+
import { bufferAuth } from './auth';
|
|
3
|
+
import { bufferClient } from './client';
|
|
4
|
+
|
|
5
|
+
async function fetchOrganizations(accessToken: string): Promise<Organization[]> {
|
|
6
|
+
const data = await bufferClient.graphql<{
|
|
7
|
+
account: { organizations: Organization[] };
|
|
8
|
+
}>({
|
|
9
|
+
accessToken,
|
|
10
|
+
query: `query { account { organizations { id name } } }`,
|
|
11
|
+
});
|
|
12
|
+
return data.account?.organizations ?? [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function fetchChannels(
|
|
16
|
+
accessToken: string,
|
|
17
|
+
organizationId: string,
|
|
18
|
+
): Promise<Channel[]> {
|
|
19
|
+
const data = await bufferClient.graphql<{ channels: Channel[] }>({
|
|
20
|
+
accessToken,
|
|
21
|
+
query: `query Channels($organizationId: OrganizationId!) {
|
|
22
|
+
channels(input: { organizationId: $organizationId }) {
|
|
23
|
+
id
|
|
24
|
+
name
|
|
25
|
+
service
|
|
26
|
+
organizationId
|
|
27
|
+
createdAt
|
|
28
|
+
}
|
|
29
|
+
}`,
|
|
30
|
+
variables: { organizationId },
|
|
31
|
+
});
|
|
32
|
+
return data.channels ?? [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchPosts({
|
|
36
|
+
accessToken,
|
|
37
|
+
organizationId,
|
|
38
|
+
channelIds,
|
|
39
|
+
statusFilter,
|
|
40
|
+
first = 50,
|
|
41
|
+
}: {
|
|
42
|
+
accessToken: string;
|
|
43
|
+
organizationId: string;
|
|
44
|
+
channelIds?: string[];
|
|
45
|
+
statusFilter?: (status: string | undefined) => boolean;
|
|
46
|
+
first?: number;
|
|
47
|
+
}): Promise<BufferPost[]> {
|
|
48
|
+
const filter: Record<string, unknown> = {};
|
|
49
|
+
if (channelIds && channelIds.length > 0) filter['channelIds'] = channelIds;
|
|
50
|
+
|
|
51
|
+
const data = await bufferClient.graphql<{
|
|
52
|
+
posts: { edges?: Array<{ node: BufferPost }> };
|
|
53
|
+
}>({
|
|
54
|
+
accessToken,
|
|
55
|
+
query: `query Posts($input: PostsInput!, $first: Int) {
|
|
56
|
+
posts(input: $input, first: $first) {
|
|
57
|
+
edges {
|
|
58
|
+
node {
|
|
59
|
+
id
|
|
60
|
+
text
|
|
61
|
+
status
|
|
62
|
+
createdAt
|
|
63
|
+
updatedAt
|
|
64
|
+
dueAt
|
|
65
|
+
sentAt
|
|
66
|
+
channelId
|
|
67
|
+
channelService
|
|
68
|
+
channel { id name service }
|
|
69
|
+
tags { id name }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}`,
|
|
74
|
+
variables: {
|
|
75
|
+
input: {
|
|
76
|
+
organizationId,
|
|
77
|
+
...(Object.keys(filter).length > 0 ? { filter } : {}),
|
|
78
|
+
},
|
|
79
|
+
first,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const posts = (data.posts?.edges ?? []).map((edge) => edge.node);
|
|
83
|
+
return statusFilter ? posts.filter((post) => statusFilter(post.status)) : posts;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const bufferProps = {
|
|
87
|
+
organizationId: () =>
|
|
88
|
+
Property.Dropdown<string, true, typeof bufferAuth>({
|
|
89
|
+
auth: bufferAuth,
|
|
90
|
+
displayName: 'Organization',
|
|
91
|
+
description: 'The Buffer organization to use.',
|
|
92
|
+
required: true,
|
|
93
|
+
refreshers: [],
|
|
94
|
+
options: async ({ auth }) => {
|
|
95
|
+
if (!auth) {
|
|
96
|
+
return {
|
|
97
|
+
disabled: true,
|
|
98
|
+
placeholder: 'Connect your Buffer account first.',
|
|
99
|
+
options: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const organizations = await fetchOrganizations(auth.secret_text);
|
|
104
|
+
return {
|
|
105
|
+
disabled: false,
|
|
106
|
+
options: organizations.map((org) => ({
|
|
107
|
+
label: org.name,
|
|
108
|
+
value: org.id,
|
|
109
|
+
})),
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
return {
|
|
113
|
+
disabled: true,
|
|
114
|
+
placeholder: 'Failed to load organizations.',
|
|
115
|
+
options: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
channelIds: (required = true) =>
|
|
122
|
+
Property.MultiSelectDropdown<string, boolean, typeof bufferAuth>({
|
|
123
|
+
auth: bufferAuth,
|
|
124
|
+
displayName: 'Channels',
|
|
125
|
+
description: 'The Buffer channels to publish to.',
|
|
126
|
+
required,
|
|
127
|
+
refreshers: ['organizationId'],
|
|
128
|
+
options: async ({ auth, organizationId }) => {
|
|
129
|
+
if (!auth || !organizationId) {
|
|
130
|
+
return {
|
|
131
|
+
disabled: true,
|
|
132
|
+
placeholder: 'Select an organization first.',
|
|
133
|
+
options: [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const channels = await fetchChannels(
|
|
138
|
+
auth.secret_text,
|
|
139
|
+
organizationId as string,
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
disabled: false,
|
|
143
|
+
options: channels.map((channel) => ({
|
|
144
|
+
label: `${channel.name} (${channel.service})`,
|
|
145
|
+
value: channel.id,
|
|
146
|
+
})),
|
|
147
|
+
};
|
|
148
|
+
} catch {
|
|
149
|
+
return {
|
|
150
|
+
disabled: true,
|
|
151
|
+
placeholder: 'Failed to load channels.',
|
|
152
|
+
options: [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
|
|
158
|
+
channelId: () =>
|
|
159
|
+
Property.Dropdown<string, true, typeof bufferAuth>({
|
|
160
|
+
auth: bufferAuth,
|
|
161
|
+
displayName: 'Channel',
|
|
162
|
+
description: 'The Buffer channel.',
|
|
163
|
+
required: true,
|
|
164
|
+
refreshers: ['organizationId'],
|
|
165
|
+
options: async ({ auth, organizationId }) => {
|
|
166
|
+
if (!auth || !organizationId) {
|
|
167
|
+
return {
|
|
168
|
+
disabled: true,
|
|
169
|
+
placeholder: 'Select an organization first.',
|
|
170
|
+
options: [],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const channels = await fetchChannels(
|
|
175
|
+
auth.secret_text,
|
|
176
|
+
organizationId as string,
|
|
177
|
+
);
|
|
178
|
+
return {
|
|
179
|
+
disabled: false,
|
|
180
|
+
options: channels.map((channel) => ({
|
|
181
|
+
label: `${channel.name} (${channel.service})`,
|
|
182
|
+
value: channel.id,
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
} catch {
|
|
186
|
+
return {
|
|
187
|
+
disabled: true,
|
|
188
|
+
placeholder: 'Failed to load channels.',
|
|
189
|
+
options: [],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const bufferQueries = {
|
|
197
|
+
fetchOrganizations,
|
|
198
|
+
fetchChannels,
|
|
199
|
+
fetchPosts,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export type Organization = { id: string; name: string };
|
|
203
|
+
|
|
204
|
+
export type Channel = {
|
|
205
|
+
id: string;
|
|
206
|
+
name: string;
|
|
207
|
+
service: string;
|
|
208
|
+
organizationId: string;
|
|
209
|
+
createdAt?: string;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export type BufferPost = {
|
|
213
|
+
id: string;
|
|
214
|
+
text?: string;
|
|
215
|
+
status?: string;
|
|
216
|
+
createdAt?: string;
|
|
217
|
+
updatedAt?: string;
|
|
218
|
+
dueAt?: string;
|
|
219
|
+
sentAt?: string;
|
|
220
|
+
channelId?: string;
|
|
221
|
+
channelService?: string;
|
|
222
|
+
channel?: { id: string; name: string; service: string };
|
|
223
|
+
tags?: Array<{ id: string; name: string }>;
|
|
224
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTrigger,
|
|
3
|
+
TriggerStrategy,
|
|
4
|
+
AppConnectionValueForAuthProperty,
|
|
5
|
+
} from '@agnocon/pieces-framework';
|
|
6
|
+
import {
|
|
7
|
+
DedupeStrategy,
|
|
8
|
+
Polling,
|
|
9
|
+
pollingHelper,
|
|
10
|
+
} from '@agnocon/pieces-common';
|
|
11
|
+
import { bufferAuth } from '../common/auth';
|
|
12
|
+
import { bufferProps, bufferQueries, Channel } from '../common/props';
|
|
13
|
+
|
|
14
|
+
const polling: Polling<
|
|
15
|
+
AppConnectionValueForAuthProperty<typeof bufferAuth>,
|
|
16
|
+
{ organizationId: string }
|
|
17
|
+
> = {
|
|
18
|
+
strategy: DedupeStrategy.TIMEBASED,
|
|
19
|
+
async items({ auth, propsValue }) {
|
|
20
|
+
const channels = await bufferQueries.fetchChannels(
|
|
21
|
+
auth.secret_text,
|
|
22
|
+
propsValue.organizationId,
|
|
23
|
+
);
|
|
24
|
+
return channels.map((channel) => ({
|
|
25
|
+
epochMilliSeconds: channel.createdAt
|
|
26
|
+
? new Date(channel.createdAt).getTime()
|
|
27
|
+
: 0,
|
|
28
|
+
data: channel,
|
|
29
|
+
}));
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const newChannel = createTrigger({
|
|
34
|
+
auth: bufferAuth,
|
|
35
|
+
name: 'new_channel',
|
|
36
|
+
displayName: 'New Channel',
|
|
37
|
+
description: 'Triggers when a new channel is connected to your Buffer organization.',
|
|
38
|
+
aiMetadata: {
|
|
39
|
+
description:
|
|
40
|
+
'Fires when a new social media channel (e.g. a Facebook page, Instagram or X account) is connected to the selected Buffer organization, representing the newly linked channel.',
|
|
41
|
+
},
|
|
42
|
+
type: TriggerStrategy.POLLING,
|
|
43
|
+
props: {
|
|
44
|
+
organizationId: bufferProps.organizationId(),
|
|
45
|
+
},
|
|
46
|
+
sampleData: {
|
|
47
|
+
id: 'channel_id_example',
|
|
48
|
+
name: 'My Page',
|
|
49
|
+
service: 'facebook',
|
|
50
|
+
organizationId: 'org_id_example',
|
|
51
|
+
createdAt: '2026-05-25T10:00:00.000Z',
|
|
52
|
+
} satisfies Channel,
|
|
53
|
+
async onEnable(context) {
|
|
54
|
+
await pollingHelper.onEnable(polling, {
|
|
55
|
+
auth: context.auth,
|
|
56
|
+
store: context.store,
|
|
57
|
+
propsValue: context.propsValue,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
async onDisable(context) {
|
|
61
|
+
await pollingHelper.onDisable(polling, {
|
|
62
|
+
auth: context.auth,
|
|
63
|
+
store: context.store,
|
|
64
|
+
propsValue: context.propsValue,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
async test(context) {
|
|
68
|
+
return await pollingHelper.test(polling, context);
|
|
69
|
+
},
|
|
70
|
+
async run(context) {
|
|
71
|
+
return await pollingHelper.poll(polling, context);
|
|
72
|
+
},
|
|
73
|
+
});
|