Leon4gr45's picture
Upload folder using huggingface_hub (part 10)
a271c58 verified
Raw
History Blame Contribute Delete
33.5 kB
import {
BadRequestException,
Injectable,
ValidationPipe,
} from '@nestjs/common';
import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.repository';
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
import dayjs from 'dayjs';
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
import {
Integration,
Post,
Media,
From,
CreationMethod,
State,
} from '@prisma/client';
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
import { GetPostsListDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.list.dto';
import { shuffle } from 'lodash';
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import utc from 'dayjs/plugin/utc';
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto';
import {
minifyPostsList,
minifyPosts,
} from '@gitroom/helpers/utils/posts.list.minify';
import axios from 'axios';
import sharp from 'sharp';
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
import { Readable } from 'stream';
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
dayjs.extend(utc);
import * as Sentry from '@sentry/nestjs';
import { TemporalService } from 'nestjs-temporal-core';
import { TypedSearchAttributes } from '@temporalio/common';
import {
organizationId,
postId as postIdSearchParam,
} from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
import { AnalyticsData } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { timer } from '@gitroom/helpers/utils/timer';
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
import { stripLinks } from '@gitroom/helpers/utils/strip.links';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
import { weightedLength } from '@gitroom/helpers/utils/count.length';
type PostWithConditionals = Post & {
integration?: Integration;
childrenPost: Post[];
};
@Injectable()
export class PostsService {
private storage = UploadFactory.createStorage();
constructor(
private _postRepository: PostsRepository,
private _integrationManager: IntegrationManager,
private _integrationService: IntegrationService,
private _mediaService: MediaService,
private _shortLinkService: ShortLinkService,
private _openaiService: OpenaiService,
private _temporalService: TemporalService,
private _refreshIntegrationService: RefreshIntegrationService
) {}
searchForMissingThreeHoursPosts() {
return this._postRepository.searchForMissingThreeHoursPosts();
}
updatePost(id: string, postId: string, releaseURL: string) {
return this._postRepository.updatePost(id, postId, releaseURL);
}
async getMissingContent(
orgId: string,
postId: string,
forceRefresh = false
): Promise<{ id: string; url: string }[]> {
const post = await this._postRepository.getPostById(postId, orgId);
if (!post || post.releaseId !== 'missing') {
return [];
}
const integrationProvider = this._integrationManager.getSocialIntegration(
post.integration.providerIdentifier
);
if (!integrationProvider.missing) {
return [];
}
const getIntegration = post.integration!;
if (
dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
forceRefresh
) {
const data = await this._refreshIntegrationService.refresh(
getIntegration
);
if (!data) {
return [];
}
const { accessToken } = data;
if (accessToken) {
getIntegration.token = accessToken;
if (integrationProvider.refreshWait) {
await timer(10000);
}
} else {
await this._integrationService.disconnectChannel(orgId, getIntegration);
return [];
}
}
try {
return await integrationProvider.missing(
getIntegration.internalId,
getIntegration.token
);
} catch (e) {
console.log(e);
if (e instanceof RefreshToken) {
return this.getMissingContent(orgId, postId, true);
}
}
return [];
}
async getPostById(postId: string, orgId: string) {
return this._postRepository.getPostById(postId, orgId);
}
async updateReleaseId(orgId: string, postId: string, releaseId: string) {
return this._postRepository.updateReleaseId(postId, orgId, releaseId);
}
async checkPostAnalytics(
orgId: string,
postId: string,
date: number,
forceRefresh = false
): Promise<AnalyticsData[] | { missing: true }> {
const post = await this._postRepository.getPostById(postId, orgId);
if (!post || !post.releaseId) {
return [];
}
if (post.releaseId === 'missing') {
return { missing: true };
}
const integrationProvider = this._integrationManager.getSocialIntegration(
post.integration.providerIdentifier
);
if (!integrationProvider.postAnalytics) {
return [];
}
const getIntegration = post.integration!;
if (
dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
forceRefresh
) {
const data = await this._refreshIntegrationService.refresh(
getIntegration
);
if (!data) {
return [];
}
const { accessToken } = data;
if (accessToken) {
getIntegration.token = accessToken;
if (integrationProvider.refreshWait) {
await timer(10000);
}
} else {
await this._integrationService.disconnectChannel(orgId, getIntegration);
return [];
}
}
// const getIntegrationData = await ioRedis.get(
// `integration:${orgId}:${post.id}:${date}`
// );
// if (getIntegrationData) {
// return JSON.parse(getIntegrationData);
// }
try {
const loadAnalytics = await integrationProvider.postAnalytics(
getIntegration.internalId,
getIntegration.token,
post.releaseId,
date
);
await ioRedis.set(
`integration:${orgId}:${post.id}:${date}`,
JSON.stringify(loadAnalytics),
'EX',
!process.env.NODE_ENV || process.env.NODE_ENV === 'development'
? 1
: 3600
);
return loadAnalytics;
} catch (e) {
console.log(e);
if (e instanceof RefreshToken) {
return this.checkPostAnalytics(orgId, postId, date, true);
}
}
return [];
}
async getStatistics(orgId: string, id: string) {
const getPost = await this.getPostsRecursively(id, true, orgId, true);
const content = getPost.map((p) => p.content);
const shortLinksTracking = await this._shortLinkService.getStatistics(
content
);
return {
clicks: shortLinksTracking,
};
}
async mapTypeToPost(
body: CreatePostDto,
organization: string,
replaceDraft: boolean = false
): Promise<CreatePostDto> {
if (!body?.posts?.every((p) => p?.integration?.id)) {
throw new BadRequestException('All posts must have an integration id');
}
const mappedValues = {
...body,
type: replaceDraft ? 'schedule' : body?.type,
posts: await Promise.all(
body?.posts?.map(async (post) => {
const integration = await this._integrationService.getIntegrationById(
organization,
post.integration.id
);
if (!integration) {
throw new BadRequestException(
`Integration with id ${post.integration.id} not found`
);
}
return {
type: replaceDraft ? 'schedule' : body?.type,
...post,
settings: {
...(post.settings || ({} as any)),
__type: integration.providerIdentifier,
},
};
}) || []
),
};
const validationPipe = new ValidationPipe({
skipMissingProperties: false,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
});
return await validationPipe.transform(mappedValues, {
type: 'body',
metatype: CreatePostDto,
});
}
async getPostsRecursively(
id: string,
includeIntegration = false,
orgId?: string,
isFirst?: boolean
): Promise<PostWithConditionals[]> {
const post = await this._postRepository.getPost(
id,
includeIntegration,
orgId,
isFirst
);
if (!post) {
return [];
}
return [
post!,
...(post?.childrenPost?.length
? await this.getPostsRecursively(
post?.childrenPost?.[0]?.id,
false,
orgId,
false
)
: []),
];
}
async getPosts(orgId: string, query: GetPostsDto) {
return this._postRepository.getPosts(orgId, query);
}
async getPostsMinified(orgId: string, query: GetPostsDto) {
return minifyPosts({
posts: await this._postRepository.getPosts(orgId, query),
});
}
async getPostsList(orgId: string, query: GetPostsListDto) {
return minifyPostsList(
await this._postRepository.getPostsList(orgId, query)
);
}
async updateMedia(id: string, imagesList: any[], convertToJPEG = false) {
try {
let imageUpdateNeeded = false;
const getImageList = await Promise.all(
(
await Promise.all(
(imagesList || []).map(async (p: any) => {
if (!p.path && p.id) {
imageUpdateNeeded = true;
return this._mediaService.getMediaById(p.id);
}
return p;
})
)
)
.map((m) => {
return {
...m,
url:
m.path.indexOf('http') === -1
? process.env.FRONTEND_URL +
'/' +
process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY +
m.path
: m.path,
type: 'image',
path:
m.path.indexOf('http') === -1
? process.env.UPLOAD_DIRECTORY + m.path
: m.path,
};
})
.map(async (m) => {
if (!convertToJPEG) {
return m;
}
if (hasExtension(m.path, 'png')) {
imageUpdateNeeded = true;
const response = await axios.get(m.url, {
responseType: 'arraybuffer',
});
const imageBuffer = Buffer.from(response.data);
// Use sharp to get the metadata of the image
const buffer = await sharp(imageBuffer)
.jpeg({ quality: 100 })
.toBuffer();
const { path, originalname } = await this.storage.uploadFile({
buffer,
mimetype: 'image/jpeg',
size: buffer.length,
path: '',
fieldname: '',
destination: '',
stream: new Readable(),
filename: '',
originalname: '',
encoding: '',
});
return {
...m,
name: originalname,
url:
path.indexOf('http') === -1
? process.env.FRONTEND_URL +
'/' +
process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY +
path
: path,
type: 'image',
path:
path.indexOf('http') === -1
? process.env.UPLOAD_DIRECTORY + path
: path,
};
}
return m;
})
);
if (imageUpdateNeeded) {
await this._postRepository.updateImages(
id,
JSON.stringify(getImageList)
);
}
return getImageList;
} catch (err: any) {
return imagesList;
}
}
async getPostGroupDebugExport(orgId: string, group: string) {
const loadAll = await this._postRepository.getPostsByGroup(orgId, group);
const errors = await this._postRepository.getErrorsByPostIds(
loadAll.map((p) => p.id)
);
const posts = this.arrangePostsByGroup(loadAll, undefined);
const rootPost = posts[0] as any;
return {
type: 'draft' as const,
shortLink: false,
date: rootPost.publishDate.toISOString(),
tags:
rootPost.tags?.map((t: any) => ({
value: t.tag.id,
label: t.tag.name,
})) || [],
posts: [
{
integration: { id: 'REPLACE_WITH_LOCAL_INTEGRATION_ID' },
group: rootPost.group,
settings: JSON.parse(rootPost.settings || '{}'),
value: posts.map((post) => ({
content: post.content,
image: JSON.parse(post.image || '[]'),
delay: post.delay || 0,
})),
},
],
_debug: {
providerIdentifier: rootPost.integration?.providerIdentifier,
providerName: rootPost.integration?.name,
state: rootPost.state,
error: rootPost.error,
errors: errors.map((e) => ({
message: e.message,
platform: e.platform,
body: e.body,
createdAt: e.createdAt,
})),
originalGroup: group,
originalPublishDate: rootPost.publishDate,
exportedAt: new Date().toISOString(),
},
};
}
async getPostsByGroup(orgId: string, group: string) {
const convertToJPEG = false;
const loadAll = await this._postRepository.getPostsByGroup(orgId, group);
const posts = this.arrangePostsByGroup(loadAll, undefined);
return {
group: posts?.[0]?.group,
posts: await Promise.all(
(posts || []).map(async (post) => ({
...post,
image: await this.updateMedia(
post.id,
JSON.parse(post.image || '[]'),
convertToJPEG
),
}))
),
integrationPicture: posts[0]?.integration?.picture,
integration: posts[0].integrationId,
settings: JSON.parse(posts[0].settings || '{}'),
};
}
arrangePostsByGroup(all: any, parent?: string): PostWithConditionals[] {
const findAll = all
.filter((p: any) =>
!parent ? !p.parentPostId : p.parentPostId === parent
)
.map(({ integration, ...all }: any) => ({
...all,
...(!parent ? { integration } : {}),
}));
return [
...findAll,
...(findAll.length
? findAll.flatMap((p: any) => this.arrangePostsByGroup(all, p.id))
: []),
];
}
async getPost(orgId: string, id: string, convertToJPEG = false) {
const posts = await this.getPostsRecursively(id, true, orgId, true);
const list = {
group: posts?.[0]?.group,
posts: await Promise.all(
(posts || []).map(async (post) => ({
...post,
image: await this.updateMedia(
post.id,
JSON.parse(post.image || '[]'),
convertToJPEG
),
}))
),
integrationPicture: posts[0]?.integration?.picture,
integration: posts[0].integrationId,
settings: JSON.parse(posts[0].settings || '{}'),
};
return list;
}
async getOldPosts(orgId: string, date: string) {
return this._postRepository.getOldPosts(orgId, date);
}
public async updateTags(orgId: string, post: Post[]): Promise<Post[]> {
const plainText = JSON.stringify(post);
const extract = Array.from(
plainText.match(/\(post:[a-zA-Z0-9-_]+\)/g) || []
);
if (!extract.length) {
return post;
}
const ids = (extract || []).map((e) =>
e.replace('(post:', '').replace(')', '')
);
const urls = await this._postRepository.getPostUrls(orgId, ids);
const newPlainText = ids.reduce((acc, value) => {
const findUrl = urls?.find?.((u) => u.id === value)?.releaseURL || '';
return acc.replace(
new RegExp(`\\(post:${value}\\)`, 'g'),
findUrl.split(',')[0]
);
}, plainText);
return this.updateTags(orgId, JSON.parse(newPlainText) as Post[]);
}
public async checkInternalPlug(
integration: Integration,
orgId: string,
id: string,
settings: any
) {
const plugs = Object.entries(settings).filter(([key]) => {
return key.indexOf('plug-') > -1;
});
if (plugs.length === 0) {
return [];
}
const parsePlugs = plugs.reduce((all, [key, value]) => {
const [_, name, identifier] = key.split('--');
all[name] = all[name] || { name };
all[name][identifier] = value;
return all;
}, {} as any);
const list: {
name: string;
integrations: { id: string }[];
delay: string;
active: boolean;
}[] = Object.values(parsePlugs);
return (list || []).flatMap((trigger) => {
return (trigger?.integrations || []).flatMap((int) => ({
type: 'internal-plug',
post: id,
originalIntegration: integration.id,
integration: int.id,
plugName: trigger.name,
orgId: orgId,
delay: +trigger.delay,
information: trigger,
}));
});
}
public async checkPlugs(
orgId: string,
providerName: string,
integrationId: string
) {
const loadAllPlugs = this._integrationManager.getAllPlugs();
const getPlugs = await this._integrationService.getPlugs(
orgId,
integrationId
);
const currentPlug = loadAllPlugs.find((p) => p.identifier === providerName);
return getPlugs
.filter((plug) => {
return currentPlug?.plugs?.some(
(p: any) => p.methodName === plug.plugFunction
);
})
.map((plug) => {
const runPlug = currentPlug?.plugs?.find(
(p: any) => p.methodName === plug.plugFunction
)!;
return {
type: 'global',
plugId: plug.id,
delay: runPlug.runEveryMilliseconds,
totalRuns: runPlug.totalRuns,
};
});
}
async deletePost(orgId: string, group: string) {
const post = await this._postRepository.deletePost(orgId, group);
if (post?.id) {
try {
const workflows = this._temporalService.client
.getRawClient()
?.workflow.list({
query: `postId="${post.id}" AND ExecutionStatus="Running"`,
});
for await (const executionInfo of workflows) {
try {
const workflow =
await this._temporalService.client.getWorkflowHandle(
executionInfo.workflowId
);
if (
workflow &&
(await workflow.describe()).status.name !== 'TERMINATED'
) {
await workflow.terminate();
}
} catch (err) {}
}
} catch (err) {}
}
return { error: true };
}
async countPostsFromDay(orgId: string, date: Date) {
return this._postRepository.countPostsFromDay(orgId, date);
}
getPostByForWebhookId(id: string) {
return this._postRepository.getPostByForWebhookId(id);
}
async startWorkflow(
taskQueue: string,
postId: string,
orgId: string,
state: State
) {
try {
const workflows = this._temporalService.client
.getRawClient()
?.workflow.list({
query: `postId="${postId}" AND ExecutionStatus="Running"`,
});
for await (const executionInfo of workflows) {
try {
const workflow = await this._temporalService.client.getWorkflowHandle(
executionInfo.workflowId
);
if (
workflow &&
(await workflow.describe()).status.name !== 'TERMINATED'
) {
await workflow.terminate();
}
} catch (err) {}
}
} catch (err) {}
if (state === 'DRAFT') {
return;
}
try {
await this._temporalService.client
.getRawClient()
?.workflow.start('postWorkflowV105', {
workflowId: `post_${postId}`,
taskQueue: 'main',
workflowIdConflictPolicy: 'TERMINATE_EXISTING',
args: [
{
taskQueue: taskQueue,
postId: postId,
organizationId: orgId,
},
],
typedSearchAttributes: new TypedSearchAttributes([
{
key: postIdSearchParam,
value: postId,
},
{
key: organizationId,
value: orgId,
},
]),
});
} catch (err) {}
}
/**
* Server-side validation that used to live on the client (`checkValidity` +
* the manage modal loop). Runs the provider's settings DTO validation, the
* provider `checkValidity` (media rules) and the empty-content / too-long
* character checks. Returns one result per post so the frontend can show the
* same toasts it did before — and so `/posts` can refuse to create invalid
* posts.
*/
async validatePosts(
orgId: string,
posts: Array<{
integration: { id: string };
value: Array<{
content?: string;
image?: Array<{ path: string; thumbnail?: string }>;
}>;
settings?: any;
}>
) {
return Promise.all(
(posts || []).map(async (post) => {
const integration = await this._integrationService.getIntegrationById(
orgId,
post?.integration?.id
);
if (!integration) {
throw new BadRequestException(
`Integration with id ${post?.integration?.id} not found`
);
}
const provider = this._integrationManager.getSocialIntegration(
integration.providerIdentifier
);
let additionalSettings: any[] = [];
try {
additionalSettings = JSON.parse(
integration.additionalSettings || '[]'
);
} catch {
additionalSettings = [];
}
const settings = post.settings || {};
const media = (post.value || []).map((p) => p.image || []);
// Settings DTO validation — mirrors the client `form.trigger()`.
let valid = true;
let settingsError = '';
if (provider?.dto) {
const instance = plainToInstance(provider.dto, settings, {
enableImplicitConversion: false,
});
const validationErrors = await validate(instance as object, {
skipMissingProperties: false,
});
settingsError = this.firstValidationError(validationErrors);
valid = validationErrors.length === 0;
}
// Provider-specific media validation (the old client `checkValidity`).
let errors: string | true = true;
try {
errors = await provider.checkValidity(
media,
settings,
additionalSettings
);
} catch (err: any) {
errors = err?.message || 'Invalid media';
}
const maximumCharacters = provider.maxLength(additionalSettings);
const isX = integration.providerIdentifier === 'x';
const emptyContent = (post.value || []).some((a) => {
const strip = stripHtmlValidation('normal', a.content || '', true);
const length = isX ? weightedLength(strip) : strip.length;
return length === 0 && (a.image || []).length === 0;
});
const tooLong = (post.value || []).some((a) => {
const strip = stripHtmlValidation('normal', a.content || '', true);
const weighted = isX ? weightedLength(strip) : strip.length;
const totalCharacters =
weighted > strip.length ? weighted : strip.length;
return totalCharacters > (maximumCharacters || 1000000);
});
return {
id: integration.id,
identifier: integration.providerIdentifier,
name: integration.name,
valid,
settingsError,
errors,
emptyContent,
tooLong,
maximumCharacters,
};
})
);
}
/** Returns the first class-validator message (incl. nested children), or ''. */
private firstValidationError(errors: any[]): string {
for (const e of errors || []) {
if (e?.constraints) {
return Object.values(e.constraints as Record<string, string>)[0] || '';
}
const child = e?.children?.length
? this.firstValidationError(e.children)
: '';
if (child) {
return child;
}
}
return '';
}
async createPost(
orgId: string,
body: CreatePostDto,
creationMethod: CreationMethod
): Promise<any[]> {
const postList = [];
for (const post of body.posts) {
const provider = this._integrationManager.getSocialIntegration(
(post.settings as any)?.__type
);
const removeLinks = !!provider?.stripLinks?.();
const messages = (post.value || []).map((p) => p.content);
// No point shortlinking links on platforms that strip them out anyway
const updateContent =
!body.shortLink || removeLinks
? messages
: await this._shortLinkService.convertTextToShortLinks(
orgId,
messages
);
post.value = (post.value || []).map((p, i) => ({
...p,
content: removeLinks ? stripLinks(updateContent[i]) : updateContent[i],
}));
const { posts } = await this._postRepository.createOrUpdatePost(
body.type,
orgId,
body.type === 'now' ? dayjs().format('YYYY-MM-DDTHH:mm:00') : body.date,
post,
body.tags,
creationMethod,
body.inter
);
if (!posts?.length) {
return [] as any[];
}
if (body.type !== 'update') {
this.startWorkflow(
post.settings.__type.split('-')[0].toLowerCase(),
posts[0].id,
orgId,
posts[0].state
).catch((err) => {});
}
Sentry.metrics.count('post_created', 1);
postList.push({
postId: posts[0].id,
integration: post.integration.id,
});
}
return postList;
}
async separatePosts(content: string, len: number) {
return this._openaiService.separatePosts(content, len);
}
async changeState(id: string, state: State, err?: any, body?: any) {
return this._postRepository.changeState(id, state, err, body);
}
async changePostStatus(
orgId: string,
id: string,
status: 'draft' | 'schedule'
) {
const getPostById = await this._postRepository.getPostById(id, orgId);
if (!getPostById) {
throw new BadRequestException('Post not found');
}
const state: State = status === 'draft' ? 'DRAFT' : 'QUEUE';
await this._postRepository.changeState(id, state);
try {
await this.startWorkflow(
getPostById.integration.providerIdentifier.split('-')[0].toLowerCase(),
getPostById.id,
orgId,
state
);
} catch (err) {}
return { id, state };
}
async changeDate(
orgId: string,
id: string,
date: string,
action: 'schedule' | 'update' = 'schedule'
) {
const getPostById = await this._postRepository.getPostById(id, orgId);
// schedule: Set status to QUEUE and change date (reschedule the post)
// update: Just change the date without changing the status
const newDate = await this._postRepository.changeDate(
orgId,
id,
date,
getPostById.state === 'DRAFT',
action
);
if (action === 'schedule') {
try {
await this.startWorkflow(
getPostById.integration.providerIdentifier
.split('-')[0]
.toLowerCase(),
getPostById.id,
orgId,
getPostById.state === 'DRAFT' ? 'DRAFT' : 'QUEUE'
);
} catch (err) {}
}
return newDate;
}
async generatePostsDraft(orgId: string, body: CreateGeneratedPostsDto) {
const getAllIntegrations = (
await this._integrationService.getIntegrationsList(orgId)
).filter((f) => !f.disabled && f.providerIdentifier !== 'reddit');
// const posts = chunk(body.posts, getAllIntegrations.length);
const allDates = dayjs()
.isoWeek(body.week)
.year(body.year)
.startOf('isoWeek');
const dates = [...new Array(7)].map((_, i) => {
return allDates.add(i, 'day').format('YYYY-MM-DD');
});
const findTime = (): string => {
const totalMinutes = Math.floor(Math.random() * 144) * 10;
// Convert total minutes to hours and minutes
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
// Format hours and minutes to always be two digits
const formattedHours = hours.toString().padStart(2, '0');
const formattedMinutes = minutes.toString().padStart(2, '0');
const randomDate =
shuffle(dates)[0] + 'T' + `${formattedHours}:${formattedMinutes}:00`;
if (dayjs(randomDate).isBefore(dayjs())) {
return findTime();
}
return randomDate;
};
for (const integration of getAllIntegrations) {
for (const toPost of body.posts) {
const group = makeId(10);
const randomDate = findTime();
await this.createPost(
orgId,
{
type: 'draft',
date: randomDate,
order: '',
shortLink: false,
tags: [],
posts: [
{
group,
integration: {
id: integration.id,
},
settings: {
__type: integration.providerIdentifier as any,
title: '',
tags: [],
subreddit: [],
},
value: [
...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
}`,
image: [],
},
],
},
],
},
'WEB'
);
}
}
}
findAllExistingCategories() {
return this._postRepository.findAllExistingCategories();
}
findAllExistingTopicsOfCategory(category: string) {
return this._postRepository.findAllExistingTopicsOfCategory(category);
}
findPopularPosts(category: string, topic?: string) {
return this._postRepository.findPopularPosts(category, topic);
}
async findFreeDateTime(orgId: string, integrationId?: string) {
const findTimes = await this._integrationService.findFreeDateTime(
orgId,
integrationId
);
return this.findFreeDateTimeRecursive(
orgId,
findTimes,
dayjs.utc().startOf('day')
);
}
async createPopularPosts(post: {
category: string;
topic: string;
content: string;
hook: string;
}) {
return this._postRepository.createPopularPosts(post);
}
private async findFreeDateTimeRecursive(
orgId: string,
times: number[],
date: dayjs.Dayjs
): Promise<string> {
const list = await this._postRepository.getPostsCountsByDates(
orgId,
times,
date
);
if (!list.length) {
return this.findFreeDateTimeRecursive(orgId, times, date.add(1, 'day'));
}
const num = list.reduce<null | number>((prev, curr) => {
if (prev === null || prev > curr) {
return curr;
}
return prev;
}, null) as number;
return date.clone().add(num, 'minutes').format('YYYY-MM-DDTHH:mm:00');
}
getComments(postId: string) {
return this._postRepository.getComments(postId);
}
getTags(orgId: string) {
return this._postRepository.getTags(orgId);
}
createTag(orgId: string, body: CreateTagDto) {
return this._postRepository.createTag(orgId, body);
}
editTag(id: string, orgId: string, body: CreateTagDto) {
return this._postRepository.editTag(id, orgId, body);
}
deleteTag(id: string, orgId: string) {
return this._postRepository.deleteTag(id, orgId);
}
createComment(
orgId: string,
userId: string,
postId: string,
comment: string
) {
return this._postRepository.createComment(orgId, userId, postId, comment);
}
}