/** * WordPress dependencies */ import apiFetch from '@safe-wordpress/api-fetch'; import { dispatch, resolveSelect, select } from '@safe-wordpress/data'; import { addQueryArgs } from '@safe-wordpress/url'; /** * External dependencies */ import z from 'zod'; import { mapValues } from 'lodash'; import { v4 as uuid } from 'uuid'; import { getCharLimitInNetwork } from '@nelio-content/networks'; import { computeSocialMessageText, getBaseDatetime, showErrorNotice, } from '@nelio-content/utils'; import type { BufferSettings, Maybe, MediaId, PlurkQualifier, Post, PostId, SavingSocialMessage, SocialMessage, SocialNetworkName, SocialProfile, TikTokSettings, Url, Uuid, WebhookSettings, } from '@nelio-content/types'; /** * Internal dependencies */ import { store as NC_DATA } from '../../../store'; import { computeWebhookSettings } from '../../../../utils/derived-attributes'; type NewSocialMessage = SavingSocialMessage & { slots: [ { readonly id: Uuid; readonly profileId: Uuid; readonly targetName: string | 'default'; readonly network: SocialNetworkName; readonly permalink: Maybe< Url >; readonly title?: string; readonly content?: string; readonly tags?: ReadonlyArray< string >; readonly targetDisplayName?: string; readonly tiktokSettings?: TikTokSettings; readonly webhookSettings?: WebhookSettings; readonly webhookSettingsComputed?: WebhookSettings; readonly bufferSettings?: BufferSettings; readonly plurkQualifier?: PlurkQualifier; readonly type: SocialMessage[ 'type' ]; }, ]; }; /** * Creates a new social message using the provided attributes. * * @param message Object Message attributes. * * @return SocialMessage returns the new social message. * * @since 3.0.0 */ export async function createSocialMessage( message: unknown ): Promise< SocialMessage > { const attrs = messageSchema.parse( message ); const post = attrs.post ? await resolveSelect( NC_DATA ).getPost( attrs.post ) : undefined; const postArgs = ( post?.permalinkQueryArgs ?? [] ).reduce( ( r, [ k, v ] ) => ( { ...r, [ k ]: v } ), {} ); const postUrls = post ? { default: addQueryArgs( post.permalink, postArgs ) as Url, ...mapValues( post.permalinks, ( url ) => addQueryArgs( url ?? '', postArgs ) as Url ), } : undefined; const data: NewSocialMessage = { ...getDatetimeProps( post, attrs.date, attrs.time ), image: 'imageUrl' in attrs ? attrs.imageUrl : undefined, imageId: 'imageId' in attrs ? attrs.imageId : undefined, postAuthor: post?.author, postId: post?.id, postType: post?.type, source: 'manual', text: attrs.text, textComputed: computeSocialMessageText( getCharLimitInNetwork( attrs.profile.network ), post, attrs.text ), slots: [ { id: uuid(), profileId: attrs.profile.id, targetName: attrs.targetName ?? 'default', network: attrs.profile.network, permalink: postUrls?.[ attrs.profile.network ] ?? postUrls?.default, type: !! attrs.imageUrl ? 'image' : attrs.type ?? 'text', ...( attrs.profile.network === 'tiktok' && attrs.tiktokSettings && { tiktokSettings: attrs.tiktokSettings, } ), ...( attrs.profile.network === 'plurk' && attrs.plurkQualifier && { plurkQualifier: attrs.plurkQualifier, } ), ...( attrs.profile.network === 'webhook' && attrs.webhookSettings && { webhookSettings: attrs.webhookSettings, webhookSettingsComputed: computeWebhookSettings( attrs.webhookSettings, post ), } ), ...( attrs.profile.isBuffer && attrs.bufferSettings && { bufferSettings: attrs.bufferSettings, } ), }, ], }; const siteId = select( NC_DATA ).getSiteId(); const apiRoot = select( NC_DATA ).getApiRoot(); const token = select( NC_DATA ).getAuthenticationToken(); const result = await apiFetch< [ SocialMessage ] >( { url: `${ apiRoot }/site/${ siteId }/social`, method: 'POST', credentials: 'omit', mode: 'cors', headers: { Authorization: `Bearer ${ token }`, }, data, } ); await dispatch( NC_DATA ).receiveSocialMessages( result ); return result[ 0 ]; } export async function resetSocialMessages(): Promise< void > { const status = select( NC_DATA ).getResetStatus(); if ( 'ready' !== status ) { return; } try { await dispatch( NC_DATA ).setResetStatus( 'resetting' ); await apiFetch( { path: '/nelio-content/v1/social/reset', method: 'PUT', } ); await dispatch( NC_DATA ).setResetStatus( 'done' ); } catch ( e ) { await showErrorNotice( e ); await dispatch( NC_DATA ).setResetStatus( 'error' ); } } // ======= // HELPERS // ======= const plurkQualifierSchema = z.enum( [ 'plays', 'buys', 'sells', 'loves', 'likes', 'shares', 'hates', 'wants', 'wishes', 'needs', 'has', 'will', 'hopes', 'asks', 'wonders', 'feels', 'thinks', 'draws', 'is', 'says', 'eats', 'writes', 'whispers', ] ); const tiktokSettingsSchema = z.object( { privacyLevel: z.enum( [ 'PUBLIC_TO_EVERYONE', 'MUTUAL_FOLLOW_FRIENDS', 'FOLLOWER_OF_CREATOR', 'SELF_ONLY', ] ), disableDuet: z.boolean(), disableStitch: z.boolean(), disableComment: z.boolean(), discloseContent: z.boolean(), isBrandContent: z.boolean(), isBrandOrganic: z.boolean(), } ); const webhookHeaderSchema = z.object( { key: z.string(), value: z.string(), } ); const webhookFormFieldSchema = z.object( { key: z.string(), value: z.string(), } ); const webhookSettingsSchema = z .object( { url: z.string(), method: z.enum( [ 'POST', 'GET', 'HEAD', 'PATCH', 'PUT', 'DELETE' ] ), headers: z.array( webhookHeaderSchema ), } ) .and( z.discriminatedUnion( 'bodyType', [ z.object( { bodyType: z.literal( 'none' ), } ), z.object( { bodyType: z.literal( 'json' ), bodyContent: z.string(), } ), z.object( { bodyType: z.literal( 'form' ), bodyContent: z.array( webhookFormFieldSchema ), } ), ] ) ); const bufferSettingsSchema = z.object( { isManagedInBuffer: z.boolean() } ); const messageSchema = z .object( { profile: z .string() .uuid() .transform( ( v ) => select( NC_DATA ).getSocialProfile( v as Uuid ) ) .refine( ( p ): p is SocialProfile => !! p ), targetName: z.string().optional(), text: z.string().trim().min( 1 ), date: z.union( [ z.number().nonnegative(), z.string().regex( /\d{4}-\d{2}-\d{2}/ ), ] ), time: z.union( [ z.number().nonnegative(), z.string().regex( /\d{2}:\d{2}/ ), ] ), post: z .number() .optional() .transform( ( p ) => p as Maybe< PostId > ), imageUrl: z .string() .url() .optional() .transform( ( v ) => v as Maybe< Url > ), imageId: z .number() .optional() .transform( ( v ) => v as Maybe< MediaId > ), type: z .enum( [ 'text', 'image', 'auto-image', 'video', 'multi-media', 'webhook', ] ) .optional(), plurkQualifier: plurkQualifierSchema.optional(), tiktokSettings: tiktokSettingsSchema.optional(), webhookSettings: webhookSettingsSchema.optional(), bufferSettings: bufferSettingsSchema.optional(), } ) // IF time is number THEN date is 0 .refine( ( d ) => 'number' !== typeof d.time || 0 === d.date, 'numeric time can only be set if date is 0' ) // IF imageId THEN also imageUrl .refine( ( d ) => ! d.imageId || !! d.imageUrl, 'imageUrl is missing' ); const getDatetimeProps = ( post: Maybe< Post >, date: string | number, time: string | number ) => { const dateType = 'number' === typeof date ? 'positive-days' : 'exact'; const dateValue = `${ date }`; const timeType = 'number' === typeof date ? 'positive-hours' : 'exact'; const timeValue = `${ time }`; return { baseDatetime: getBaseDatetime( post, dateType ), schedule: '', dateType, dateValue, timeType, timeValue, timezone: select( NC_DATA ).getSiteTimezone(), } as const; };