@happyvertical/social 0.74.8
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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/README.md +485 -0
- package/dist/index.d.ts +1114 -0
- package/dist/index.js +2695 -0
- package/dist/index.js.map +1 -0
- package/metadata.json +29 -0
- package/package.json +55 -0
package/AGENT.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @happyvertical/social
|
|
2
|
+
|
|
3
|
+
<!-- BEGIN AGENT:GENERATED -->
|
|
4
|
+
## Purpose
|
|
5
|
+
Social platform adapters for publishing to YouTube, Threads, X, and Bluesky
|
|
6
|
+
|
|
7
|
+
## Package Map
|
|
8
|
+
- Package: `@happyvertical/social`
|
|
9
|
+
- Hierarchy path: `@happyvertical/sdk > packages > social`
|
|
10
|
+
- Workspace position: `25 of 30` local packages
|
|
11
|
+
- Internal dependencies: `@happyvertical/logger`, `@happyvertical/utils`
|
|
12
|
+
- Internal dependents: none
|
|
13
|
+
- Knowledge graph files: `AGENT.md`, `metadata.json`, `ecosystem-manifest.json`
|
|
14
|
+
|
|
15
|
+
## Build & Test
|
|
16
|
+
```bash
|
|
17
|
+
pnpm --filter @happyvertical/social build
|
|
18
|
+
pnpm --filter @happyvertical/social test
|
|
19
|
+
pnpm --filter @happyvertical/social clean
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Agent Correction Loops
|
|
23
|
+
- If module resolution or export errors mention a workspace dependency, build the dependency first (`pnpm --filter @happyvertical/logger build`, `pnpm --filter @happyvertical/utils build`) and then rerun `pnpm --filter @happyvertical/social build`.
|
|
24
|
+
- If tests or exports fail after API, type, or bundle changes, run `pnpm --filter @happyvertical/social clean` followed by `pnpm --filter @happyvertical/social build` and `pnpm --filter @happyvertical/social test`.
|
|
25
|
+
- If failures span multiple packages or Turborepo ordering looks wrong, run `pnpm build` and `pnpm typecheck` from the repo root before retrying package-scoped commands.
|
|
26
|
+
|
|
27
|
+
## Ecosystem Relationships
|
|
28
|
+
- Provides: Social platform adapters for publishing to YouTube, Threads, X, and Bluesky
|
|
29
|
+
- Implements: none
|
|
30
|
+
- Requires: @happyvertical/logger, @happyvertical/utils
|
|
31
|
+
- Stability: stable (Primary package surface is described as implemented and production-oriented.)
|
|
32
|
+
<!-- END AGENT:GENERATED -->
|
|
33
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: social
|
|
3
|
+
title: "@happyvertical/social: Social Platform Publishing"
|
|
4
|
+
sidebar_label: "@happyvertical/social"
|
|
5
|
+
sidebar_position: 11
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# @happyvertical/social
|
|
9
|
+
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
|
|
12
|
+
A unified interface for publishing content to social platforms in the HAVE SDK.
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
The `@happyvertical/social` package provides adapters for publishing links, text, images, and videos to major social platforms including YouTube, Facebook Pages, Threads, X (Twitter), and Bluesky. Each adapter implements a consistent interface, making it easy to publish to multiple platforms with the same code.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Multi-Platform Support**: YouTube, Facebook Pages, Threads, X (Twitter), Bluesky
|
|
21
|
+
- **Unified Interface**: Consistent API across all platforms
|
|
22
|
+
- **OAuth Support**: Built-in OAuth 2.0 with PKCE for YouTube and OAuth 1.0a/OAuth 2.0 support for X
|
|
23
|
+
- **Media Publishing**: Support for link, text, image, and video content
|
|
24
|
+
- **Safety Modes**: Dry-run and non-public publish modes for testing without public posts
|
|
25
|
+
- **Cross-Posting**: Publish to multiple platforms simultaneously
|
|
26
|
+
- **Analytics**: Retrieve post engagement metrics
|
|
27
|
+
- **Platform Capabilities**: Query platform-specific limits and features
|
|
28
|
+
- **Type-Safe**: Full TypeScript support with comprehensive type definitions
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Install with bun (recommended)
|
|
34
|
+
bun add @happyvertical/social
|
|
35
|
+
|
|
36
|
+
# Or with npm
|
|
37
|
+
npm install @happyvertical/social
|
|
38
|
+
|
|
39
|
+
# Or with pnpm
|
|
40
|
+
pnpm add @happyvertical/social
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Basic Usage
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { getSocial } from '@happyvertical/social';
|
|
49
|
+
|
|
50
|
+
// YouTube
|
|
51
|
+
const youtube = await getSocial({
|
|
52
|
+
type: 'youtube',
|
|
53
|
+
clientId: process.env.YOUTUBE_CLIENT_ID!,
|
|
54
|
+
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
|
|
55
|
+
accessToken: 'user-access-token',
|
|
56
|
+
refreshToken: 'user-refresh-token',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Publish video
|
|
60
|
+
const result = await youtube.publishVideo({
|
|
61
|
+
file: fs.readFileSync('video.mp4'),
|
|
62
|
+
title: 'Breaking News from Bentley',
|
|
63
|
+
description: 'Latest updates from the town council meeting.',
|
|
64
|
+
tags: ['news', 'local', 'bentley'],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log(`Published: ${result.url}`);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Multiple Platforms
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { getSocial, getSocialMulti, publishToAll } from '@happyvertical/social';
|
|
74
|
+
|
|
75
|
+
// Create adapters for multiple platforms
|
|
76
|
+
const adapters = await getSocialMulti([
|
|
77
|
+
{
|
|
78
|
+
type: 'youtube',
|
|
79
|
+
clientId: process.env.YOUTUBE_CLIENT_ID!,
|
|
80
|
+
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
|
|
81
|
+
accessToken: 'youtube-token',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: 'bluesky',
|
|
85
|
+
identifier: 'myhandle.bsky.social',
|
|
86
|
+
password: process.env.BLUESKY_APP_PASSWORD!,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'x',
|
|
90
|
+
apiKey: process.env.X_API_KEY!,
|
|
91
|
+
apiSecret: process.env.X_API_SECRET!,
|
|
92
|
+
accessToken: process.env.X_ACCESS_TOKEN!,
|
|
93
|
+
accessSecret: process.env.X_ACCESS_SECRET!,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'facebook',
|
|
97
|
+
pageId: process.env.FACEBOOK_PAGE_ID!,
|
|
98
|
+
accessToken: process.env.FACEBOOK_PAGE_ACCESS_TOKEN!,
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
// Publish a story link to all supported platforms at once
|
|
103
|
+
const results = await publishToAll(adapters, {
|
|
104
|
+
type: 'link',
|
|
105
|
+
text: 'Breaking news from Bentley!',
|
|
106
|
+
url: 'https://example.com/article',
|
|
107
|
+
tags: ['news', 'local'],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Check results per platform
|
|
111
|
+
for (const [platform, result] of results) {
|
|
112
|
+
if (result.success) {
|
|
113
|
+
console.log(`${platform}: Success`);
|
|
114
|
+
} else {
|
|
115
|
+
console.log(`${platform}: Failed - ${result.error?.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Safety Modes
|
|
121
|
+
|
|
122
|
+
Adapters default to public publishing for backward compatibility. Set `publishMode` when testing request shapes or creating non-public platform objects.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const youtube = await getSocial({
|
|
126
|
+
type: 'youtube',
|
|
127
|
+
clientId: process.env.YOUTUBE_CLIENT_ID!,
|
|
128
|
+
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
|
|
129
|
+
accessToken: 'user-access-token',
|
|
130
|
+
publishMode: 'private_or_scheduled',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await youtube.publishVideo({
|
|
134
|
+
file: videoBuffer,
|
|
135
|
+
title: 'Council update',
|
|
136
|
+
isShort: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log(result.status); // "staged"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Platform Adapters
|
|
143
|
+
|
|
144
|
+
### YouTube
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const youtube = await getSocial({
|
|
148
|
+
type: 'youtube',
|
|
149
|
+
clientId: 'your-client-id',
|
|
150
|
+
clientSecret: 'your-client-secret',
|
|
151
|
+
accessToken: 'user-access-token',
|
|
152
|
+
refreshToken: 'user-refresh-token',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Publish video (supports Shorts with 9:16 aspect ratio)
|
|
156
|
+
await youtube.publishVideo({
|
|
157
|
+
file: videoBuffer,
|
|
158
|
+
title: 'Video Title',
|
|
159
|
+
description: 'Video description with #hashtags',
|
|
160
|
+
tags: ['tag1', 'tag2'],
|
|
161
|
+
visibility: 'public', // 'public' | 'unlisted' | 'private'
|
|
162
|
+
scheduledAt: new Date('2025-02-01'), // Optional scheduling
|
|
163
|
+
thumbnail: thumbnailBuffer, // Custom thumbnail
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// OAuth flow
|
|
167
|
+
const { url, state, codeVerifier } = youtube.getAuthorizationUrl({
|
|
168
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
169
|
+
scopes: ['https://www.googleapis.com/auth/youtube.upload'],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Exchange code for tokens
|
|
173
|
+
const tokens = await youtube.exchangeCode({
|
|
174
|
+
code: authorizationCode,
|
|
175
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
176
|
+
codeVerifier,
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Bluesky
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const bluesky = await getSocial({
|
|
184
|
+
type: 'bluesky',
|
|
185
|
+
identifier: 'myhandle.bsky.social', // or DID
|
|
186
|
+
password: 'app-password', // Use app password, not main password
|
|
187
|
+
pdsUrl: 'https://bsky.social', // Optional custom PDS
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Authenticate
|
|
191
|
+
await bluesky.authenticate();
|
|
192
|
+
|
|
193
|
+
// Publish text with link card
|
|
194
|
+
await bluesky.publishText({
|
|
195
|
+
text: 'Check out this article!',
|
|
196
|
+
linkUrl: 'https://example.com/article',
|
|
197
|
+
tags: ['news'],
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Publish image
|
|
201
|
+
await bluesky.publishImage({
|
|
202
|
+
file: imageBuffer,
|
|
203
|
+
description: 'Image description',
|
|
204
|
+
altText: 'Accessible alt text',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Publish a first-class link card
|
|
208
|
+
await bluesky.publishLink({
|
|
209
|
+
url: 'https://example.com/article',
|
|
210
|
+
title: 'Local story',
|
|
211
|
+
description: 'A short summary for the card',
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### X (Twitter)
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const x = await getSocial({
|
|
219
|
+
type: 'x',
|
|
220
|
+
apiKey: 'consumer-key',
|
|
221
|
+
apiSecret: 'consumer-secret',
|
|
222
|
+
accessToken: 'user-access-token',
|
|
223
|
+
accessSecret: 'user-access-secret',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Authenticate
|
|
227
|
+
await x.authenticate();
|
|
228
|
+
|
|
229
|
+
// Publish text
|
|
230
|
+
await x.publishText({
|
|
231
|
+
text: 'Hello from Bentley! 🏔️',
|
|
232
|
+
tags: ['news', 'local'],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Publish with image
|
|
236
|
+
await x.publishImage({
|
|
237
|
+
file: imageBuffer,
|
|
238
|
+
description: 'Breaking news',
|
|
239
|
+
altText: 'News headline image',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Publish video
|
|
243
|
+
await x.publishVideo({
|
|
244
|
+
file: videoBuffer,
|
|
245
|
+
description: 'Watch the latest update',
|
|
246
|
+
linkUrl: 'https://example.com', // Inline by default
|
|
247
|
+
linkBehavior: 'reply', // Optional: post the link as a reply instead
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Threads
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const threads = await getSocial({
|
|
255
|
+
type: 'threads',
|
|
256
|
+
accessToken: 'meta-access-token',
|
|
257
|
+
userId: 'threads-user-id',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Publish text
|
|
261
|
+
await threads.publishText({
|
|
262
|
+
text: 'Hello from Threads!',
|
|
263
|
+
tags: ['meta', 'social'],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Publish image (requires publicly accessible URL)
|
|
267
|
+
await threads.publishImage({
|
|
268
|
+
file: 'https://example.com/image.png', // URL required, not buffer
|
|
269
|
+
description: 'Image caption',
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Publish a link attachment
|
|
273
|
+
await threads.publishLink({
|
|
274
|
+
url: 'https://example.com/article',
|
|
275
|
+
text: 'Read the latest story',
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Facebook Pages
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const facebook = await getSocial({
|
|
283
|
+
type: 'facebook',
|
|
284
|
+
pageId: 'page-id',
|
|
285
|
+
accessToken: 'page-access-token',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Publish a Page feed link post
|
|
289
|
+
await facebook.publishLink({
|
|
290
|
+
url: 'https://example.com/article',
|
|
291
|
+
text: 'Read the latest story',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Create an unpublished Page post for testing
|
|
295
|
+
const safeFacebook = await getSocial({
|
|
296
|
+
type: 'facebook',
|
|
297
|
+
pageId: 'page-id',
|
|
298
|
+
accessToken: 'page-access-token',
|
|
299
|
+
publishMode: 'private_or_scheduled',
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await safeFacebook.publishText({
|
|
303
|
+
text: 'Draft post',
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## API Reference
|
|
308
|
+
|
|
309
|
+
### SocialPlatform Interface
|
|
310
|
+
|
|
311
|
+
All adapters implement this interface:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
interface SocialPlatform {
|
|
315
|
+
readonly platform: string;
|
|
316
|
+
|
|
317
|
+
// Authentication
|
|
318
|
+
authenticate(): Promise<AuthResult>;
|
|
319
|
+
refreshToken(token: string): Promise<AuthResult>;
|
|
320
|
+
|
|
321
|
+
// Publishing
|
|
322
|
+
publishVideo(video: VideoPost): Promise<PostResult>;
|
|
323
|
+
publishImage(image: ImagePost): Promise<PostResult>;
|
|
324
|
+
publishText(text: TextPost): Promise<PostResult>;
|
|
325
|
+
publishLink(link: LinkPost): Promise<PostResult>;
|
|
326
|
+
|
|
327
|
+
// Management
|
|
328
|
+
getPost(postId: string): Promise<Post>;
|
|
329
|
+
deletePost(postId: string): Promise<void>;
|
|
330
|
+
getAnalytics(postId: string): Promise<PostAnalytics>;
|
|
331
|
+
|
|
332
|
+
// Capabilities
|
|
333
|
+
getCapabilities(): PlatformCapabilities;
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Platform Capabilities
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const caps = youtube.getCapabilities();
|
|
341
|
+
console.log(`Max video length: ${caps.maxVideoLength}s`);
|
|
342
|
+
console.log(`Max video size: ${caps.maxVideoSize / (1024 * 1024)}MB`);
|
|
343
|
+
console.log(`Supports scheduling: ${caps.scheduling}`);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
| Platform | Link | Video | Image | Text | Scheduling | Safe non-public mode | Max Video |
|
|
347
|
+
|----------|------|-------|-------|------|------------|----------------------|-----------|
|
|
348
|
+
| YouTube | ✗ | ✓ | ✗ | ✗ | ✓ | private upload | 256GB |
|
|
349
|
+
| Facebook Pages | ✓ | ✓ | ✓ | ✓ | ✓ | unpublished/scheduled | 10GB |
|
|
350
|
+
| Threads | ✓ | ✓ | ✓ | ✓ | ✗ | staged container | 1GB |
|
|
351
|
+
| X | ✓ | ✓ | ✓ | ✓ | ✗ | staged media/dry run | 512MB |
|
|
352
|
+
| Bluesky | ✓ | ✗ | ✓ | ✓ | ✗ | dry run/blob staging | N/A |
|
|
353
|
+
|
|
354
|
+
## Error Handling
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
import {
|
|
358
|
+
getSocial,
|
|
359
|
+
SocialError,
|
|
360
|
+
SocialAuthError,
|
|
361
|
+
SocialRateLimitError,
|
|
362
|
+
} from '@happyvertical/social';
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
await adapter.publishText({ text: 'Hello!' });
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (error instanceof SocialAuthError) {
|
|
368
|
+
console.error(`Auth error on ${error.platform}: ${error.message}`);
|
|
369
|
+
// Refresh token or re-authenticate
|
|
370
|
+
} else if (error instanceof SocialRateLimitError) {
|
|
371
|
+
console.error(`Rate limited. Retry after ${error.retryAfter}s`);
|
|
372
|
+
} else if (error instanceof SocialError) {
|
|
373
|
+
console.error(`Error: ${error.code} - ${error.message}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Types
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
interface VideoPost {
|
|
382
|
+
file: Buffer | string;
|
|
383
|
+
title?: string;
|
|
384
|
+
description?: string;
|
|
385
|
+
thumbnail?: Buffer | string;
|
|
386
|
+
tags?: string[];
|
|
387
|
+
linkUrl?: string;
|
|
388
|
+
visibility?: 'public' | 'unlisted' | 'private';
|
|
389
|
+
scheduledAt?: Date;
|
|
390
|
+
categoryId?: string; // YouTube category
|
|
391
|
+
isShort?: boolean;
|
|
392
|
+
linkBehavior?: 'inline' | 'attachment' | 'reply' | 'none';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
interface ImagePost {
|
|
396
|
+
file: Buffer | string;
|
|
397
|
+
description?: string;
|
|
398
|
+
altText?: string;
|
|
399
|
+
linkUrl?: string;
|
|
400
|
+
tags?: string[];
|
|
401
|
+
linkBehavior?: 'inline' | 'attachment' | 'reply' | 'none';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
interface TextPost {
|
|
405
|
+
text: string;
|
|
406
|
+
linkUrl?: string;
|
|
407
|
+
tags?: string[];
|
|
408
|
+
replyTo?: string; // Post ID to reply to
|
|
409
|
+
linkBehavior?: 'inline' | 'attachment' | 'reply' | 'none';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
interface LinkPost {
|
|
413
|
+
url: string;
|
|
414
|
+
text?: string;
|
|
415
|
+
title?: string;
|
|
416
|
+
description?: string;
|
|
417
|
+
tags?: string[];
|
|
418
|
+
scheduledAt?: Date;
|
|
419
|
+
linkBehavior?: 'inline' | 'attachment' | 'reply' | 'none';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
interface PostResult {
|
|
423
|
+
id: string;
|
|
424
|
+
url: string;
|
|
425
|
+
status: 'published' | 'scheduled' | 'processing' | 'staged' | 'dry_run';
|
|
426
|
+
publishedAt?: Date;
|
|
427
|
+
scheduledAt?: Date;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
interface PostAnalytics {
|
|
431
|
+
views?: number;
|
|
432
|
+
impressions?: number;
|
|
433
|
+
likes?: number;
|
|
434
|
+
comments?: number;
|
|
435
|
+
shares?: number;
|
|
436
|
+
clicks?: number;
|
|
437
|
+
raw?: unknown;
|
|
438
|
+
lastUpdated?: Date;
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Best Practices
|
|
443
|
+
|
|
444
|
+
### Credential Management
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// Use environment variables
|
|
448
|
+
const youtube = await getSocial({
|
|
449
|
+
type: 'youtube',
|
|
450
|
+
clientId: process.env.YOUTUBE_CLIENT_ID!,
|
|
451
|
+
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
|
|
452
|
+
accessToken: await getStoredToken('youtube'),
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Implement token refresh
|
|
456
|
+
youtube.authenticate().catch(async (error) => {
|
|
457
|
+
if (error instanceof SocialAuthError) {
|
|
458
|
+
const refreshed = await youtube.refreshToken(storedRefreshToken);
|
|
459
|
+
await storeToken('youtube', refreshed.accessToken);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Platform-Specific Optimization
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// X: Inline links by default, or post links as replies per account/post
|
|
468
|
+
await x.publishVideo({
|
|
469
|
+
file: videoBuffer,
|
|
470
|
+
description: 'Watch the news',
|
|
471
|
+
linkUrl: 'https://example.com/article',
|
|
472
|
+
linkBehavior: 'reply',
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// YouTube: Use scheduling for optimal posting times
|
|
476
|
+
await youtube.publishVideo({
|
|
477
|
+
file: videoBuffer,
|
|
478
|
+
title: 'Morning News',
|
|
479
|
+
scheduledAt: new Date('2025-01-27T08:00:00Z'),
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
## License
|
|
484
|
+
|
|
485
|
+
This package is part of the HAVE SDK and is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
|