Skip to main content

@happyvertical/social

License: MIT

A unified interface for publishing content to social platforms in the HAVE SDK.

Overview

The @happyvertical/social package provides adapters for publishing text, images, and videos to major social platforms including YouTube, Threads, X (Twitter), and Bluesky. Each adapter implements a consistent interface, making it easy to publish to multiple platforms with the same code.

Features

  • Multi-Platform Support: YouTube, Threads, X (Twitter), Bluesky
  • Unified Interface: Consistent API across all platforms
  • OAuth Support: Built-in OAuth 2.0 with PKCE for YouTube
  • Media Publishing: Support for text, images, and video content
  • Cross-Posting: Publish to multiple platforms simultaneously
  • Analytics: Retrieve post engagement metrics
  • Platform Capabilities: Query platform-specific limits and features
  • Type-Safe: Full TypeScript support with comprehensive type definitions

Installation

# Install with bun (recommended)
bun add @happyvertical/social

# Or with npm
npm install @happyvertical/social

# Or with pnpm
pnpm add @happyvertical/social

Quick Start

Basic Usage

import { getSocial } from '@happyvertical/social';

// YouTube
const youtube = await getSocial({
type: 'youtube',
clientId: process.env.YOUTUBE_CLIENT_ID!,
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
accessToken: 'user-access-token',
refreshToken: 'user-refresh-token',
});

// Publish video
const result = await youtube.publishVideo({
file: fs.readFileSync('video.mp4'),
title: 'Breaking News from Bentley',
description: 'Latest updates from the town council meeting.',
tags: ['news', 'local', 'bentley'],
});

console.log(`Published: ${result.url}`);

Multiple Platforms

import { getSocial, getSocialMulti, publishToAll } from '@happyvertical/social';

// Create adapters for multiple platforms
const adapters = await getSocialMulti([
{
type: 'youtube',
clientId: process.env.YOUTUBE_CLIENT_ID!,
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
accessToken: 'youtube-token',
},
{
type: 'bluesky',
identifier: 'myhandle.bsky.social',
password: process.env.BLUESKY_APP_PASSWORD!,
},
{
type: 'x',
apiKey: process.env.X_API_KEY!,
apiSecret: process.env.X_API_SECRET!,
accessToken: process.env.X_ACCESS_TOKEN!,
accessSecret: process.env.X_ACCESS_SECRET!,
},
]);

// Publish to all platforms at once
const results = await publishToAll(adapters, {
type: 'text',
text: 'Breaking news from Bentley!',
linkUrl: 'https://example.com/article',
tags: ['news', 'local'],
});

// Check results per platform
for (const [platform, result] of results) {
if (result.success) {
console.log(`${platform}: Success`);
} else {
console.log(`${platform}: Failed - ${result.error?.message}`);
}
}

Platform Adapters

YouTube

const youtube = await getSocial({
type: 'youtube',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
accessToken: 'user-access-token',
refreshToken: 'user-refresh-token',
});

// Publish video (supports Shorts with 9:16 aspect ratio)
await youtube.publishVideo({
file: videoBuffer,
title: 'Video Title',
description: 'Video description with #hashtags',
tags: ['tag1', 'tag2'],
visibility: 'public', // 'public' | 'unlisted' | 'private'
scheduledAt: new Date('2025-02-01'), // Optional scheduling
thumbnail: thumbnailBuffer, // Custom thumbnail
});

// OAuth flow
const { url, state, codeVerifier } = youtube.getAuthorizationUrl({
redirectUri: 'https://yourapp.com/callback',
scopes: ['https://www.googleapis.com/auth/youtube.upload'],
});

// Exchange code for tokens
const tokens = await youtube.exchangeCode({
code: authorizationCode,
redirectUri: 'https://yourapp.com/callback',
codeVerifier,
});

Bluesky

const bluesky = await getSocial({
type: 'bluesky',
identifier: 'myhandle.bsky.social', // or DID
password: 'app-password', // Use app password, not main password
pdsUrl: 'https://bsky.social', // Optional custom PDS
});

// Authenticate
await bluesky.authenticate();

// Publish text with link card
await bluesky.publishText({
text: 'Check out this article!',
linkUrl: 'https://example.com/article',
tags: ['news'],
});

// Publish image
await bluesky.publishImage({
file: imageBuffer,
description: 'Image description',
altText: 'Accessible alt text',
});

X (Twitter)

const x = await getSocial({
type: 'x',
apiKey: 'consumer-key',
apiSecret: 'consumer-secret',
accessToken: 'user-access-token',
accessSecret: 'user-access-secret',
});

// Authenticate
await x.authenticate();

// Publish text
await x.publishText({
text: 'Hello from Bentley! 🏔️',
tags: ['news', 'local'],
});

// Publish with image
await x.publishImage({
file: imageBuffer,
description: 'Breaking news',
altText: 'News headline image',
});

// Publish video
await x.publishVideo({
file: videoBuffer,
description: 'Watch the latest update',
linkUrl: 'https://example.com', // Posted as reply for better algorithm
});

Threads

const threads = await getSocial({
type: 'threads',
accessToken: 'meta-access-token',
userId: 'threads-user-id',
});

// Publish text
await threads.publishText({
text: 'Hello from Threads!',
tags: ['meta', 'social'],
});

// Publish image (requires publicly accessible URL)
await threads.publishImage({
file: 'https://example.com/image.png', // URL required, not buffer
description: 'Image caption',
});

API Reference

SocialPlatform Interface

All adapters implement this interface:

interface SocialPlatform {
readonly platform: string;

// Authentication
authenticate(): Promise<AuthResult>;
refreshToken(token: string): Promise<AuthResult>;

// Publishing
publishVideo(video: VideoPost): Promise<PostResult>;
publishImage(image: ImagePost): Promise<PostResult>;
publishText(text: TextPost): Promise<PostResult>;

// Management
getPost(postId: string): Promise<Post>;
deletePost(postId: string): Promise<void>;
getAnalytics(postId: string): Promise<PostAnalytics>;

// Capabilities
getCapabilities(): PlatformCapabilities;
}

Platform Capabilities

const caps = youtube.getCapabilities();
console.log(`Max video length: ${caps.maxVideoLength}s`);
console.log(`Max video size: ${caps.maxVideoSize / (1024 * 1024)}MB`);
console.log(`Supports scheduling: ${caps.scheduling}`);
PlatformVideoImageTextSchedulingMax Video
YouTube256GB
Threads1GB
X512MB
BlueskyN/A

Error Handling

import {
getSocial,
SocialError,
SocialAuthError,
SocialRateLimitError,
} from '@happyvertical/social';

try {
await adapter.publishText({ text: 'Hello!' });
} catch (error) {
if (error instanceof SocialAuthError) {
console.error(`Auth error on ${error.platform}: ${error.message}`);
// Refresh token or re-authenticate
} else if (error instanceof SocialRateLimitError) {
console.error(`Rate limited. Retry after ${error.retryAfter}s`);
} else if (error instanceof SocialError) {
console.error(`Error: ${error.code} - ${error.message}`);
}
}

Types

interface VideoPost {
file: Buffer | string;
title?: string;
description?: string;
thumbnail?: Buffer | string;
tags?: string[];
linkUrl?: string;
visibility?: 'public' | 'unlisted' | 'private';
scheduledAt?: Date;
categoryId?: string; // YouTube category
}

interface ImagePost {
file: Buffer | string;
description?: string;
altText?: string;
linkUrl?: string;
tags?: string[];
}

interface TextPost {
text: string;
linkUrl?: string;
tags?: string[];
replyTo?: string; // Post ID to reply to
}

interface PostResult {
id: string;
url: string;
status: 'published' | 'scheduled' | 'processing';
publishedAt?: Date;
scheduledAt?: Date;
}

interface PostAnalytics {
views?: number;
likes?: number;
comments?: number;
shares?: number;
clicks?: number;
lastUpdated?: Date;
}

Best Practices

Credential Management

// Use environment variables
const youtube = await getSocial({
type: 'youtube',
clientId: process.env.YOUTUBE_CLIENT_ID!,
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
accessToken: await getStoredToken('youtube'),
});

// Implement token refresh
youtube.authenticate().catch(async (error) => {
if (error instanceof SocialAuthError) {
const refreshed = await youtube.refreshToken(storedRefreshToken);
await storeToken('youtube', refreshed.accessToken);
}
});

Platform-Specific Optimization

// X: Post link as reply for better algorithm performance
// The adapter handles this automatically when linkUrl is provided
await x.publishVideo({
file: videoBuffer,
description: 'Watch the news',
linkUrl: 'https://example.com/article', // Posted as separate reply
});

// YouTube: Use scheduling for optimal posting times
await youtube.publishVideo({
file: videoBuffer,
title: 'Morning News',
scheduledAt: new Date('2025-01-27T08:00:00Z'),
});

License

This package is part of the HAVE SDK and is licensed under the MIT License - see the LICENSE file for details.