rspace-online/modules/rsocials/lib/platform-specs.ts

339 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Postiz platform specifications.
*
* Character limits, required fields, media rules and postType defaults per
* Postiz provider identifier. Drives the planner palette and inline editor:
* dropping a platform onto the canvas creates a `post` node pre-tagged with
* that platform's constraints.
*
* Limits reflect Postiz 5.x provider handlers as of 2026-04. Numbers match
* what Postiz's `settings.__type` handlers enforce server-side.
*/
export type PostizPlatformId =
| 'x'
| 'linkedin'
| 'facebook'
| 'instagram'
| 'threads'
| 'bluesky'
| 'mastodon'
| 'youtube'
| 'tiktok'
| 'reddit'
| 'pinterest'
| 'discord'
| 'slack';
export type PostizMediaKind = 'image' | 'video' | 'gif';
export type PostizPostType =
| 'text'
| 'thread'
| 'image'
| 'carousel'
| 'video'
| 'short'
| 'story'
| 'reel'
| 'link';
export interface PlatformSpec {
id: PostizPlatformId;
label: string;
icon: string;
color: string;
/** Main content character limit (per segment for threads). */
charLimit: number;
/** Optional title/subject limit — rendered as a separate input when set. */
titleLimit?: number;
titleRequired?: boolean;
/** Supports a native multi-segment thread via Postiz `value[]`. */
supportsThreads: boolean;
/** Media types the platform accepts (drives the inline editor hint). */
mediaTypes: PostizMediaKind[];
/** Maximum media items per post. 0 = media not allowed. */
maxMedia: number;
/** Media is required (cannot post text-only). */
mediaRequired?: boolean;
/** Default postType when dropped on the canvas. */
defaultPostType: PostizPostType;
/** Extra postTypes selectable for this platform. */
postTypes: PostizPostType[];
/** Hashtag count limit (soft warning when exceeded). 0 = no hashtags. */
hashtagLimit: number;
/** Human-readable note displayed under the content editor. */
note: string;
/** Extra per-platform input fields rendered in the inline editor. */
extraFields?: PlatformExtraField[];
}
export interface PlatformExtraField {
key: string;
label: string;
type: 'text' | 'select' | 'textarea' | 'checkbox';
placeholder?: string;
options?: string[];
required?: boolean;
maxLength?: number;
}
export const PLATFORM_SPECS: Record<PostizPlatformId, PlatformSpec> = {
x: {
id: 'x',
label: 'X (Twitter)',
icon: '𝕏',
color: '#000000',
charLimit: 280,
supportsThreads: true,
mediaTypes: ['image', 'video', 'gif'],
maxMedia: 4,
defaultPostType: 'text',
postTypes: ['text', 'thread', 'image', 'video'],
hashtagLimit: 3,
note: '280 chars/tweet. Threads supported. Up to 4 media per tweet.',
extraFields: [
{
key: 'who_can_reply_post', label: 'Who can reply',
type: 'select', options: ['everyone', 'following', 'mentionedUsers'],
},
],
},
linkedin: {
id: 'linkedin',
label: 'LinkedIn',
icon: 'in',
color: '#0A66C2',
charLimit: 3000,
supportsThreads: false,
mediaTypes: ['image', 'video'],
maxMedia: 9,
defaultPostType: 'text',
postTypes: ['text', 'image', 'carousel', 'video'],
hashtagLimit: 5,
note: '3000 chars. Up to 9 images or 1 video. 35 hashtags recommended.',
extraFields: [
{ key: 'post_to_company', label: 'Post as company page', type: 'checkbox' },
],
},
facebook: {
id: 'facebook',
label: 'Facebook',
icon: 'f',
color: '#1877F2',
charLimit: 63206,
supportsThreads: false,
mediaTypes: ['image', 'video'],
maxMedia: 10,
defaultPostType: 'text',
postTypes: ['text', 'image', 'video', 'link'],
hashtagLimit: 6,
note: 'Effectively unlimited text. First 250 chars show in preview.',
},
instagram: {
id: 'instagram',
label: 'Instagram',
icon: '📷',
color: '#E4405F',
charLimit: 2200,
supportsThreads: false,
mediaTypes: ['image', 'video'],
maxMedia: 10,
mediaRequired: true,
defaultPostType: 'image',
postTypes: ['image', 'carousel', 'reel', 'story'],
hashtagLimit: 30,
note: '2200-char caption. Media required. Up to 10 items in a carousel.',
extraFields: [
{
key: 'post_type', label: 'Post type',
type: 'select', options: ['post', 'reel', 'story'],
},
],
},
threads: {
id: 'threads',
label: 'Threads',
icon: '@',
color: '#000000',
charLimit: 500,
supportsThreads: true,
mediaTypes: ['image', 'video'],
maxMedia: 10,
defaultPostType: 'text',
postTypes: ['text', 'thread', 'image'],
hashtagLimit: 1,
note: '500 chars/post. Threads supported. One hashtag per post.',
},
bluesky: {
id: 'bluesky',
label: 'Bluesky',
icon: '🦋',
color: '#0085FF',
charLimit: 300,
supportsThreads: true,
mediaTypes: ['image', 'gif'],
maxMedia: 4,
defaultPostType: 'text',
postTypes: ['text', 'thread', 'image'],
hashtagLimit: 3,
note: '300 chars/post. Threads supported. Up to 4 images.',
},
mastodon: {
id: 'mastodon',
label: 'Mastodon',
icon: '🐘',
color: '#6364FF',
charLimit: 500,
supportsThreads: true,
mediaTypes: ['image', 'video', 'gif'],
maxMedia: 4,
defaultPostType: 'text',
postTypes: ['text', 'thread', 'image'],
hashtagLimit: 10,
note: '500 chars/toot (instance-dependent). Threads via replies.',
extraFields: [
{
key: 'visibility', label: 'Visibility',
type: 'select', options: ['public', 'unlisted', 'private', 'direct'],
},
{ key: 'spoiler_text', label: 'Content warning', type: 'text', placeholder: 'Optional CW' },
],
},
youtube: {
id: 'youtube',
label: 'YouTube',
icon: '▶️',
color: '#FF0000',
charLimit: 5000,
titleLimit: 100,
titleRequired: true,
supportsThreads: false,
mediaTypes: ['video'],
maxMedia: 1,
mediaRequired: true,
defaultPostType: 'video',
postTypes: ['video', 'short'],
hashtagLimit: 15,
note: 'Title ≤100 chars. Description ≤5000. Video required.',
extraFields: [
{
key: 'type', label: 'Visibility',
type: 'select', options: ['public', 'unlisted', 'private'],
},
{
key: 'category', label: 'Category ID',
type: 'select',
options: ['1', '2', '10', '15', '17', '19', '20', '22', '23', '24', '25', '26', '27', '28'],
},
],
},
tiktok: {
id: 'tiktok',
label: 'TikTok',
icon: '🎵',
color: '#000000',
charLimit: 2200,
titleLimit: 150,
supportsThreads: false,
mediaTypes: ['video'],
maxMedia: 1,
mediaRequired: true,
defaultPostType: 'video',
postTypes: ['video'],
hashtagLimit: 10,
note: 'Caption ≤2200 chars. Video required. Short-form vertical.',
extraFields: [
{
key: 'privacy_level', label: 'Privacy',
type: 'select', options: ['PUBLIC_TO_EVERYONE', 'MUTUAL_FOLLOW_FRIENDS', 'SELF_ONLY'],
},
{ key: 'disable_duet', label: 'Disable duets', type: 'checkbox' },
{ key: 'disable_comment', label: 'Disable comments', type: 'checkbox' },
],
},
reddit: {
id: 'reddit',
label: 'Reddit',
icon: '🔶',
color: '#FF4500',
charLimit: 40000,
titleLimit: 300,
titleRequired: true,
supportsThreads: false,
mediaTypes: ['image', 'video'],
maxMedia: 1,
defaultPostType: 'text',
postTypes: ['text', 'link', 'image', 'video'],
hashtagLimit: 0,
note: 'Title ≤300 chars. Hashtags unused. Subreddit required.',
extraFields: [
{ key: 'subreddit', label: 'Subreddit', type: 'text', required: true, placeholder: 'r/example' },
{
key: 'flair', label: 'Flair (optional)',
type: 'text', placeholder: 'Flair text',
},
],
},
pinterest: {
id: 'pinterest',
label: 'Pinterest',
icon: '📌',
color: '#E60023',
charLimit: 500,
titleLimit: 100,
supportsThreads: false,
mediaTypes: ['image', 'video'],
maxMedia: 1,
mediaRequired: true,
defaultPostType: 'image',
postTypes: ['image', 'video'],
hashtagLimit: 20,
note: 'Pin description ≤500. Title ≤100. Image or video required.',
extraFields: [
{ key: 'link', label: 'Destination URL', type: 'text', placeholder: 'https://' },
{ key: 'board', label: 'Board', type: 'text', placeholder: 'Board name' },
],
},
discord: {
id: 'discord',
label: 'Discord',
icon: '🎮',
color: '#5865F2',
charLimit: 2000,
supportsThreads: false,
mediaTypes: ['image', 'video', 'gif'],
maxMedia: 10,
defaultPostType: 'text',
postTypes: ['text', 'image'],
hashtagLimit: 0,
note: '2000 chars per message. Posts into a configured channel.',
},
slack: {
id: 'slack',
label: 'Slack',
icon: '#',
color: '#4A154B',
charLimit: 40000,
supportsThreads: false,
mediaTypes: ['image'],
maxMedia: 10,
defaultPostType: 'text',
postTypes: ['text'],
hashtagLimit: 0,
note: 'Long-form message into a configured channel.',
},
};
export const ALL_PLATFORM_IDS: PostizPlatformId[] = [
'x', 'linkedin', 'facebook', 'instagram', 'threads', 'bluesky',
'mastodon', 'youtube', 'tiktok', 'reddit', 'pinterest', 'discord', 'slack',
];
export function getPlatformSpec(platform: string): PlatformSpec | null {
return (PLATFORM_SPECS as Record<string, PlatformSpec>)[platform] || null;
}
export function charLimitFor(platform: string): number {
return getPlatformSpec(platform)?.charLimit ?? 2200;
}