/** * WordPress dependencies */ import { _x, sprintf } from '@safe-wordpress/i18n'; /** * External dependencies */ import { keyBy, filter, find, map, without, isArray, uniq } from 'lodash'; import twitter from 'twitter-text'; import type { AndArray, Maybe, MediaItem, NonEmptyArray, OrArray, RequirementsErrorAttributes, SocialKind, SocialNetwork, SocialNetworkName, SocialNetworkSupport, Url, } from '@nelio-content/types'; const BASIC_AUTOMATION_FREQS: SocialNetwork[ 'automationFreqs' ] = { reshare: 0, publication: 1, }; const GENERIC_ERRORS = { /* translators: %s: Social network name. */ missingImage: _x( 'Image required by %s', 'text', 'nelio-content' ), /* translators: %s: Social network name. */ missingMedia: _x( 'Image or video required by %s', 'text', 'nelio-content' ), /* translators: %s: Social network name. */ missingRelatedPost: _x( 'Add a related post on %s', 'user', 'nelio-content' ), /* translators: %s: Social network name. */ missingText: _x( 'Please write status update on %s', 'user', 'nelio-content' ), /* translators: %s: Social network name. */ missingVideo: _x( 'Video required by %s', 'text', 'nelio-content' ), textTooLong: _x( 'Please write a shorter social message', 'user', 'nelio-content' ), }; const DEFAULT_SUPPORTS: NonEmptyArray< SocialNetworkSupport > = [ 'automations', 'image', 'network-image', 'network-template', 'preview', 'profile-template', 'recurrence', 'related-post', 'reusability', 'text', ]; const DEFAULT_REQUIRES: OrArray< AndArray< SocialNetworkSupport > > = [ [ 'text' ], ]; const NETWORKS: NonEmptyArray< SocialNetwork > = [ { id: 'twitter' as const, active: true, automationFreqs: { reshare: 8, publication: 2, }, labels: { add: _x( 'Add X Profile', 'command', 'nelio-content' ), name: _x( 'X', 'text', 'nelio-content' ), settings: _x( 'X Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 280, supports: [ ...DEFAULT_SUPPORTS, 'multi-image', 'video', 'buffer-connection', 'hootsuite-connection', 'link-shortener', ], supportedImageTypes: [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', ], supportedVideoTypes: [ 'video' ], requires: DEFAULT_REQUIRES, validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', ].includes( media.mime ) ) { return _x( 'Unsupported image type in X (Twitter)', 'text', 'nelio-content' ); } if ( media.mime !== 'image/gif' && megas( media.filesizeInBytes ) > 5 ) { return _x( 'X (Twitter) image should be smaller than 5MB', 'text', 'nelio-content' ); } if ( media.mime === 'image/gif' && megas( media.filesizeInBytes ) > 15 ) { return _x( 'X (Twitter) GIF image should be smaller than 15MB', 'text', 'nelio-content' ); } return undefined; }, video: ( media ) => { if ( ! media ) { return undefined; } if ( media.duration < 0.5 ) { return _x( 'X (Twitter) video must be at least half a second long', 'text', 'nelio-content' ); } if ( media.duration > 140 ) { return _x( 'X (Twitter) video must be shorter than 140 seconds', 'text', 'nelio-content' ); } if ( megas( media.filesizeInBytes ) > 512 ) { return _x( 'X (Twitter) video should be smaller than 512MB', 'text', 'nelio-content' ); } return undefined; }, }, }, { id: 'facebook' as const, active: true, automationFreqs: { reshare: 1, publication: 1, }, labels: { add: _x( 'Add Facebook Profile', 'command', 'nelio-content' ), name: _x( 'Facebook', 'text', 'nelio-content' ), settings: _x( 'Facebook Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 10000, supports: [ ...DEFAULT_SUPPORTS, 'buffer-connection', 'hootsuite-connection', 'video', 'link-shortener', ], supportedImageTypes: [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/tiff', ], supportedVideoTypes: [ 'video/mp4' ], requires: DEFAULT_REQUIRES, validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( megas( media.filesizeInBytes ) > 4 ) { return _x( 'Facebook image should be smaller than 4MB', 'text', 'nelio-content' ); } return undefined; }, video: ( media ) => { if ( ! media ) { return undefined; } if ( 'video/mp4' !== media.mime ) { return _x( 'Unsupported video type in Facebook', 'text', 'nelio-content' ); } return undefined; }, }, kinds: [ { id: 'page', label: _x( 'Page', 'facebook profile', 'nelio-content' ), }, ], }, { id: 'linkedin' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add LinkedIn Profile', 'command', 'nelio-content' ), name: _x( 'LinkedIn', 'text', 'nelio-content' ), settings: _x( 'LinkedIn Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 3000, supports: [ ...DEFAULT_SUPPORTS, 'buffer-connection', 'hootsuite-connection', 'multi-image', 'video', 'link-shortener', ], supportedImageTypes: [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', ], supportedVideoTypes: [ 'video/mp4' ], requires: DEFAULT_REQUIRES, validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/tiff', ].includes( media.mime ) ) { return _x( 'Unsupported image type in LinkedIn', 'text', 'nelio-content' ); } return undefined; }, video: ( media ) => { if ( ! media ) { return undefined; } if ( media.mime !== 'video/mp4' ) { return _x( 'Unsupported video type in LinkedIn', 'text', 'nelio-content' ); } if ( media.duration < 3 ) { return _x( 'LinkedIn video must be at least 3 seconds long', 'text', 'nelio-content' ); } if ( media.duration > 30 * 60 ) { return _x( 'LinkedIn video must be shorter than 30 minutes', 'text', 'nelio-content' ); } if ( kilos( media.filesizeInBytes ) < 75 ) { return _x( 'LinkedIn video must be at least 75KB', 'text', 'nelio-content' ); } if ( megas( media.filesizeInBytes ) > 500 ) { return _x( 'LinkedIn video should be smaller than 500MB', 'text', 'nelio-content' ); } return undefined; }, }, kinds: [ { id: 'single', label: _x( 'Personal', 'linkedin profile', 'nelio-content' ), }, { id: 'company', label: _x( 'Company', 'linkedin profile', 'nelio-content' ), }, ], }, { id: 'gmb' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Google My Business Location', 'command', 'nelio-content' ), name: _x( 'Google My Business', 'text', 'nelio-content' ), settings: _x( 'Google My Business Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 1500, supports: [ ...DEFAULT_SUPPORTS, 'buffer-connection', 'link-shortener', ], supportedImageTypes: [ 'image/jpeg', 'image/jpg', 'image/png' ], requires: DEFAULT_REQUIRES, validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'image/jpeg', 'image/jpg', 'image/png' ].includes( media.mime ) ) { return _x( 'Unsupported image type in Google My Business', 'text', 'nelio-content' ); } if ( megas( media.filesizeInBytes ) > 5 ) { return _x( 'Google My Business image should be smaller than 5MB', 'text', 'nelio-content' ); } if ( kilos( media.filesizeInBytes ) < 10 ) { return _x( 'Google My Business image should be at least 10KB', 'text', 'nelio-content' ); } if ( media.height < 250 ) { return _x( 'Google My Business image height must be at least 250px', 'text', 'nelio-content' ); } if ( media.width < 250 ) { return _x( 'Google My Business image width must be at least 250px', 'text', 'nelio-content' ); } return undefined; }, }, kinds: [ { id: 'location', label: _x( 'Location', 'Google My Business profile', 'nelio-content' ), }, ], }, { id: 'pinterest' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Pinterest Profile', 'command', 'nelio-content' ), name: _x( 'Pinterest', 'text', 'nelio-content' ), settings: _x( 'Pinterest Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 500, supports: [ ...without( DEFAULT_SUPPORTS, 'network-template' ), 'multi-image', 'multi-target', 'buffer-connection', 'link-shortener', ], supportedImageTypes: [ 'image' ], requires: [ [ 'multi-target', 'text', 'image' ] ], validators: { images: ( media ) => { if ( uniq( media.map( ( m ) => `${ m.width }-${ m.height }` ) ) .length !== 1 ) { return _x( 'Images in a multi-image Pinterest pin must have the same width/height ratios.', 'text', 'nelio-content' ); } return undefined; }, }, }, { id: 'reddit' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Subreddit', 'command', 'nelio-content' ), name: _x( 'Reddit', 'text', 'nelio-content' ), settings: _x( 'Reddit Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 500, supports: without( DEFAULT_SUPPORTS, 'network-image' ), supportedImageTypes: [ 'image' ], requires: DEFAULT_REQUIRES, kinds: [ { id: 'subreddit', label: _x( 'Subreddit', 'Reddit profile', 'nelio-content' ), }, ], }, { id: 'instagram' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Instagram Profile', 'command', 'nelio-content' ), name: _x( 'Instagram', 'text', 'nelio-content' ), settings: _x( 'Instagram Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 2000, supports: [ ...DEFAULT_SUPPORTS, 'buffer-connection', 'hootsuite-connection', 'multi-image', 'multi-media', 'video', 'link-shortener', ], supportedImageTypes: [ 'image/jpeg', 'image/jpg' ], supportedVideoTypes: [ 'video/mp4', 'video/quicktime' ], requires: [ [ 'text', 'image' ] ], validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'image/jpeg', 'image/jpg' ].includes( media.mime ) ) { return _x( 'Unsupported image type in Instagram', 'text', 'nelio-content' ); } if ( megas( media.filesizeInBytes ) > 8 ) { return _x( 'Instagram image should be smaller than 8MB', 'text', 'nelio-content' ); } return undefined; }, video: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'video/mp4', 'video/quicktime' ].includes( media.mime ) ) { return _x( 'Unsupported video type in Instagram', 'text', 'nelio-content' ); } return undefined; }, }, }, { id: 'telegram' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Telegram Connection', 'command', 'nelio-content' ), name: _x( 'Telegram', 'text', 'nelio-content' ), settings: _x( 'Telegram Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 4096, supports: [ ...DEFAULT_SUPPORTS, 'multi-image', 'video', 'multi-media', 'link-shortener', ], supportedImageTypes: [ 'image' ], supportedVideoTypes: [ 'video' ], requires: DEFAULT_REQUIRES, validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( megas( media.filesizeInBytes ) > 5 ) { return _x( 'Telegram image should be smaller than 5MB', 'text', 'nelio-content' ); } return undefined; }, video: ( media ) => { if ( ! media ) { return undefined; } if ( megas( media.filesizeInBytes ) > 20 ) { return _x( 'Telegram video should be smaller than 20MB', 'text', 'nelio-content' ); } return undefined; }, }, }, { id: 'medium' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Medium Connection', 'command', 'nelio-content' ), name: _x( 'Medium', 'text', 'nelio-content' ), settings: _x( 'Medium Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 4096, tagsLimit: 5, supports: [ 'preview', 'related-post', 'medium-tags' ], requires: [ [ 'preview', 'related-post' ] ], kinds: [ { id: 'single', label: _x( 'Personal', 'medium profile', 'nelio-content' ), }, { id: 'publication', label: _x( 'Publication', 'medium profile', 'nelio-content' ), }, ], }, { id: 'blogger' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Blogger Connection', 'command', 'nelio-content' ), name: _x( 'Blogger', 'text', 'nelio-content' ), settings: _x( 'Blogger Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 10000, supports: [ 'preview', 'related-post' ], requires: [ [ 'preview', 'related-post' ] ], kinds: [ { id: 'blog', label: _x( 'Blog', 'Blogger blog', 'nelio-content' ), }, ], }, { id: 'tiktok' as const, active: true, automationFreqs: { reshare: 0, publication: 0, }, labels: { add: _x( 'Add TikTok Profile', 'command', 'nelio-content' ), name: _x( 'TikTok', 'text', 'nelio-content' ), settings: _x( 'TikTok Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 2000, supports: [ 'recurrence', 'reusability', 'text', 'preview', 'video', 'image', 'multi-image', 'link-shortener', ], supportedImageTypes: [ 'image/jpeg', 'image/jpg', 'image/webp' ], supportedVideoTypes: [ 'video/mp4', 'video/webm', 'video/quicktime' ], requires: [ [ 'video' ], [ 'image' ], [ 'multi-image' ] ], validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'image/jpeg', 'image/jpg', 'image/webp' ].includes( media.mime ) ) { return _x( 'Unsupported image type in TikTok', 'text', 'nelio-content' ); } if ( megas( media.filesizeInBytes ) > 20 ) { return _x( 'TikTok image should be smaller than 20MB', 'text', 'nelio-content' ); } if ( media.height > 1080 ) { return _x( 'TikTok image height must be less than or equal to 1080px', 'text', 'nelio-content' ); } return undefined; }, video: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'video/mp4', 'video/webm', 'video/quicktime' ].includes( media.mime ) ) { return _x( 'Unsupported video type in TikTok', 'text', 'nelio-content' ); } if ( media.duration > 10 * 60 ) { return _x( 'TikTok video must be shorter than 10 minutes', 'text', 'nelio-content' ); } return undefined; }, general: ( attrs ) => { if ( attrs.network === 'tiktok' && attrs.tiktokSettings?.discloseContent && ! attrs.tiktokSettings.isBrandContent && ! attrs.tiktokSettings.isBrandOrganic ) { return _x( 'You need to indicate if your content promotes yourself, a third party, or both', 'user', 'nelio-content' ); } return undefined; }, }, }, { id: 'tumblr' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Tumblr Profile', 'command', 'nelio-content' ), name: _x( 'Tumblr', 'text', 'nelio-content' ), settings: _x( 'Tumblr Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 500, supports: DEFAULT_SUPPORTS, supportedImageTypes: [ 'image' ], requires: DEFAULT_REQUIRES, }, { id: 'mastodon' as const, active: true, automationFreqs: { reshare: 8, publication: 2, }, labels: { add: _x( 'Add Mastodon Profile', 'command', 'nelio-content' ), name: _x( 'Mastodon', 'text', 'nelio-content' ), settings: _x( 'Mastodon Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 500, supports: [ ...DEFAULT_SUPPORTS, 'multi-image', 'video', 'buffer-connection', 'link-shortener', ], supportedImageTypes: [ 'image' ], supportedVideoTypes: [ 'video/mp4', 'video/x-m4v', 'video/quicktime', 'video/webm', ], requires: DEFAULT_REQUIRES, validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( megas( media.filesizeInBytes ) > 16 ) { return _x( 'Mastodon image should be smaller than 16MB', 'text', 'nelio-content' ); } return undefined; }, video: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'video/mp4', 'video/x-m4v', 'video/quicktime', 'video/webm', ].includes( media.mime ) ) { return _x( 'Unsupported video type in Mastodon', 'text', 'nelio-content' ); } if ( megas( media.filesizeInBytes ) > 99 ) { return _x( 'Mastodon video should be smaller than 99MB', 'text', 'nelio-content' ); } return undefined; }, }, }, { id: 'bluesky' as const, active: true, automationFreqs: { reshare: 8, publication: 2, }, labels: { add: _x( 'Add Bluesky Profile', 'command', 'nelio-content' ), name: _x( 'Bluesky', 'text', 'nelio-content' ), settings: _x( 'Bluesky Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 300, supports: [ ...DEFAULT_SUPPORTS, 'multi-image', 'link-shortener' ], supportedImageTypes: [ 'image' ], requires: DEFAULT_REQUIRES, validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( megas( media.filesizeInBytes ) > 1 ) { return _x( 'Bluesky image should be smaller than 1MB', 'text', 'nelio-content' ); } return undefined; }, }, }, { id: 'threads' as const, active: true, automationFreqs: { reshare: 8, publication: 2, }, labels: { add: _x( 'Add Threads Profile', 'command', 'nelio-content' ), name: _x( 'Threads', 'text', 'nelio-content' ), settings: _x( 'Threads Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 300, supports: [ ...DEFAULT_SUPPORTS, 'hootsuite-connection', 'video', 'multi-image', 'multi-media', 'link-shortener', ], supportedImageTypes: [ 'image/jpeg', 'image/jpg', 'image/png' ], supportedVideoTypes: [ 'video/mp4', 'video/quicktime' ], requires: DEFAULT_REQUIRES, validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( megas( media.filesizeInBytes ) > 8 ) { return _x( 'Threads image should be smaller than 8MB', 'text', 'nelio-content' ); } return undefined; }, video: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'video/mp4', 'video/quicktime' ].includes( media.mime ) ) { return _x( 'Unsupported video type in Threads', 'text', 'nelio-content' ); } if ( media.duration > 300 ) { return _x( 'TikTok video must be shorter than 300 seconds', 'text', 'nelio-content' ); } return undefined; }, }, }, { id: 'vk' as const, active: true, automationFreqs: { reshare: 1, publication: 1, }, labels: { add: _x( 'Add VK Profile', 'command', 'nelio-content' ), name: _x( 'VK', 'text', 'nelio-content' ), settings: _x( 'VK Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 10000, supports: [ ...DEFAULT_SUPPORTS, 'video', 'multi-image', 'multi-media', 'link-shortener', ], supportedImageTypes: [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', ], supportedVideoTypes: [ 'video/x-msvideo', 'video/mp4', 'video/3gpp', 'video/mpeg', 'video/quicktime', 'video/x-flv', 'video/x-ms-wmv', ], requires: DEFAULT_REQUIRES, validators: { image: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', ].includes( media.mime ) ) { return _x( 'Unsupported image type in VK', 'text', 'nelio-content' ); } if ( megas( media.filesizeInBytes ) > 50 ) { return _x( 'VK image should be smaller than 50MB', 'text', 'nelio-content' ); } if ( media.height + media.width > 14000 ) { return _x( 'Sum of the height and width is no more than 14,000 pixels for VK image', 'text', 'nelio-content' ); } return undefined; }, video: ( media ) => { if ( ! media ) { return undefined; } if ( ! [ 'video/x-msvideo', // AVI 'video/mp4', // MP4 'video/3gpp', // 3GP 'video/mpeg', // MPEG 'video/quicktime', // MOV 'video/x-flv', // FLV 'video/x-ms-wmv', // WMV ].includes( media.mime ) ) { return _x( 'Unsupported video type in VK', 'text', 'nelio-content' ); } return undefined; }, }, kinds: [ { id: 'single', label: _x( 'Personal', 'vk profile', 'nelio-content' ), }, { id: 'group', label: _x( 'Community', 'vk profile', 'nelio-content' ), }, ], }, { id: 'plurk' as const, active: true, automationFreqs: { reshare: 8, publication: 2, }, labels: { add: _x( 'Add Plurk Profile', 'command', 'nelio-content' ), name: _x( 'Plurk', 'text', 'nelio-content' ), settings: _x( 'Plurk Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 280, supports: [ 'automations', 'network-template', 'profile-template', 'recurrence', 'related-post', 'reusability', 'text', 'preview', 'link-shortener', ], requires: DEFAULT_REQUIRES, }, { id: 'discord' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Discord Channel', 'command', 'nelio-content' ), name: _x( 'Discord', 'text', 'nelio-content' ), settings: _x( 'Discord Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 2000, supports: [ ...DEFAULT_SUPPORTS, 'multi-image', 'link-shortener' ], supportedImageTypes: [ 'image' ], requires: DEFAULT_REQUIRES, }, { id: 'ok' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add OK.ru Profile', 'command', 'nelio-content' ), name: _x( 'OK.ru', 'text', 'nelio-content' ), settings: _x( 'OK.ru Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 3000, supports: [ ...DEFAULT_SUPPORTS, 'multi-image', 'link-shortener', /* 'video'*/ ], supportedImageTypes: [ 'image' ], //supportedVideoTypes: [ 'video' ], requires: DEFAULT_REQUIRES, // validators: { // video: ( media ) => { // if ( ! media ) { // return undefined; // } //end if // if ( kilos( media.filesizeInBytes ) < 16 ) { // return _x( // 'OK.ru video must be at least 16KB', // 'text', // 'nelio-content' // ); // } //end if // if ( megas( media.filesizeInBytes ) > 1024 ) { // return _x( // 'OK.ru video should be smaller than 1GB', // 'text', // 'nelio-content' // ); // } //end if // return undefined; // }, // }, kinds: [ { id: 'single', label: _x( 'Personal', 'ok.ru profile', 'nelio-content' ), }, { id: 'group', label: _x( 'Group', 'ok.ru profile', 'nelio-content' ), }, ], }, { id: 'slack' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Slack Channel', 'command', 'nelio-content' ), name: _x( 'Slack', 'text', 'nelio-content' ), settings: _x( 'Slack Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 2000, supports: [ ...DEFAULT_SUPPORTS, 'multi-image', 'link-shortener' ], supportedImageTypes: [ 'image' ], requires: DEFAULT_REQUIRES, }, { id: 'band' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Band', 'command', 'nelio-content' ), name: _x( 'Band', 'text', 'nelio-content' ), settings: _x( 'Band Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 2000, supports: [ 'automations', 'network-template', 'preview', 'profile-template', 'recurrence', 'related-post', 'reusability', 'text', 'link-shortener', ], requires: DEFAULT_REQUIRES, }, { id: 'webhook' as const, active: true, automationFreqs: BASIC_AUTOMATION_FREQS, labels: { add: _x( 'Add Webhook', 'command', 'nelio-content' ), name: _x( 'Webhook', 'text', 'nelio-content' ), settings: _x( 'Webhook Settings', 'text', 'nelio-content' ), }, icon: ( ), limit: 10000, supports: [ 'automations', 'related-post', 'reusability' ], requires: [], validators: { general: ( attrs ) => { if ( attrs.network === 'webhook' && ! isUrl( attrs.webhookSettings?.url ) ) { return _x( 'Provide a fully qualified URL to send the webhook data', 'user', 'nelio-content' ); } if ( attrs.network === 'webhook' && attrs.webhookSettings?.bodyType === 'json' && ! isValidJSON( attrs.webhookSettings?.bodyContent ) ) { return _x( 'Provide a valid JSON body for the webhook', 'user', 'nelio-content' ); } return undefined; }, }, }, ]; const ACTIVE_NETWORKS = filter( NETWORKS, { active: true } ); const MAX_CHAR_LIMIT: number = Math.max( ...map( ACTIVE_NETWORKS, 'limit' ) ); const MIN_CHAR_LIMIT: number = Math.min( ...map( ACTIVE_NETWORKS, 'limit' ) ); type TargetLabel = { readonly explanation: string; readonly loading: string; readonly noTargetsExplanation: string; readonly selectTarget: string; readonly selectTargetError: string; readonly selectTargets: string; readonly targetLabel: string; readonly title: string; }; const TARGET_LABELS: Record< string, TargetLabel > = { default: { explanation: _x( 'Please select where your message will be shared on:', 'user', 'nelio-content' ), loading: _x( 'Loading…', 'text', 'nelio-content' ), noTargetsExplanation: _x( 'Your profile does not have any locations yet. Please configure one first.', 'user', 'nelio-content' ), selectTarget: _x( 'Select a location…', 'user', 'nelio-content' ), selectTargets: _x( 'Select locations…', 'user', 'nelio-content' ), selectTargetError: _x( 'Please select a location', 'user', 'nelio-content' ), targetLabel: _x( 'Location', 'text', 'nelio-content' ), title: _x( 'Select Location', 'text', 'nelio-content' ), }, pinterest: { explanation: _x( 'Please select the boards your message will be shared on:', 'user (pinterest boards)', 'nelio-content' ), loading: _x( 'Loading boards…', 'text (pinterest boards)', 'nelio-content' ), noTargetsExplanation: _x( 'Your Pinterest profile does not have any boards yet. Go to Pinterest and create one first.', 'user (no pinterest boards)', 'nelio-content' ), selectTarget: _x( 'Select a board…', 'user', 'nelio-content' ), selectTargets: _x( 'Select boards…', 'user', 'nelio-content' ), selectTargetError: _x( 'Please select a board', 'user', 'nelio-content' ), targetLabel: _x( 'Board', 'text (pinterest target name)', 'nelio-content' ), title: _x( 'Select Boards', 'text (pinterest boards)', 'nelio-content' ), }, reddit: { explanation: _x( 'Please select where your message will be shared on:', 'user', 'nelio-content' ), loading: _x( 'Loading…', 'text', 'nelio-content' ), noTargetsExplanation: _x( 'Your profile does not have any subreddits yet. Please configure one first.', 'user', 'nelio-content' ), selectTarget: _x( 'Select a subreddit…', 'user', 'nelio-content' ), selectTargets: _x( 'Select subreddits…', 'user', 'nelio-content' ), selectTargetError: _x( 'Please select a subreddit', 'user', 'nelio-content' ), targetLabel: _x( 'Subreddit', 'text', 'nelio-content' ), title: _x( 'Select Subreddit', 'text', 'nelio-content' ), }, }; const _supportedNetworks = map( ACTIVE_NETWORKS, 'id' ); export const getSupportedNetworks = (): Array< SocialNetworkName > => _supportedNetworks; const _networksById = keyBy( ACTIVE_NETWORKS, 'id' ); export const getCharLimitInNetwork = ( networkName: SocialNetworkName ): number => { const network = _networksById[ networkName ] || { limit: MAX_CHAR_LIMIT }; return network.limit; }; export const getMinCharLimit = (): number => MIN_CHAR_LIMIT; export const getMaxCharLimit = (): number => MAX_CHAR_LIMIT; export const getTagsLimitInNetwork = ( networkName: SocialNetworkName ): number => { const network = _networksById[ networkName ] || { tagsLimit: 0 }; return network.tagsLimit ?? 0; }; const LINK_25CH = 'aaaabaaaabaaaabaaaabaaaab'; export function getMessageLength( text: string ): number { text = ` ${ text } `; text = text.replaceAll( /(\s)https?:\/\/[^\s]+/g, `$1${ LINK_25CH }` ); text = text.trim(); return twitter.parseTweet( text ).weightedLength; } export const getDefaultPublicationValue = ( network: SocialNetworkName ): number => _networksById[ network ]?.automationFreqs?.publication || 0; export const getDefaultReshareValue = ( network: SocialNetworkName ): number => _networksById[ network ]?.automationFreqs?.reshare || 0; export const doesNetworkSupport = ( support: SocialNetworkSupport, networkName: Maybe< SocialNetworkName > ): boolean => !! networkName && !! _networksById[ networkName ]?.supports.includes( support ); export const doesNetworkRequire = ( support: SocialNetworkSupport, networkName: Maybe< SocialNetworkName > ): boolean => !! networkName && !! _networksById[ networkName ]?.requires.some( ( r ) => r.includes( support ) ); export const getNetworkLabel = ( label: keyof SocialNetwork[ 'labels' ], networkName: SocialNetworkName ): string => { const network = _networksById[ networkName ]; return network?.labels[ label ] || ''; }; export const getSupportedImageTypes = ( networkName: SocialNetworkName ): ReadonlyArray< string > => { const network = _networksById[ networkName ]; return network?.supportedImageTypes ?? []; }; export const getSupportedVideoTypes = ( networkName: SocialNetworkName ): ReadonlyArray< string > => { const network = _networksById[ networkName ]; return network?.supportedVideoTypes ?? []; }; export const getTargetLabel = ( label: keyof TargetLabel, networkName: SocialNetworkName ): string => { const labels = TARGET_LABELS[ networkName ] || TARGET_LABELS.default; return labels?.[ label ] || ''; }; export const getNetworkKinds = ( networkName: SocialNetworkName ): ReadonlyArray< SocialKind > => { const network = _networksById[ networkName ]; return network?.kinds || []; }; export const getNetworkIcon = ( networkName: SocialNetworkName ): JSX.Element => _networksById[ networkName ]?.icon || ( ); export function getRequirementsError( attrs: RequirementsErrorAttributes ): Maybe< string > { const network = _networksById[ attrs.network ]; if ( ! network ) { return _x( 'Unknown error', 'text', 'nelio-content' ); } const getReqErr = ( req: SocialNetworkSupport ): string => { switch ( req ) { case 'image': case 'multi-image': if ( [ 'image', 'auto-image' ].includes( attrs.type ) || ( network.supports.includes( 'video' ) && 'video' === attrs.type ) || ( network.supports.includes( 'multi-media' ) && 'multi-media' === attrs.type ) ) { return ''; } return network.supports.includes( 'video' ) || network.supports.includes( 'multi-media' ) ? GENERIC_ERRORS.missingMedia : GENERIC_ERRORS.missingImage; case 'multi-media': if ( [ 'image', 'auto-image', 'video', 'multi-media' ].includes( attrs.type ) ) { return ''; } return GENERIC_ERRORS.missingMedia; case 'multi-target': return ! attrs.targetName ? getTargetLabel( 'selectTargetError', attrs.network ) : ''; case 'text': if ( attrs.textComputed === '' ) { return GENERIC_ERRORS.missingText; } return getMessageLength( attrs.textComputed ) > network.limit ? GENERIC_ERRORS.textTooLong : ''; case 'video': if ( 'video' === attrs.type || ( network.supports.includes( 'image' ) && 'image' === attrs.type ) || ( network.supports.includes( 'multi-media' ) && 'multi-media' === attrs.type ) ) { return ''; } return network.supports.includes( 'image' ) ? GENERIC_ERRORS.missingMedia : GENERIC_ERRORS.missingVideo; case 'related-post': if ( attrs.postId ) { return ''; } return GENERIC_ERRORS.missingRelatedPost; case 'automations': case 'buffer-connection': case 'hootsuite-connection': case 'link-shortener': case 'medium-tags': case 'network-image': case 'network-template': case 'preview': case 'profile-template': case 'recurrence': case 'reusability': return ''; } }; const getMediaErr = (): string => { if ( network.supports.includes( 'image' ) && 'image' === attrs.type ) { return isArray( attrs.image ) ? attrs.image .map( ( img: MediaItem ) => network.validators?.image?.( img ) ?? '' ) .filter( ( m ) => !! m )[ 0 ] ?? network.validators?.images?.( attrs.image ) ?? '' : network.validators?.image?.( attrs.image as Maybe< MediaItem > ) ?? ''; } if ( network.supports.includes( 'video' ) && 'video' === attrs.type ) { return isArray( attrs.video ) ? attrs.video .map( ( v: MediaItem ) => network.validators?.video?.( v ) ?? '' ) .filter( ( v ) => !! v )[ 0 ] ?? '' : network.validators?.video?.( attrs.video as Maybe< MediaItem > ) ?? ''; } return ''; }; const getNetworkErr = (): string => network.validators?.general?.( attrs ) ?? ''; const errors = network.requires.map( ( reqs ) => reqs.reduce( ( e, r ) => e || getReqErr( r ), '' ) ); /* eslint-disable @wordpress/valid-sprintf */ const err = sprintf( find( errors, ( e ) => e !== '' ) ?? '', network.labels.name ); /* eslint-enable @wordpress/valid-sprintf */ return err || getMediaErr() || getNetworkErr() || undefined; } // ======= // HELPERS // ======= const kilos = ( bytes: number ) => Math.round( bytes / 1024 ); const megas = ( bytes: number ) => Math.round( kilos( bytes ) / 1024 ); const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/; const isUrl = ( url?: string | Url ): url is Url => { return !! url && URL_REGEX.test( url ); }; const isValidJSON = ( value?: string ): boolean => { if ( value === undefined ) { return false; } try { JSON.parse( value ); return true; } catch { return false; } };