feat: auto posts
This commit is contained in:
parent
b4a8a1332d
commit
1837599ef9
|
|
@ -30,6 +30,7 @@ import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.
|
|||
import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments';
|
||||
import { WebhookController } from '@gitroom/backend/api/routes/webhooks.controller';
|
||||
import { SignatureController } from '@gitroom/backend/api/routes/signature.controller';
|
||||
import { AutopostController } from '@gitroom/backend/api/routes/autopost.controller';
|
||||
|
||||
const authenticatedController = [
|
||||
UsersController,
|
||||
|
|
@ -46,6 +47,7 @@ const authenticatedController = [
|
|||
AgenciesController,
|
||||
WebhookController,
|
||||
SignatureController,
|
||||
AutopostController,
|
||||
];
|
||||
@Module({
|
||||
imports: [UploadModule],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
import { AutopostDto } from '@gitroom/nestjs-libraries/dtos/autopost/autopost.dto';
|
||||
import { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@ApiTags('Autopost')
|
||||
@Controller('/autopost')
|
||||
export class AutopostController {
|
||||
constructor(private _autopostsService: AutopostService) {}
|
||||
|
||||
@Get('/')
|
||||
async getAutoposts(@GetOrgFromRequest() org: Organization) {
|
||||
return this._autopostsService.getAutoposts(org.id);
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.WEBHOOKS])
|
||||
async createAutopost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: AutopostDto
|
||||
) {
|
||||
return this._autopostsService.createAutopost(org.id, body);
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
async updateAutopost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: AutopostDto,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._autopostsService.createAutopost(org.id, body, id);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
async deleteAutopost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._autopostsService.deleteAutopost(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/:id/active')
|
||||
async changeActive(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('active') active: boolean
|
||||
) {
|
||||
return this._autopostsService.changeActive(org.id, id, active);
|
||||
}
|
||||
|
||||
@Post('/send')
|
||||
async sendWebhook(@Query('url') url: string) {
|
||||
return this._autopostsService.loadXML(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -132,10 +132,7 @@ export class PublicController {
|
|||
}
|
||||
|
||||
@Post('/crypto/:path')
|
||||
async cryptoPost(
|
||||
@Body() body: any,
|
||||
@Param('path') path: string
|
||||
) {
|
||||
async cryptoPost(@Body() body: any, @Param('path') path: string) {
|
||||
console.log('cryptoPost', body, path);
|
||||
return this._nowpayments.processPayment(path, body);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,419 @@
|
|||
import React, { FC, Fragment, useCallback, useMemo, useState } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { array, boolean, object, string } from 'yup';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import clsx from 'clsx';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { CopilotTextarea } from '@copilotkit/react-textarea';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { Slider } from '@gitroom/react/form/slider';
|
||||
|
||||
export const Autopost: FC = () => {
|
||||
const fetch = useFetch();
|
||||
const modal = useModals();
|
||||
const toaster = useToaster();
|
||||
|
||||
const list = useCallback(async () => {
|
||||
return (await fetch('/autopost')).json();
|
||||
}, []);
|
||||
|
||||
const { data, mutate } = useSWR('autopost', list);
|
||||
|
||||
const addWebhook = useCallback(
|
||||
(data?: any) => () => {
|
||||
modal.openModal({
|
||||
title: '',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
children: <AddOrEditWebhook data={data} reload={mutate} />,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const deleteHook = useCallback(
|
||||
(data: any) => async () => {
|
||||
if (await deleteDialog(`Are you sure you want to delete ${data.name}?`)) {
|
||||
await fetch(`/autopost/${data.id}`, { method: 'DELETE' });
|
||||
mutate();
|
||||
toaster.show('Webhook deleted successfully', 'success');
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const changeActive = useCallback(
|
||||
(data: any) => async (ac: 'on' | 'off') => {
|
||||
await fetch(`/autopost/${data.id}/active`, {
|
||||
body: JSON.stringify({ active: ac === 'on' }),
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
mutate();
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[20px]">Autopost</h3>
|
||||
<div className="text-customColor18 mt-[4px]">
|
||||
Autopost can automatically posts your RSS new items to social media
|
||||
</div>
|
||||
<div className="my-[16px] mt-[16px] bg-sixth border-fifth items-center border rounded-[4px] p-[24px] flex gap-[24px]">
|
||||
<div className="flex flex-col w-full">
|
||||
{!!data?.length && (
|
||||
<div className="grid grid-cols-[1fr,1fr,1fr,1fr,1fr] w-full gap-y-[10px]">
|
||||
<div>Title</div>
|
||||
<div>URL</div>
|
||||
<div>Edit</div>
|
||||
<div>Delete</div>
|
||||
<div>Active</div>
|
||||
{data?.map((p: any) => (
|
||||
<Fragment key={p.id}>
|
||||
<div className="flex flex-col justify-center">{p.title}</div>
|
||||
<div className="flex flex-col justify-center">{p.url}</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<div>
|
||||
<Button onClick={addWebhook(p)}>Edit</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<div>
|
||||
<Button onClick={deleteHook(p)}>Delete</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Slider
|
||||
value={p.active ? 'on' : 'off'}
|
||||
onChange={changeActive(p)}
|
||||
fill={true}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
onClick={addWebhook()}
|
||||
className={clsx((data?.length || 0) > 0 && 'my-[16px]')}
|
||||
>
|
||||
Add an autopost
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const details = object().shape({
|
||||
title: string().required(),
|
||||
content: string(),
|
||||
onSlot: boolean().required(),
|
||||
syncLast: boolean().required(),
|
||||
url: string().url().required(),
|
||||
active: boolean().required(),
|
||||
addPicture: boolean().required(),
|
||||
generateContent: boolean().required(),
|
||||
integrations: array().of(
|
||||
object().shape({
|
||||
id: string().required(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const options = [
|
||||
{ label: 'All integrations', value: 'all' },
|
||||
{ label: 'Specific integrations', value: 'specific' },
|
||||
];
|
||||
|
||||
const optionsChoose = [
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false },
|
||||
];
|
||||
|
||||
const postImmediately = [
|
||||
{ label: 'Post on the next available slot', value: true },
|
||||
{ label: 'Post Immediately', value: false },
|
||||
];
|
||||
|
||||
export const AddOrEditWebhook: FC<{ data?: any; reload: () => void }> = (
|
||||
props
|
||||
) => {
|
||||
const { data, reload } = props;
|
||||
const fetch = useFetch();
|
||||
const [allIntegrations, setAllIntegrations] = useState(
|
||||
(JSON.parse(data?.integrations || '[]')?.length || 0) > 0
|
||||
? options[1]
|
||||
: options[0]
|
||||
);
|
||||
const modal = useModals();
|
||||
const toast = useToaster();
|
||||
const [valid, setValid] = useState(data?.url || '');
|
||||
const [lastUrl, setLastUrl] = useState(data?.lastUrl || '');
|
||||
|
||||
const form = useForm({
|
||||
resolver: yupResolver(details),
|
||||
values: {
|
||||
title: data?.title || '',
|
||||
content: data?.content || '',
|
||||
onSlot: data?.onSlot || false,
|
||||
syncLast: data?.syncLast || false,
|
||||
url: data?.url || '',
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
active: data?.hasOwnProperty?.('active') ? data?.active : true,
|
||||
addPicture: data?.addPicture || false,
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
generateContent: data?.hasOwnProperty?.('generateContent')
|
||||
? data?.generateContent
|
||||
: true,
|
||||
integrations: JSON.parse(data?.integrations || '[]') || [],
|
||||
},
|
||||
});
|
||||
|
||||
const generateContent = form.watch('generateContent');
|
||||
const content = form.watch('content');
|
||||
const url = form.watch('url');
|
||||
const syncLast = form.watch('syncLast');
|
||||
|
||||
const integrations = form.watch('integrations');
|
||||
const integration = useCallback(async () => {
|
||||
return (await fetch('/integrations/list')).json();
|
||||
}, []);
|
||||
|
||||
const changeIntegration = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const findValue = options.find(
|
||||
(option) => option.value === e.target.value
|
||||
)!;
|
||||
setAllIntegrations(findValue);
|
||||
if (findValue.value === 'all') {
|
||||
form.setValue('integrations', []);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const { data: dataList, isLoading } = useSWR('integrations', integration);
|
||||
|
||||
const callBack = useCallback(
|
||||
async (values: any) => {
|
||||
await fetch(data?.id ? `/autopost/${data?.id}` : '/autopost', {
|
||||
method: data?.id ? 'PUT' : 'POST',
|
||||
body: JSON.stringify({
|
||||
...(data?.id ? { id: data.id } : {}),
|
||||
...values,
|
||||
...(!syncLast ? { lastUrl } : { lastUrl: '' }),
|
||||
}),
|
||||
});
|
||||
|
||||
toast.show(
|
||||
data?.id
|
||||
? 'Webhook updated successfully'
|
||||
: 'Webhook added successfully',
|
||||
'success'
|
||||
);
|
||||
|
||||
modal.closeAll();
|
||||
reload();
|
||||
},
|
||||
[data, integrations, lastUrl, syncLast]
|
||||
);
|
||||
|
||||
const sendTest = useCallback(async () => {
|
||||
const url = form.getValues('url');
|
||||
try {
|
||||
const { success, url: newUrl } = await (
|
||||
await fetch(`/autopost/send?url=${encodeURIComponent(url)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
contentType: 'application/json',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
if (!success) {
|
||||
setValid('');
|
||||
toast.show('Could not use this RSS feed', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.show('RSS valid!', 'success');
|
||||
setValid(url);
|
||||
setLastUrl(newUrl);
|
||||
} catch (e: any) {
|
||||
/** empty **/
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(callBack)}>
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 bg-sixth p-[16px] pt-0 w-[500px]">
|
||||
<TopTitle title={data ? 'Edit autopost' : 'Add autopost'} />
|
||||
<button
|
||||
className="outline-none absolute right-[20px] top-[15px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
onClick={modal.closeAll}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<Input label="Title" {...form.register('title')} />
|
||||
<Input label="URL" {...form.register('url')} />
|
||||
<Select
|
||||
label="Should we sync the current last post?"
|
||||
{...form.register('syncLast', {
|
||||
setValueAs: (value) => {
|
||||
return value === 'true' || value === true;
|
||||
},
|
||||
})}
|
||||
>
|
||||
{optionsChoose.map((option) => (
|
||||
<option key={String(option.value)} value={String(option.value)}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
label="When should we post it?"
|
||||
{...form.register('onSlot', {
|
||||
setValueAs: (value) => value === 'true' || value === true,
|
||||
})}
|
||||
>
|
||||
{postImmediately.map((option) => (
|
||||
<option key={String(option.value)} value={String(option.value)}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
label="Autogenerate content"
|
||||
{...form.register('generateContent', {
|
||||
setValueAs: (value) => value === 'true' || value === true,
|
||||
})}
|
||||
>
|
||||
{optionsChoose.map((option) => (
|
||||
<option key={String(option.value)} value={String(option.value)}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{!generateContent && (
|
||||
<>
|
||||
<div className={`${interClass} text-[14px] mb-[6px]`}>
|
||||
Post content
|
||||
</div>
|
||||
<CopilotTextarea
|
||||
disableBranding={true}
|
||||
className={clsx(
|
||||
'!min-h-40 !max-h-80 p-2 overflow-x-hidden scrollbar scrollbar-thumb-[#612AD5] bg-customColor2 outline-none mb-[16px] border-fifth border rounded-[4px]'
|
||||
)}
|
||||
value={content}
|
||||
onChange={(e) => {
|
||||
form.setValue('content', e.target.value);
|
||||
}}
|
||||
placeholder="Write your post..."
|
||||
autosuggestionsConfig={{
|
||||
textareaPurpose: `Assist me in writing social media post`,
|
||||
chatApiConfigs: {},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Select
|
||||
label="Generate Picture?"
|
||||
{...form.register('addPicture', {
|
||||
setValueAs: (value) => value === 'true' || value === true,
|
||||
})}
|
||||
>
|
||||
{optionsChoose.map((option) => (
|
||||
<option key={String(option.value)} value={String(option.value)}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
value={allIntegrations.value}
|
||||
name="integrations"
|
||||
label="Integrations"
|
||||
disableForm={true}
|
||||
onChange={changeIntegration}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{allIntegrations.value === 'specific' && dataList && !isLoading && (
|
||||
<PickPlatforms
|
||||
integrations={dataList.integrations}
|
||||
selectedIntegrations={integrations as any[]}
|
||||
onChange={(e) => form.setValue('integrations', e)}
|
||||
singleSelect={false}
|
||||
toolTip={true}
|
||||
isMain={true}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-[10px]">
|
||||
{valid === url && (syncLast || !!lastUrl) && (
|
||||
<Button
|
||||
type="submit"
|
||||
className="mt-[24px]"
|
||||
disabled={
|
||||
valid !== url ||
|
||||
!form.formState.isValid ||
|
||||
(allIntegrations.value === 'specific' &&
|
||||
!integrations?.length)
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-[24px]"
|
||||
onClick={sendTest}
|
||||
disabled={
|
||||
!form.formState.isValid ||
|
||||
(allIntegrations.value === 'specific' &&
|
||||
!integrations?.length)
|
||||
}
|
||||
>
|
||||
Send Test
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -24,6 +24,7 @@ import { Webhooks } from '@gitroom/frontend/components/webhooks/webhooks';
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Tabs } from '@mantine/core';
|
||||
import { SignaturesComponent } from '@gitroom/frontend/components/settings/signatures.component';
|
||||
import { Autopost } from '@gitroom/frontend/components/autopost/autopost';
|
||||
|
||||
export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
|
||||
const { isGeneral } = useVariables();
|
||||
|
|
@ -127,6 +128,9 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
|
|||
{!!user?.tier?.webhooks && (
|
||||
<Tabs.Tab value="webhooks">Webhooks</Tabs.Tab>
|
||||
)}
|
||||
{!!user?.tier?.autoPost && (
|
||||
<Tabs.Tab value="autopost">Auto Post</Tabs.Tab>
|
||||
)}
|
||||
<Tabs.Tab value="signatures">Signatures</Tabs.Tab>
|
||||
{!!user?.tier?.public_api && isGeneral && showLogout && (
|
||||
<Tabs.Tab value="api">Public API</Tabs.Tab>
|
||||
|
|
@ -227,6 +231,12 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
|
|||
</Tabs.Panel>
|
||||
)}
|
||||
|
||||
{!!user?.tier?.autoPost && (
|
||||
<Tabs.Panel value="autopost" pt="md">
|
||||
<Autopost />
|
||||
</Tabs.Panel>
|
||||
)}
|
||||
|
||||
<Tabs.Panel value="signatures" pt="md">
|
||||
<SignaturesComponent />
|
||||
</Tabs.Panel>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ import { Controller } from '@nestjs/common';
|
|||
import { EventPattern, Transport } from '@nestjs/microservices';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
|
||||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
|
||||
@Controller()
|
||||
export class PostsController {
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _webhooksService: WebhooksService
|
||||
private _webhooksService: WebhooksService,
|
||||
private _autopostsService: AutopostService
|
||||
) {}
|
||||
|
||||
@EventPattern('post', Transport.REDIS)
|
||||
|
|
@ -32,9 +34,11 @@ export class PostsController {
|
|||
|
||||
@EventPattern('webhooks', Transport.REDIS)
|
||||
async webhooks(data: { org: string; since: string }) {
|
||||
return this._webhooksService.fireWebhooks(
|
||||
data.org,
|
||||
data.since
|
||||
);
|
||||
return this._webhooksService.fireWebhooks(data.org, data.since);
|
||||
}
|
||||
|
||||
@EventPattern('cron', Transport.REDIS)
|
||||
async cron(data: { id: string }) {
|
||||
return this._autopostsService.startAutopost(data.id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ export class BullMqClient extends ClientProxy {
|
|||
return queue.remove(jobId);
|
||||
}
|
||||
|
||||
deleteScheduler(pattern: string, jobId: string) {
|
||||
const queue = this.getQueue(pattern);
|
||||
return queue.removeJobScheduler(jobId);
|
||||
}
|
||||
|
||||
async publishAsync(
|
||||
packet: ReadPacket<any>,
|
||||
callback: (packet: WritePacket<any>) => void
|
||||
|
|
@ -75,6 +80,24 @@ export class BullMqClient extends ClientProxy {
|
|||
async dispatchEvent(packet: ReadPacket<any>): Promise<any> {
|
||||
console.log('event to dispatch: ', packet);
|
||||
const queue = this.getQueue(packet.pattern);
|
||||
if (packet.data.options.cron) {
|
||||
const { cron, immediately } = packet.data.options;
|
||||
const id = packet.data.id ?? v4();
|
||||
await queue.upsertJobScheduler(
|
||||
id,
|
||||
{ pattern: cron, ...(immediately ? { immediately } : {}) },
|
||||
{
|
||||
name: id,
|
||||
data: packet.data,
|
||||
opts: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await queue.add(packet.pattern, packet.data, {
|
||||
jobId: packet.data.id ?? v4(),
|
||||
...packet.data.options,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { AutopostDto } from '@gitroom/nestjs-libraries/dtos/autopost/autopost.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AutopostRepository {
|
||||
constructor(private _autoPost: PrismaRepository<'autoPost'>) {}
|
||||
|
||||
getTotal(orgId: string) {
|
||||
return this._autoPost.model.autoPost.count({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getAutoposts(orgId: string) {
|
||||
return this._autoPost.model.autoPost.findMany({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteAutopost(orgId: string, id: string) {
|
||||
return this._autoPost.model.autoPost.update({
|
||||
where: {
|
||||
id,
|
||||
organizationId: orgId,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getAutopost(id: string) {
|
||||
return this._autoPost.model.autoPost.findUnique({
|
||||
where: {
|
||||
id,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateUrl(id: string, url: string) {
|
||||
return this._autoPost.model.autoPost.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
lastUrl: url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
changeActive(orgId: string, id: string, active: boolean) {
|
||||
return this._autoPost.model.autoPost.update({
|
||||
where: {
|
||||
id,
|
||||
organizationId: orgId,
|
||||
},
|
||||
data: {
|
||||
active,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createAutopost(orgId: string, body: AutopostDto, id?: string) {
|
||||
const { id: newId, active } = await this._autoPost.model.autoPost.upsert({
|
||||
where: {
|
||||
id: id || uuidv4(),
|
||||
organizationId: orgId,
|
||||
},
|
||||
create: {
|
||||
organizationId: orgId,
|
||||
url: body.url,
|
||||
title: body.title,
|
||||
integrations: JSON.stringify(body.integrations),
|
||||
active: body.active,
|
||||
content: body.content,
|
||||
generateContent: body.generateContent,
|
||||
addPicture: body.addPicture,
|
||||
syncLast: body.syncLast,
|
||||
onSlot: body.onSlot,
|
||||
lastUrl: body.lastUrl,
|
||||
},
|
||||
update: {
|
||||
url: body.url,
|
||||
title: body.title,
|
||||
integrations: JSON.stringify(body.integrations),
|
||||
active: body.active,
|
||||
content: body.content,
|
||||
generateContent: body.generateContent,
|
||||
addPicture: body.addPicture,
|
||||
syncLast: body.syncLast,
|
||||
onSlot: body.onSlot,
|
||||
lastUrl: body.lastUrl,
|
||||
},
|
||||
});
|
||||
|
||||
return { id: newId, active };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository';
|
||||
import { AutopostDto } from '@gitroom/nestjs-libraries/dtos/autopost/autopost.dto';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import dayjs from 'dayjs';
|
||||
import { END, START, StateGraph } from '@langchain/langgraph';
|
||||
import { AutoPost, Integration } from '@prisma/client';
|
||||
import { BaseMessage } from '@langchain/core/messages';
|
||||
import striptags from 'striptags';
|
||||
import { ChatOpenAI, DallEAPIWrapper } from '@langchain/openai';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { z } from 'zod';
|
||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import Parser from 'rss-parser';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
const parser = new Parser();
|
||||
|
||||
interface WorkflowChannelsState {
|
||||
messages: BaseMessage[];
|
||||
integrations: Integration[];
|
||||
body: AutoPost;
|
||||
description: string;
|
||||
image: string;
|
||||
id: string;
|
||||
load: {
|
||||
date: string;
|
||||
url: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
const dalle = new DallEAPIWrapper({
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
|
||||
model: 'dall-e-3',
|
||||
});
|
||||
|
||||
const generateContent = z.object({
|
||||
socialMediaPostContent: z
|
||||
.string()
|
||||
.describe('Content for social media posts max 120 chars'),
|
||||
});
|
||||
|
||||
const dallePrompt = z.object({
|
||||
generatedTextToBeSentToDallE: z
|
||||
.string()
|
||||
.describe('Generated prompt from description to be sent to DallE'),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class AutopostService {
|
||||
constructor(
|
||||
private _autopostsRepository: AutopostRepository,
|
||||
private _workerServiceProducer: BullMqClient,
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService
|
||||
) {}
|
||||
|
||||
async stopAll(org: string) {
|
||||
const getAll = (await this.getAutoposts(org)).filter(f => f.active);
|
||||
for (const autopost of getAll) {
|
||||
await this.changeActive(org, autopost.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
getAutoposts(orgId: string) {
|
||||
return this._autopostsRepository.getAutoposts(orgId);
|
||||
}
|
||||
|
||||
async createAutopost(orgId: string, body: AutopostDto, id?: string) {
|
||||
const data = await this._autopostsRepository.createAutopost(
|
||||
orgId,
|
||||
body,
|
||||
id
|
||||
);
|
||||
|
||||
await this.processCron(body.active, data.id);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async changeActive(orgId: string, id: string, active: boolean) {
|
||||
const data = await this._autopostsRepository.changeActive(
|
||||
orgId,
|
||||
id,
|
||||
active
|
||||
);
|
||||
await this.processCron(active, id);
|
||||
return data;
|
||||
}
|
||||
|
||||
async processCron(active: boolean, id: string) {
|
||||
if (active) {
|
||||
return this._workerServiceProducer.emit('cron', {
|
||||
id,
|
||||
options: {
|
||||
cron: '1 * * * *',
|
||||
immediately: true,
|
||||
},
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return this._workerServiceProducer.deleteScheduler('cron', id);
|
||||
}
|
||||
|
||||
async deleteAutopost(orgId: string, id: string) {
|
||||
const data = await this._autopostsRepository.deleteAutopost(orgId, id);
|
||||
await this.processCron(false, id);
|
||||
return data;
|
||||
}
|
||||
|
||||
async loadXML(url: string) {
|
||||
try {
|
||||
const {items} = await parser.parseURL(url);
|
||||
const findLast = items.reduce(
|
||||
(all: any, current: any) => {
|
||||
if (dayjs(current.pubDate).isAfter(all.pubDate)) {
|
||||
return current;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
{ pubDate: dayjs().subtract(100, 'years') }
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
date: findLast.pubDate,
|
||||
url: findLast.link,
|
||||
description: striptags(
|
||||
findLast?.['content:encoded'] || findLast?.content || findLast?.description || ''
|
||||
)
|
||||
.replace(/\n/g, ' ')
|
||||
.trim(),
|
||||
};
|
||||
} catch (err) {
|
||||
/** sent **/
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
static state = () =>
|
||||
new StateGraph<WorkflowChannelsState>({
|
||||
channels: {
|
||||
messages: {
|
||||
reducer: (currentState, updateValue) =>
|
||||
currentState.concat(updateValue),
|
||||
default: () => [],
|
||||
},
|
||||
body: null,
|
||||
description: null,
|
||||
load: null,
|
||||
image: null,
|
||||
integrations: null,
|
||||
id: null,
|
||||
},
|
||||
});
|
||||
|
||||
async loadUrl(url: string) {
|
||||
try {
|
||||
const loadDom = new JSDOM(await (await fetch(url)).text());
|
||||
loadDom.window.document
|
||||
.querySelectorAll('script')
|
||||
.forEach((s) => s.remove());
|
||||
loadDom.window.document
|
||||
.querySelectorAll('style')
|
||||
.forEach((s) => s.remove());
|
||||
// remove all html, script and styles
|
||||
return striptags(loadDom.window.document.body.innerHTML);
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async generateDescription(state: WorkflowChannelsState) {
|
||||
if (!state.body.generateContent) {
|
||||
return {
|
||||
...state,
|
||||
description: state.body.content,
|
||||
};
|
||||
}
|
||||
|
||||
const description =
|
||||
state.load.description || (await this.loadUrl(state.load.url));
|
||||
if (!description) {
|
||||
return {
|
||||
...state,
|
||||
description: '',
|
||||
};
|
||||
}
|
||||
|
||||
const structuredOutput = model.withStructuredOutput(generateContent);
|
||||
const { socialMediaPostContent } = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets raw 'description' of a content and generate a social media post content.
|
||||
Rules:
|
||||
- Maximum 100 chars
|
||||
- Try to make it a short as possible to fit any social media
|
||||
- Add line breaks between sentences (\\n)
|
||||
- Don't add hashtags
|
||||
- Add emojis when needed
|
||||
|
||||
'description':
|
||||
{content}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
content: description,
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
description: socialMediaPostContent,
|
||||
};
|
||||
}
|
||||
|
||||
async generatePicture(state: WorkflowChannelsState) {
|
||||
const structuredOutput = model.withStructuredOutput(dallePrompt);
|
||||
const { generatedTextToBeSentToDallE } =
|
||||
await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets description and generate a prompt that will be sent to DallE to generate pictures.
|
||||
|
||||
content:
|
||||
{content}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
content: state.load.description || state.description,
|
||||
});
|
||||
|
||||
const image = await dalle.invoke(generatedTextToBeSentToDallE);
|
||||
|
||||
return { ...state, image };
|
||||
}
|
||||
|
||||
async schedulePost(state: WorkflowChannelsState) {
|
||||
const nextTime = await this._postsService.findFreeDateTime(
|
||||
state.integrations[0].organizationId
|
||||
);
|
||||
|
||||
await this._postsService.createPost(state.integrations[0].organizationId, {
|
||||
date: nextTime + 'Z',
|
||||
order: makeId(10),
|
||||
shortLink: false,
|
||||
type: 'draft',
|
||||
tags: [],
|
||||
posts: state.integrations.map((i) => ({
|
||||
settings: {
|
||||
subtitle: '',
|
||||
title: '',
|
||||
tags: [],
|
||||
subreddit: [],
|
||||
},
|
||||
group: makeId(10),
|
||||
integration: { id: i.id },
|
||||
value: [
|
||||
{
|
||||
id: makeId(10),
|
||||
content: state.description.replace(/\n/g, '\n\n') + '\n\n' + state.load.url,
|
||||
image: !state.image
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: makeId(10),
|
||||
name: makeId(10),
|
||||
path: state.image,
|
||||
organizationId: state.integrations[0].organizationId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async updateUrl(state: WorkflowChannelsState) {
|
||||
await this._autopostsRepository.updateUrl(state.id, state.load.url);
|
||||
}
|
||||
|
||||
async startAutopost(id: string) {
|
||||
const getPost = await this._autopostsRepository.getAutopost(id);
|
||||
if (!getPost || !getPost.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const load = await this.loadXML(getPost.url);
|
||||
if (!load.success || load.url === getPost.lastUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = await this._integrationService.getIntegrationsList(
|
||||
getPost.organizationId
|
||||
);
|
||||
|
||||
const parseIntegrations = JSON.parse(getPost.integrations || '[]') || [];
|
||||
const neededIntegrations = integrations.filter((i) =>
|
||||
parseIntegrations.some((ii: any) => ii.id === i.id)
|
||||
);
|
||||
|
||||
const integrationsToSend =
|
||||
parseIntegrations.length === 0 ? integrations : neededIntegrations;
|
||||
if (integrationsToSend.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = AutopostService.state();
|
||||
const workflow = state
|
||||
.addNode('generate-description', this.generateDescription.bind(this))
|
||||
.addNode('generate-picture', this.generatePicture.bind(this))
|
||||
.addNode('schedule-post', this.schedulePost.bind(this))
|
||||
.addNode('update-url', this.updateUrl.bind(this))
|
||||
.addEdge(START, 'generate-description')
|
||||
.addConditionalEdges(
|
||||
'generate-description',
|
||||
(state: WorkflowChannelsState) => {
|
||||
if (!state.description) {
|
||||
return 'schedule-post';
|
||||
}
|
||||
if (state.body.addPicture) {
|
||||
return 'generate-picture';
|
||||
}
|
||||
return 'schedule-post';
|
||||
}
|
||||
)
|
||||
.addEdge('generate-picture', 'schedule-post')
|
||||
.addEdge('schedule-post', 'update-url')
|
||||
.addEdge('update-url', END);
|
||||
|
||||
const app = workflow.compile();
|
||||
await app.invoke({
|
||||
messages: [],
|
||||
id,
|
||||
body: getPost,
|
||||
load,
|
||||
integrations: integrationsToSend,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,8 @@ import { WebhooksRepository } from '@gitroom/nestjs-libraries/database/prisma/we
|
|||
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
|
||||
import { SignatureRepository } from '@gitroom/nestjs-libraries/database/prisma/signatures/signature.repository';
|
||||
import { SignatureService } from '@gitroom/nestjs-libraries/database/prisma/signatures/signature.service';
|
||||
import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository';
|
||||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
|
@ -60,6 +62,8 @@ import { SignatureService } from '@gitroom/nestjs-libraries/database/prisma/sign
|
|||
StripeService,
|
||||
MessagesRepository,
|
||||
SignatureRepository,
|
||||
AutopostRepository,
|
||||
AutopostService,
|
||||
SignatureService,
|
||||
MediaService,
|
||||
MediaRepository,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
|||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { difference, uniq } from 'lodash';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -28,11 +30,24 @@ export class IntegrationService {
|
|||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _integrationRepository: IntegrationRepository,
|
||||
private _autopostsRepository: AutopostRepository,
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _notificationService: NotificationService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
|
||||
async changeActiveCron(orgId: string) {
|
||||
const data = await this._autopostsRepository.getAutoposts(
|
||||
orgId,
|
||||
);
|
||||
|
||||
for (const item of data.filter(f => f.active)) {
|
||||
await this._workerServiceProducer.deleteScheduler('cron', item.id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async setTimes(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
|
|
@ -210,7 +225,10 @@ export class IntegrationService {
|
|||
const integrations = (
|
||||
await this._integrationRepository.getIntegrationsList(org)
|
||||
).filter((f) => !f.disabled);
|
||||
if (!!process.env.STRIPE_PUBLISHABLE_KEY && integrations.length >= totalChannels) {
|
||||
if (
|
||||
!!process.env.STRIPE_PUBLISHABLE_KEY &&
|
||||
integrations.length >= totalChannels
|
||||
) {
|
||||
throw new Error('You have reached the maximum number of channels');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
|||
import dayjs from 'dayjs';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationService {
|
||||
constructor(
|
||||
private _organizationRepository: OrganizationRepository,
|
||||
private _notificationsService: NotificationService
|
||||
private _notificationsService: NotificationService,
|
||||
) {}
|
||||
async createOrgAndUser(
|
||||
body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string },
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ model Organization {
|
|||
webhooks Webhooks[]
|
||||
tags Tags[]
|
||||
signatures Signatures[]
|
||||
autoPost AutoPost[]
|
||||
}
|
||||
|
||||
model Tags {
|
||||
|
|
@ -577,6 +578,27 @@ model Webhooks {
|
|||
@@index([deletedAt])
|
||||
}
|
||||
|
||||
model AutoPost {
|
||||
id String @id @default(uuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
title String
|
||||
content String?
|
||||
onSlot Boolean
|
||||
syncLast Boolean
|
||||
url String
|
||||
lastUrl String
|
||||
active Boolean
|
||||
addPicture Boolean
|
||||
generateContent Boolean
|
||||
integrations String
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([deletedAt])
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface PricingInnerInterface {
|
|||
image_generation_count: number;
|
||||
public_api: boolean;
|
||||
webhooks: number;
|
||||
autoPost: boolean;
|
||||
}
|
||||
export interface PricingInterface {
|
||||
[key: string]: PricingInnerInterface;
|
||||
|
|
@ -33,6 +34,7 @@ export const pricing: PricingInterface = {
|
|||
image_generator: false,
|
||||
public_api: false,
|
||||
webhooks: 0,
|
||||
autoPost: false,
|
||||
},
|
||||
STANDARD: {
|
||||
current: 'STANDARD',
|
||||
|
|
@ -49,6 +51,7 @@ export const pricing: PricingInterface = {
|
|||
image_generator: false,
|
||||
public_api: true,
|
||||
webhooks: 2,
|
||||
autoPost: false,
|
||||
},
|
||||
TEAM: {
|
||||
current: 'TEAM',
|
||||
|
|
@ -65,6 +68,7 @@ export const pricing: PricingInterface = {
|
|||
image_generator: true,
|
||||
public_api: true,
|
||||
webhooks: 10,
|
||||
autoPost: true,
|
||||
},
|
||||
PRO: {
|
||||
current: 'PRO',
|
||||
|
|
@ -81,6 +85,7 @@ export const pricing: PricingInterface = {
|
|||
image_generator: true,
|
||||
public_api: true,
|
||||
webhooks: 30,
|
||||
autoPost: true,
|
||||
},
|
||||
ULTIMATE: {
|
||||
current: 'ULTIMATE',
|
||||
|
|
@ -97,5 +102,6 @@ export const pricing: PricingInterface = {
|
|||
image_generator: true,
|
||||
public_api: true,
|
||||
webhooks: 10000,
|
||||
autoPost: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o
|
|||
import { Organization } from '@prisma/client';
|
||||
import dayjs from 'dayjs';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionService {
|
||||
constructor(
|
||||
private readonly _subscriptionRepository: SubscriptionRepository,
|
||||
private readonly _integrationService: IntegrationService,
|
||||
private readonly _organizationService: OrganizationService
|
||||
private readonly _organizationService: OrganizationService,
|
||||
) {}
|
||||
|
||||
getSubscriptionByOrganizationId(organizationId: string) {
|
||||
|
|
@ -123,6 +122,10 @@ export class SubscriptionService {
|
|||
);
|
||||
}
|
||||
|
||||
if (billing === 'FREE') {
|
||||
await this._integrationService.changeActiveCron(getOrgByCustomerId?.id!);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
// if (to.faq < from.faq) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDefined,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class Integrations {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class AutopostDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
content: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
lastUrl: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsDefined()
|
||||
onSlot: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsDefined()
|
||||
syncLast: boolean;
|
||||
|
||||
@IsUrl()
|
||||
@IsDefined()
|
||||
url: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsDefined()
|
||||
active: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsDefined()
|
||||
addPicture: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsDefined()
|
||||
generateContent: boolean;
|
||||
|
||||
@IsArray()
|
||||
@Type(() => Integrations)
|
||||
@ValidateNested({ each: true })
|
||||
integrations: Integrations[];
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Stripe from 'stripe';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OrderItems, Organization, User } from '@prisma/client';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -90,6 +90,7 @@
|
|||
"@types/remove-markdown": "^0.3.4",
|
||||
"@types/sha256": "^0.2.2",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"@types/striptags": "^0.0.5",
|
||||
"@types/yup": "^0.32.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uiw/react-md-editor": "^4.0.3",
|
||||
|
|
@ -123,6 +124,7 @@
|
|||
"dayjs": "^1.11.10",
|
||||
"emoji-picker-react": "^4.12.0",
|
||||
"facebook-nodejs-business-sdk": "^21.0.5",
|
||||
"fast-xml-parser": "^4.5.1",
|
||||
"google-auth-library": "^9.11.0",
|
||||
"googleapis": "^137.1.0",
|
||||
"ioredis": "^5.3.2",
|
||||
|
|
@ -159,11 +161,13 @@
|
|||
"reflect-metadata": "^0.1.13",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"resend": "^3.2.0",
|
||||
"rss-parser": "^3.13.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"sha256": "^0.2.0",
|
||||
"sharp": "^0.33.4",
|
||||
"simple-statistics": "^7.8.3",
|
||||
"stripe": "^15.5.0",
|
||||
"striptags": "^3.2.0",
|
||||
"sweetalert2": "^11.6.13",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
|
|
|
|||
Loading…
Reference in New Issue