feat: delayed comments
This commit is contained in:
parent
4836b2e7c1
commit
2544e870aa
|
|
@ -148,7 +148,7 @@ export const AddEditModalInnerInner: FC<AddEditModalProps> = (props) => {
|
|||
0,
|
||||
existingData.integration,
|
||||
existingData.posts.map((post) => ({
|
||||
delay: 0,
|
||||
delay: post.delay,
|
||||
content:
|
||||
post.content.indexOf('<p>') > -1
|
||||
? post.content
|
||||
|
|
|
|||
|
|
@ -1,17 +1,42 @@
|
|||
'use client';
|
||||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { DelayIcon } from '@gitroom/frontend/components/ui/icons';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { DelayIcon, DropdownArrowIcon } from '@gitroom/frontend/components/ui/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
|
||||
const delayOptions = [
|
||||
{ value: 1, label: '1m' },
|
||||
{ value: 2, label: '2m' },
|
||||
{ value: 5, label: '5m' },
|
||||
{ value: 10, label: '10m' },
|
||||
{ value: 15, label: '15m' },
|
||||
{ value: 30, label: '30m' },
|
||||
{ value: 60, label: '1h' },
|
||||
{ value: 120, label: '2h' },
|
||||
];
|
||||
|
||||
export const DelayComponent: FC<{
|
||||
currentIndex: number;
|
||||
currentDelay: number;
|
||||
}> = ({ currentIndex, currentDelay }) => {
|
||||
const t = useT();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [customValue, setCustomValue] = useState('');
|
||||
|
||||
const isCustomDelay = currentDelay > 0 && !delayOptions.some((opt) => opt.value === currentDelay);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && isCustomDelay) {
|
||||
setCustomValue(String(currentDelay));
|
||||
} else if (isOpen && !isCustomDelay) {
|
||||
setCustomValue('');
|
||||
}
|
||||
}, [isOpen, isCustomDelay, currentDelay]);
|
||||
|
||||
const { current, setInternalDelay, setGlobalDelay } = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
current: state.current,
|
||||
|
|
@ -20,6 +45,13 @@ export const DelayComponent: FC<{
|
|||
}))
|
||||
);
|
||||
|
||||
const ref = useClickOutside(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(false);
|
||||
});
|
||||
|
||||
const setDelay = useCallback(
|
||||
(index: number) => (minutes: number) => {
|
||||
if (current !== 'global') {
|
||||
|
|
@ -31,20 +63,92 @@ export const DelayComponent: FC<{
|
|||
[currentIndex, current]
|
||||
);
|
||||
|
||||
const handleSelectDelay = useCallback(
|
||||
(minutes: number) => {
|
||||
setDelay(currentIndex)(minutes);
|
||||
setIsOpen(false);
|
||||
},
|
||||
[currentIndex, setDelay]
|
||||
);
|
||||
|
||||
const getCurrentDelayLabel = () => {
|
||||
if (!currentDelay) return null;
|
||||
const option = delayOptions.find((opt) => opt.value === currentDelay);
|
||||
return option?.label || `${currentDelay} min`;
|
||||
};
|
||||
|
||||
return (
|
||||
<DelayIcon
|
||||
// move it into the modal
|
||||
onClick={() => setDelay(currentIndex)(100)}
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content={
|
||||
!currentDelay
|
||||
? t('delay_comment', 'Delay comment')
|
||||
: `Comment delayed by ${currentDelay} minutes`
|
||||
}
|
||||
className={clsx(
|
||||
'cursor-pointer',
|
||||
currentDelay > 0 && 'bg-[#D82D7E] text-white rounded-full'
|
||||
<div ref={ref} className="relative">
|
||||
<div
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content={
|
||||
!currentDelay
|
||||
? t('delay_comment', 'Delay comment')
|
||||
: `${t('delay_comment_by', 'Comment delayed by')} ${getCurrentDelayLabel()}`
|
||||
}
|
||||
className={clsx(
|
||||
'cursor-pointer flex items-center gap-[4px]',
|
||||
currentDelay > 0 && 'bg-[#D82D7E] text-white rounded-full'
|
||||
)}
|
||||
>
|
||||
<DelayIcon />
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="z-[300] absolute end-0 top-[100%] w-[200px] bg-newBgColorInner p-[8px] menu-shadow translate-y-[10px] flex flex-col rounded-[8px]">
|
||||
<div className="grid grid-cols-4 gap-[4px]">
|
||||
{delayOptions.map((option) => (
|
||||
<div
|
||||
onClick={() => handleSelectDelay(option.value)}
|
||||
key={option.value}
|
||||
className={clsx(
|
||||
'h-[32px] flex items-center justify-center rounded-[4px] cursor-pointer hover:bg-newBgColor text-[13px]',
|
||||
currentDelay === option.value && 'bg-[#612BD3] text-white hover:bg-[#612BD3]'
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t border-newTextColor/10 mt-[8px] pt-[8px]">
|
||||
<div className="flex gap-[4px]">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={customValue}
|
||||
onChange={(e) => setCustomValue(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
placeholder="Custom min"
|
||||
className={clsx(
|
||||
'flex-1 w-full h-[32px] px-[8px] rounded-[4px] bg-newBgColor border text-[13px] outline-none focus:border-[#612BD3]',
|
||||
isCustomDelay ? 'border-[#612BD3]' : 'border-newTextColor/10'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const value = parseInt(customValue, 10);
|
||||
if (value > 0) {
|
||||
handleSelectDelay(value);
|
||||
setCustomValue('');
|
||||
}
|
||||
}}
|
||||
className="h-[32px] px-[10px] rounded-[4px] bg-[#612BD3] text-white text-[12px] font-[600] hover:bg-[#612BD3]/80"
|
||||
>
|
||||
Set
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{currentDelay > 0 && (
|
||||
<button
|
||||
onClick={() => handleSelectDelay(0)}
|
||||
className="mt-[8px] h-[32px] w-full rounded-[4px] text-[13px] text-red-400 hover:bg-red-400/10"
|
||||
>
|
||||
Remove delay
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -315,6 +315,7 @@ export const ManageModal: FC<AddEditModalProps> = (props) => {
|
|||
value: post.values.map((value: any) => ({
|
||||
...(value.id ? { id: value.id } : {}),
|
||||
content: value.content,
|
||||
delay: value.delay || 0,
|
||||
image:
|
||||
(value?.media || []).map(
|
||||
({ id, path, alt, thumbnail, thumbnailTimestamp }: any) => ({
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import {
|
|||
organizationId,
|
||||
postId as postIdSearchParam,
|
||||
} from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
import { postWorkflow } from '@gitroom/orchestrator/workflows';
|
||||
|
||||
@Injectable()
|
||||
@Activity()
|
||||
|
|
@ -43,7 +42,7 @@ export class PostActivity {
|
|||
for (const post of list) {
|
||||
await this._temporalService.client
|
||||
.getRawClient()
|
||||
.workflow.signalWithStart('postWorkflow', {
|
||||
.workflow.signalWithStart('postWorkflowV101', {
|
||||
workflowId: `post_${post.id}`,
|
||||
taskQueue: 'main',
|
||||
signal: 'poke',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './post.workflow';
|
||||
export * from './post-workflows/post.workflow';
|
||||
export * from './post-workflows/post.workflow.v1.0.1';
|
||||
export * from './autopost.workflow';
|
||||
export * from './digest.email.workflow';
|
||||
export * from './missing.post.workflow';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,375 @@
|
|||
import { PostActivity } from '@gitroom/orchestrator/activities/post.activity';
|
||||
import {
|
||||
ActivityFailure,
|
||||
ApplicationFailure,
|
||||
startChild,
|
||||
proxyActivities,
|
||||
sleep,
|
||||
defineSignal,
|
||||
setHandler,
|
||||
} from '@temporalio/workflow';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { capitalize, sortBy } from 'lodash';
|
||||
import { PostResponse } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import { postId as postIdSearchParam } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
|
||||
const proxyTaskQueue = (taskQueue: string) => {
|
||||
return proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
taskQueue,
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
getPostsList,
|
||||
inAppNotification,
|
||||
changeState,
|
||||
updatePost,
|
||||
sendWebhooks,
|
||||
isCommentable,
|
||||
} = proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
|
||||
const poke = defineSignal('poke');
|
||||
|
||||
export async function postWorkflowV101({
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow = false,
|
||||
}: {
|
||||
taskQueue: string;
|
||||
postId: string;
|
||||
organizationId: string;
|
||||
postNow?: boolean;
|
||||
}) {
|
||||
// Dynamic task queue, for concurrency
|
||||
const {
|
||||
postSocial,
|
||||
postComment,
|
||||
refreshToken,
|
||||
internalPlugs,
|
||||
globalPlugs,
|
||||
processInternalPlug,
|
||||
processPlug,
|
||||
} = proxyTaskQueue(taskQueue);
|
||||
|
||||
let poked = false;
|
||||
setHandler(poke, () => {
|
||||
poked = true;
|
||||
});
|
||||
|
||||
const startTime = new Date();
|
||||
// get all the posts and comments to post
|
||||
const postsList = await getPostsList(organizationId, postId);
|
||||
const [post] = postsList;
|
||||
|
||||
// in case doesn't exists for some reason, fail it
|
||||
if (!post || (!postNow && post.state !== 'QUEUE')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's a repeatable post, we should ignore this.
|
||||
if (!postNow) {
|
||||
await sleep(
|
||||
dayjs(post.publishDate).isBefore(dayjs())
|
||||
? 0
|
||||
: dayjs(post.publishDate).diff(dayjs(), 'millisecond')
|
||||
);
|
||||
}
|
||||
|
||||
// if refresh is needed from last time, let's inform the user
|
||||
if (post.integration?.refreshNeeded) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because you need to reconnect it. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's disabled, inform the user
|
||||
if (post.integration?.disabled) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because it's disabled. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we need to post comment for this social?
|
||||
const toComment =
|
||||
postsList.length === 1 ? false : await isCommentable(post.integration);
|
||||
|
||||
// list of all the saved results
|
||||
const postsResults: PostResponse[] = [];
|
||||
|
||||
// iterate over the posts
|
||||
for (let i = 0; i < postsList.length; i++) {
|
||||
// this is a small trick to repeat an action in case of token refresh
|
||||
while (true) {
|
||||
try {
|
||||
// first post the main post
|
||||
if (i === 0) {
|
||||
postsResults.push(
|
||||
...(await postSocial(post.integration as Integration, [
|
||||
postsList[i],
|
||||
]))
|
||||
);
|
||||
|
||||
// then post the comments if any
|
||||
} else {
|
||||
if (!toComment) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (postsList[i].delay) {
|
||||
await sleep(60000 * postsList[i].delay);
|
||||
}
|
||||
|
||||
postsResults.push(
|
||||
...(await postComment(
|
||||
postsResults[0].postId,
|
||||
postsResults.length === 1
|
||||
? undefined
|
||||
: postsResults[i - 1].postId,
|
||||
post.integration,
|
||||
[postsList[i]]
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// mark post as successful
|
||||
await updatePost(
|
||||
postsList[i].id,
|
||||
postsResults[i].postId,
|
||||
postsResults[i].releaseURL
|
||||
);
|
||||
|
||||
if (i === 0) {
|
||||
// send notification on a sucessful post
|
||||
await inAppNotification(
|
||||
post.integration.organizationId,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)}`,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)} at ${postsResults[0].releaseURL}`,
|
||||
true,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// break the current while to move to the next post
|
||||
break;
|
||||
} catch (err) {
|
||||
// if token refresh is needed, do it and repeat
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshToken(post.integration);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
return false;
|
||||
}
|
||||
|
||||
post.integration.token = refresh.accessToken;
|
||||
continue;
|
||||
}
|
||||
|
||||
// for other errors, change state and inform the user if needed
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
|
||||
// specific case for bad body errors
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`Error posting${i === 0 ? ' ' : ' comments '}on ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`An error occurred while posting${i === 0 ? ' ' : ' comments '}on ${
|
||||
post.integration?.providerIdentifier
|
||||
}${err?.cause?.message ? `: ${err?.cause?.message}` : ``}`,
|
||||
true,
|
||||
false,
|
||||
'fail'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send webhooks for the post
|
||||
await sendWebhooks(
|
||||
postsResults[0].postId,
|
||||
post.organizationId,
|
||||
post.integration.id
|
||||
);
|
||||
|
||||
// load internal plugs like repost by other users
|
||||
const internalPlugsList = await internalPlugs(
|
||||
post.integration,
|
||||
JSON.parse(post.settings)
|
||||
);
|
||||
|
||||
// load global plugs, like repost a post if it gets to a certain number of likes
|
||||
const globalPlugsList = (await globalPlugs(post.integration)).reduce(
|
||||
(all, current) => {
|
||||
for (let i = 1; i <= current.totalRuns; i++) {
|
||||
all.push({
|
||||
...current,
|
||||
delay: current.delay * i,
|
||||
});
|
||||
}
|
||||
|
||||
return all;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Check if the post is repeatable
|
||||
const repeatPost = !post.intervalInDays
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: 'repeat-post',
|
||||
delay:
|
||||
post.intervalInDays * 24 * 60 * 60 * 1000 -
|
||||
(new Date().getTime() - startTime.getTime()),
|
||||
},
|
||||
];
|
||||
|
||||
// Sort all the actions by delay, so we can process them in order
|
||||
const list = sortBy(
|
||||
[...internalPlugsList, ...globalPlugsList, ...repeatPost],
|
||||
'delay'
|
||||
);
|
||||
|
||||
// process all the plugs in order, we are using while because in some cases we need to remove items from the list
|
||||
while (list.length > 0) {
|
||||
// get the next to process
|
||||
const todo = list.shift();
|
||||
|
||||
// wait for the delay
|
||||
await sleep(todo.delay);
|
||||
|
||||
// process internal plug
|
||||
if (todo.type === 'internal-plug') {
|
||||
while (true) {
|
||||
try {
|
||||
await processInternalPlug({ ...todo, post: postsResults[0].postId });
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshToken(post.integration);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
return false;
|
||||
}
|
||||
|
||||
post.integration.token = refresh.accessToken;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process global plug
|
||||
if (todo.type === 'global') {
|
||||
while (true) {
|
||||
try {
|
||||
const process = await processPlug({
|
||||
...todo,
|
||||
postId: postsResults[0].postId,
|
||||
});
|
||||
if (process) {
|
||||
const toDelete = list
|
||||
.reduce((all, current, index) => {
|
||||
if (current.plugId === todo.plugId) {
|
||||
all.push(index);
|
||||
}
|
||||
|
||||
return all;
|
||||
}, [])
|
||||
.reverse();
|
||||
|
||||
for (const index of toDelete) {
|
||||
list.splice(index, 1);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshToken(post.integration);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
return false;
|
||||
}
|
||||
|
||||
post.integration.token = refresh.accessToken;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process repeat post in a new workflow, this is important so the other plugs can keep running
|
||||
if (todo.type === 'repeat-post') {
|
||||
await startChild(postWorkflowV101, {
|
||||
parentClosePolicy: 'ABANDON',
|
||||
args: [
|
||||
{
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow: true,
|
||||
},
|
||||
],
|
||||
workflowId: `post_${post.id}_${makeId(10)}`,
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: postIdSearchParam,
|
||||
value: postId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ import { TemporalService } from 'nestjs-temporal-core';
|
|||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import {
|
||||
organizationId,
|
||||
postId as postIdSearchParam,
|
||||
} from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
const parser = new Parser();
|
||||
|
||||
|
|
@ -106,7 +105,7 @@ export class AutopostService {
|
|||
try {
|
||||
return this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.start('postWorkflow', {
|
||||
?.workflow.start('postWorkflowV101', {
|
||||
workflowId: `autopost-${id}`,
|
||||
taskQueue: 'main',
|
||||
args: [{ id, immediately: true }],
|
||||
|
|
@ -286,6 +285,7 @@ export class AutopostService {
|
|||
value: [
|
||||
{
|
||||
id: makeId(10),
|
||||
delay: 0,
|
||||
content:
|
||||
state.description.replace(/\n/g, '\n\n') +
|
||||
'\n\n' +
|
||||
|
|
|
|||
|
|
@ -383,6 +383,7 @@ export class PostsRepository {
|
|||
}
|
||||
: {}),
|
||||
content: value.content,
|
||||
delay: value.delay || 0,
|
||||
group: uuid,
|
||||
intervalInDays: inter ? +inter : null,
|
||||
approvedSubmitForOrder: APPROVED_SUBMIT_FOR_ORDER.NO,
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ export class PostsService {
|
|||
|
||||
await this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.start('postWorkflow', {
|
||||
?.workflow.start('postWorkflowV101', {
|
||||
workflowId: `post_${posts[0].id}`,
|
||||
taskQueue: 'main',
|
||||
args: [
|
||||
|
|
@ -533,7 +533,7 @@ export class PostsService {
|
|||
|
||||
await this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.start('postWorkflow', {
|
||||
?.workflow.start('postWorkflowV101', {
|
||||
workflowId: `post_${getPostById.id}`,
|
||||
taskQueue: 'main',
|
||||
args: [
|
||||
|
|
@ -622,10 +622,12 @@ export class PostsService {
|
|||
...toPost.list.map((l) => ({
|
||||
id: '',
|
||||
content: l.post,
|
||||
delay: 0,
|
||||
image: [],
|
||||
})),
|
||||
{
|
||||
id: '',
|
||||
delay: 0,
|
||||
content: `Check out the full story here:\n${
|
||||
body.postId || body.url
|
||||
}`,
|
||||
|
|
|
|||
|
|
@ -385,6 +385,7 @@ model Post {
|
|||
organizationId String
|
||||
integrationId String
|
||||
content String
|
||||
delay Int @default(0)
|
||||
group String
|
||||
title String?
|
||||
description String?
|
||||
|
|
|
|||
|
|
@ -1,9 +1,25 @@
|
|||
import {
|
||||
ArrayMinSize, IsArray, IsBoolean, IsDateString, IsDefined, IsIn, IsNumber, IsOptional, IsString, MinLength, Validate, ValidateIf, ValidateNested
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsDefined,
|
||||
IsIn,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MinLength,
|
||||
Validate,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto';
|
||||
import { allProviders, type AllProvidersSettings, EmptySettings } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings';
|
||||
import {
|
||||
allProviders,
|
||||
type AllProvidersSettings,
|
||||
EmptySettings,
|
||||
} from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings';
|
||||
import { ValidContent } from '@gitroom/helpers/utils/valid.images';
|
||||
|
||||
export class Integration {
|
||||
|
|
@ -22,6 +38,10 @@ export class PostContent {
|
|||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
delay: number;
|
||||
|
||||
@IsArray()
|
||||
@Type(() => MediaDto)
|
||||
@ValidateNested({ each: true })
|
||||
|
|
|
|||
Loading…
Reference in New Issue