import axios, { AxiosError } from 'axios';
import type { PinterestContent, PinterestPinResponse } from '../../types/index.js';
import { logger } from '../../core/logger.js';
const PINTEREST_API_BASE = 'https://api.pinterest.com/v5';
interface PinterestPinData {
board_id: string;
media_source: {
source_type: string;
url?: string;
media_id?: string;
};
title?: string;
description?: string;
link?: string;
alt_text?: string;
publish_at?: string; // ISO 8601 timestamp for scheduled publication
}
/**
* Validation constraints for Pinterest API fields
*/
const FIELD_LIMITS = {
title: 100,
description: 800,
link: 2048,
alt_text: 500
} as const;
/**
* Validate field length according to Pinterest API constraints
*/
const validateFieldLength = (field: string, value: string, fieldName: string): void => {
const maxLength = FIELD_LIMITS[field as keyof typeof FIELD_LIMITS];
if (maxLength && value.length > maxLength) {
throw new Error(`${fieldName} exceeds maximum length of ${maxLength} characters. Current length: ${value.length}`);
}
};
interface PinterestBoardsResponse {
items: Array<{
id: string;
name: string;
[key: string]: unknown;
}>;
}
/**
* Pinterest API Service
*/
export class PinterestService {
private accessToken: string;
private boardId: string;
private instagramUsername: string;
constructor() {
this.accessToken = process.env.PINTEREST_ACCESS_TOKEN || '';
this.boardId = process.env.PINTEREST_BOARD_ID || '';
this.instagramUsername = process.env.INSTAGRAM_USERNAME || 'your_instagram_handle';
}
/**
* Check if Pinterest is configured
*/
isConfigured(): boolean {
return !!(this.accessToken && this.boardId);
}
/**
* Create a pin on Pinterest
* @param imageUrl - CDN URL to the image
* @param content - Pinterest content
* @param publishAt - ISO 8601 timestamp for scheduled publication (optional)
*/
async createDraftPin(
imageUrl: string,
content: PinterestContent,
publishAt: string | null = null
): Promise<PinterestPinResponse> {
try {
if (!this.accessToken || !this.boardId) {
throw new Error('Pinterest credentials not configured. Please set PINTEREST_ACCESS_TOKEN and PINTEREST_BOARD_ID');
}
// Validate field lengths
validateFieldLength('title', content.title, 'Title');
const fullDescription = `${content.description}\n\n${content.cta}`;
validateFieldLength('description', fullDescription, 'Description');
const instagramLink = `https://www.instagram.com/${this.instagramUsername}/`;
validateFieldLength('link', instagramLink, 'Link');
validateFieldLength('alt_text', content.title, 'Alt text');
// Pinterest API accepts direct image URLs
const pinData: PinterestPinData = {
board_id: this.boardId,
media_source: {
source_type: 'image_url',
url: imageUrl
},
title: content.title,
description: fullDescription,
link: instagramLink,
alt_text: content.title
};
// Add scheduling if provided (ISO 8601 timestamp)
if (publishAt) {
pinData.publish_at = publishAt;
}
logger.debug('Creating Pinterest pin', {
imageUrl,
boardId: this.boardId,
publishAt
});
const response = await axios.post<PinterestPinResponse>(
`${PINTEREST_API_BASE}/pins`,
pinData,
{
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
}
}
);
logger.info('Pinterest pin created', { pinId: response.data.id });
return response.data;
} catch (error) {
const axiosError = error as AxiosError;
logger.error('Error creating Pinterest pin', error instanceof Error ? error : new Error(String(error)), {
imageUrl,
response: axiosError.response?.data
});
throw error;
}
}
/**
* Schedule a pin for future publication
* @param imageUrl - CDN URL to the image
* @param content - Pinterest content
* @param publishDate - Date to publish (will be converted to ISO 8601 timestamp)
*/
async schedulePin(
imageUrl: string,
content: PinterestContent,
publishDate: Date
): Promise<PinterestPinResponse> {
const publishAt = publishDate.toISOString();
return await this.createDraftPin(imageUrl, content, publishAt);
}
/**
* Get all boards for the authenticated user
*/
async getBoards(): Promise<Array<{ id: string; name: string; [key: string]: unknown }>> {
try {
if (!this.accessToken) {
throw new Error('Pinterest access token not configured');
}
const response = await axios.get<PinterestBoardsResponse>(
`${PINTEREST_API_BASE}/boards`,
{
headers: {
'Authorization': `Bearer ${this.accessToken}`
},
params: {
page_size: 25
}
}
);
return response.data.items;
} catch (error) {
const axiosError = error as AxiosError;
console.error('Error fetching boards:', axiosError.response?.data || axiosError.message);
throw error;
}
}
}
// Export singleton instance
export const pinterestService = new PinterestService();