diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index d7232688..bfde9ee3 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -55,8 +55,8 @@ export class IntegrationsController { @Post('/function') async functionIntegration( - @GetOrgFromRequest() org: Organization, - @Body() body: IntegrationFunctionDto + @GetOrgFromRequest() org: Organization, + @Body() body: IntegrationFunctionDto ) { const getIntegration = await this._integrationService.getIntegrationById( org.id, @@ -124,7 +124,7 @@ export class IntegrationsController { throw new Error('Invalid api key'); } - return this._integrationService.createIntegration( + return this._integrationService.createOrUpdateIntegration( org.id, name, picture, @@ -166,7 +166,7 @@ export class IntegrationsController { throw new Error('Invalid api key'); } - return this._integrationService.createIntegration( + return this._integrationService.createOrUpdateIntegration( org.id, name, picture, diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts index f79212fb..9b25446c 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -42,6 +42,14 @@ export class PostsController { }; } + @Get('/old') + oldPosts( + @GetOrgFromRequest() org: Organization, + @Query('date') date: string + ) { + return this._postsService.getOldPosts(org.id, date); + } + @Get('/:id') getPost(@GetOrgFromRequest() org: Organization, @Param('id') id: string) { return this._postsService.getPost(org.id, id); diff --git a/apps/commands/src/command.module.ts b/apps/commands/src/command.module.ts index b0e3338b..191f3f2e 100644 --- a/apps/commands/src/command.module.ts +++ b/apps/commands/src/command.module.ts @@ -1,19 +1,25 @@ import { Module } from '@nestjs/common'; import { CommandModule as ExternalCommandModule } from 'nestjs-command'; -import {CheckStars} from "./tasks/check.stars"; -import {DatabaseModule} from "@gitroom/nestjs-libraries/database/prisma/database.module"; -import {RedisModule} from "@gitroom/nestjs-libraries/redis/redis.module"; -import {BullMqModule} from "@gitroom/nestjs-libraries/bull-mq-transport/bull-mq.module"; -import {ioRedis} from "@gitroom/nestjs-libraries/redis/redis.service"; +import { CheckStars } from './tasks/check.stars'; +import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module'; +import { RedisModule } from '@gitroom/nestjs-libraries/redis/redis.module'; +import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport/bull-mq.module'; +import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; +import {RefreshTokens} from "./tasks/refresh.tokens"; @Module({ - imports: [ExternalCommandModule, DatabaseModule, RedisModule, BullMqModule.forRoot({ - connection: ioRedis - })], - controllers: [], - providers: [CheckStars], - get exports() { - return [...this.imports, ...this.providers]; - } + imports: [ + ExternalCommandModule, + DatabaseModule, + RedisModule, + BullMqModule.forRoot({ + connection: ioRedis, + }), + ], + controllers: [], + providers: [CheckStars, RefreshTokens], + get exports() { + return [...this.imports, ...this.providers]; + }, }) export class CommandModule {} diff --git a/apps/commands/src/tasks/refresh.tokens.ts b/apps/commands/src/tasks/refresh.tokens.ts new file mode 100644 index 00000000..27a033d1 --- /dev/null +++ b/apps/commands/src/tasks/refresh.tokens.ts @@ -0,0 +1,16 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; + +@Injectable() +export class RefreshTokens { + constructor(private _integrationService: IntegrationService) {} + @Command({ + command: 'refresh', + describe: 'Refresh all tokens', + }) + async refresh() { + await this._integrationService.refreshTokens(); + return true; + } +} diff --git a/apps/cron/src/cron.module.ts b/apps/cron/src/cron.module.ts index 9a1341a5..b5f9bc8f 100644 --- a/apps/cron/src/cron.module.ts +++ b/apps/cron/src/cron.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; -import {CheckTrending} from "./tasks/check.trending"; +import {RefreshTokens} from "@gitroom/cron/tasks/refresh.tokens"; +import {CheckStars} from "@gitroom/cron/tasks/check.stars"; @Module({ imports: [ScheduleModule.forRoot()], controllers: [], - providers: [CheckTrending], + providers: [RefreshTokens, CheckStars], }) export class CronModule {} diff --git a/apps/cron/src/tasks/check.trending.ts b/apps/cron/src/tasks/check.trending.ts deleted file mode 100644 index 3430b9c1..00000000 --- a/apps/cron/src/tasks/check.trending.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import {Interval} from '@nestjs/schedule'; - -@Injectable() -export class CheckTrending { - @Interval(3600000) - checkTrending() { - console.log('hello'); - } -} \ No newline at end of file diff --git a/apps/cron/src/tasks/refresh.tokens.ts b/apps/cron/src/tasks/refresh.tokens.ts new file mode 100644 index 00000000..62ddbf00 --- /dev/null +++ b/apps/cron/src/tasks/refresh.tokens.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import {Cron} from '@nestjs/schedule'; +import {IntegrationService} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.service"; + +@Injectable() +export class RefreshTokens { + constructor( + private _integrationService: IntegrationService, + ) { + } + @Cron('0 * * * *') + async refresh() { + await this._integrationService.refreshTokens(); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx index 9884c9bb..a9a60f56 100644 --- a/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx +++ b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx @@ -1,7 +1,15 @@ import {internalFetch} from "@gitroom/helpers/utils/internal.fetch"; import {redirect} from "next/navigation"; -export default async function Page({params: {provider}, searchParams}: {params: {provider: string}, searchParams: object}) { +export default async function Page({params: {provider}, searchParams}: {params: {provider: string}, searchParams: any}) { + if (provider === 'x') { + searchParams = { + ...searchParams, + state: searchParams.oauth_token || '', + code: searchParams.oauth_verifier || '' + }; + } + await internalFetch(`/integrations/social/${provider}/connect`, { method: 'POST', body: JSON.stringify(searchParams) diff --git a/apps/frontend/src/components/analytics/stars.and.forks.tsx b/apps/frontend/src/components/analytics/stars.and.forks.tsx index e893153e..b24d3d71 100644 --- a/apps/frontend/src/components/analytics/stars.and.forks.tsx +++ b/apps/frontend/src/components/analytics/stars.and.forks.tsx @@ -7,7 +7,6 @@ import clsx from "clsx"; export const StarsAndForks: FC = (props) => { const {list} = props; - console.log(list); return ( <> {list.map(item => ( diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index 18e3a06b..cc751b5f 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FC, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useState } from 'react'; import dayjs from 'dayjs'; import { Integrations } from '@gitroom/frontend/components/launches/calendar.context'; import clsx from 'clsx'; @@ -27,6 +27,8 @@ import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pic import { ProvidersOptions } from '@gitroom/frontend/components/launches/providers.options'; import { v4 as uuidv4 } from 'uuid'; import { useSWRConfig } from 'swr'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import { postSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector'; export const AddEditModal: FC<{ date: dayjs.Dayjs; @@ -70,6 +72,8 @@ export const AddEditModal: FC<{ const expend = useExpend(); + const toaster = useToaster(); + // if it's edit just set the current integration useEffect(() => { if (existingData.integration) { @@ -159,8 +163,13 @@ export const AddEditModal: FC<{ const schedule = useCallback( (type: 'draft' | 'now' | 'schedule' | 'delete') => async () => { if (type === 'delete') { - if (!await deleteDialog('Are you sure you want to delete this post?', 'Yes, delete it!')) { - return ; + if ( + !(await deleteDialog( + 'Are you sure you want to delete this post?', + 'Yes, delete it!' + )) + ) { + return; } await fetch(`/posts/${existingData.group}`, { method: 'DELETE', @@ -183,7 +192,7 @@ export const AddEditModal: FC<{ for (const key of allKeys) { if (key.value.some((p) => !p.content || p.content.length < 6)) { setShowError(true); - return ; + return; } if (!key.valid) { @@ -205,6 +214,11 @@ export const AddEditModal: FC<{ existingData.group = uuidv4(); mutate('/posts'); + toaster.show( + !existingData.integration + ? 'Added successfully' + : 'Updated successfully' + ); modal.closeAll(); }, [] @@ -270,6 +284,7 @@ export const AddEditModal: FC<{ .getCommands() .filter((f) => f.name !== 'image'), newImage, + postSelector(date), ]} value={p.content} preview="edit" @@ -399,6 +414,7 @@ export const AddEditModal: FC<{ )} diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx index 5e1467d5..e4d617f1 100644 --- a/apps/frontend/src/components/launches/calendar.context.tsx +++ b/apps/frontend/src/components/launches/calendar.context.tsx @@ -81,7 +81,6 @@ export const CalendarWeekProvider: FC<{ const { isLoading } = swr; const { posts, comments } = swr?.data || { posts: [], comments: [] }; - console.log(comments); const changeDate = useCallback( (id: string, date: dayjs.Dayjs) => { setInternalData((d) => diff --git a/apps/frontend/src/components/launches/comments/comment.component.tsx b/apps/frontend/src/components/launches/comments/comment.component.tsx index f964836d..121fa9ee 100644 --- a/apps/frontend/src/components/launches/comments/comment.component.tsx +++ b/apps/frontend/src/components/launches/comments/comment.component.tsx @@ -187,7 +187,7 @@ export const CommentComponent: FC<{ date: dayjs.Dayjs }> = (props) => { }) ).json(); - setCommentsList(list => ([ + setCommentsList((list) => [ { id, user: { email: user?.email!, id: user?.id! }, @@ -195,7 +195,7 @@ export const CommentComponent: FC<{ date: dayjs.Dayjs }> = (props) => { childrenComment: [], }, ...list, - ])); + ]); }, [commentsList, setCommentsList] ); @@ -298,8 +298,6 @@ export const CommentComponent: FC<{ date: dayjs.Dayjs }> = (props) => { - -
{commentsList.map((comment, index) => ( <> @@ -374,6 +372,7 @@ export const CommentComponent: FC<{ date: dayjs.Dayjs }> = (props) => {
))} + ); diff --git a/apps/frontend/src/components/launches/helpers/new.image.component.tsx b/apps/frontend/src/components/launches/helpers/new.image.component.tsx index 85bacfe5..435fbea7 100644 --- a/apps/frontend/src/components/launches/helpers/new.image.component.tsx +++ b/apps/frontend/src/components/launches/helpers/new.image.component.tsx @@ -38,7 +38,8 @@ export const newImage: ICommand = { if ( state1.selectedText.includes('http') || - state1.selectedText.includes('www') + state1.selectedText.includes('www') || + state1.selectedText.includes('(post:') ) { executeCommand({ api, diff --git a/apps/frontend/src/components/launches/helpers/use.integration.ts b/apps/frontend/src/components/launches/helpers/use.integration.ts index bd9cb275..80c91814 100644 --- a/apps/frontend/src/components/launches/helpers/use.integration.ts +++ b/apps/frontend/src/components/launches/helpers/use.integration.ts @@ -1,8 +1,17 @@ -"use client"; +'use client'; -import {createContext, useContext} from "react"; -import {Integrations} from "@gitroom/frontend/components/launches/calendar.context"; +import { createContext, useContext } from 'react'; +import { Integrations } from '@gitroom/frontend/components/launches/calendar.context'; +import dayjs from 'dayjs'; -export const IntegrationContext = createContext<{integration: Integrations|undefined, value: Array<{content: string, id?: string, image?: Array<{path: string, id: string}>}>}>({integration: undefined, value: []}); +export const IntegrationContext = createContext<{ + date: dayjs.Dayjs; + integration: Integrations | undefined; + value: Array<{ + content: string; + id?: string; + image?: Array<{ path: string; id: string }>; + }>; +}>({ integration: undefined, value: [], date: dayjs() }); -export const useIntegration = () => useContext(IntegrationContext); \ No newline at end of file +export const useIntegration = () => useContext(IntegrationContext); diff --git a/apps/frontend/src/components/launches/providers.options.tsx b/apps/frontend/src/components/launches/providers.options.tsx index 05ce9034..7947b37a 100644 --- a/apps/frontend/src/components/launches/providers.options.tsx +++ b/apps/frontend/src/components/launches/providers.options.tsx @@ -3,12 +3,14 @@ import {Integrations} from "@gitroom/frontend/components/launches/calendar.conte import {PickPlatforms} from "@gitroom/frontend/components/launches/helpers/pick.platform.component"; import {IntegrationContext} from "@gitroom/frontend/components/launches/helpers/use.integration"; import {ShowAllProviders} from "@gitroom/frontend/components/launches/providers/show.all.providers"; +import dayjs from "dayjs"; export const ProvidersOptions: FC<{ integrations: Integrations[]; editorValue: Array<{ id?: string; content: string }>; + date: dayjs.Dayjs; }> = (props) => { - const { integrations, editorValue } = props; + const { integrations, editorValue, date } = props; const [selectedIntegrations, setSelectedIntegrations] = useState([ integrations[0], ]); @@ -28,7 +30,7 @@ export const ProvidersOptions: FC<{ hide={integrations.length === 1} /> { - const { value} = useIntegration(); + const { value } = useIntegration(); const settings = useSettings(); const image = useMediaDirectory(); const [coverPicture, title, tags] = settings.watch([ @@ -55,7 +56,12 @@ const DevtoPreview: FC = () => {
- p.content).join('\n')} /> + p.content).join('\n')} + />
); @@ -63,10 +69,15 @@ const DevtoPreview: FC = () => { const DevtoSettings: FC = () => { const form = useSettings(); + const { date } = useIntegration(); return ( <> - + { const HashnodeSettings: FC = () => { const form = useSettings(); + const {date} = useIntegration(); return ( <> - + void }> = (props) => { @@ -74,7 +75,7 @@ export const withProvider = ( show: boolean; }) => { const existingData = useExistingData(); - const { integration } = useIntegration(); + const { integration, date } = useIntegration(); const [editInPlace, setEditInPlace] = useState(!!existingData.integration); const [InPlaceValue, setInPlaceValue] = useState< Array<{ @@ -253,6 +254,7 @@ export const withProvider = ( .getCommands() .filter((f) => f.name !== 'image'), newImage, + postSelector(date), ]} preview="edit" // @ts-ignore @@ -338,6 +340,7 @@ export const withProvider = (
{ const MediumSettings: FC = () => { const form = useSettings(); + const {date} = useIntegration(); return ( <> - +
diff --git a/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx b/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx index 843b1930..b841e9a6 100644 --- a/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx +++ b/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx @@ -12,7 +12,8 @@ import { } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto'; import clsx from 'clsx'; import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory'; -import {deleteDialog} from "@gitroom/react/helpers/delete.dialog"; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; +import MDEditor from '@uiw/react-md-editor'; const RenderRedditComponent: FC<{ type: string; @@ -23,20 +24,16 @@ const RenderRedditComponent: FC<{ const { type, images } = props; - const [firstPost] = useFormatting(topValue, { - removeMarkdown: true, - saveBreaklines: true, - specialFunc: (text: string) => { - return text.slice(0, 280); - }, - }); + const [firstPost] = topValue; switch (type) { case 'self': return ( -
-          {firstPost?.text}
-        
+ ); case 'link': return ( @@ -81,7 +78,6 @@ const RedditPreview: FC = (props) => { return text.slice(0, 280); }, }); - console.log(settings); if (!settings || !settings.length) { return <>Please add at least one Subreddit from the settings; @@ -130,6 +126,11 @@ const RedditPreview: FC = (props) => {
{integration?.name}
+
                         {p.text}
                       
@@ -155,22 +156,29 @@ const RedditSettings: FC = () => { append({}); }, [fields, append]); - const deleteField = useCallback((index: number) => async () => { - if (!await deleteDialog('Are you sure you want to delete this Subreddit?')) return; - remove(index); - }, [fields, remove]); + const deleteField = useCallback( + (index: number) => async () => { + if ( + !(await deleteDialog('Are you sure you want to delete this Subreddit?')) + ) + return; + remove(index); + }, + [fields, remove] + ); return ( <>
{fields.map((field, index) => (
-
+
x
- +
))}
diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index e7bb4d83..dba078ca 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -8,6 +8,8 @@ import { ToolTip } from '@gitroom/frontend/components/layout/top.tip'; import { ShowMediaBoxModal } from '@gitroom/frontend/components/media/media.component'; import Image from 'next/image'; import dynamic from 'next/dynamic'; +import { Toaster } from '@gitroom/react/toaster/toaster'; +import { ShowPostSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector'; const NotificationComponent = dynamic( () => @@ -25,6 +27,8 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { + +
diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index cc188028..ec59db86 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -77,7 +77,6 @@ export const MediaBox: FC<{ }) ).json(); - console.log(data); setListMedia([...mediaList, data]); }, [mediaList] diff --git a/apps/frontend/src/components/post-url-selector/post.url.selector.tsx b/apps/frontend/src/components/post-url-selector/post.url.selector.tsx new file mode 100644 index 00000000..58843ed8 --- /dev/null +++ b/apps/frontend/src/components/post-url-selector/post.url.selector.tsx @@ -0,0 +1,219 @@ +'use client'; + +import { EventEmitter } from 'events'; +import React, { FC, useCallback, useEffect, useState } from 'react'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import { + executeCommand, + ExecuteState, + ICommand, + selectWord, + TextAreaTextApi, +} from '@uiw/react-md-editor'; +import dayjs from 'dayjs'; +import useSWR from 'swr'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import removeMd from 'remove-markdown'; + +const postUrlEmitter = new EventEmitter(); + +export const ShowPostSelector = () => { + const [showPostSelector, setShowPostSelector] = useState(false); + const [callback, setCallback] = useState<{ + callback: (tag: string) => void; + } | null>({ callback: (tag: string) => {} } as any); + const [date, setDate] = useState(dayjs()); + + useEffect(() => { + postUrlEmitter.on( + 'show', + (params: { date: dayjs.Dayjs; callback: (url: string) => void }) => { + setCallback(params); + setDate(params.date); + setShowPostSelector(true); + } + ); + + return () => { + setShowPostSelector(false); + setCallback(null); + setDate(dayjs()); + postUrlEmitter.removeAllListeners(); + }; + }, []); + + const close = useCallback(() => { + setShowPostSelector(false); + setCallback(null); + setDate(dayjs()); + }, []); + + if (!showPostSelector) { + return <>; + } + + return ( + + ); +}; + +export const showPostSelector = (date: dayjs.Dayjs) => { + return new Promise((resolve) => { + postUrlEmitter.emit('show', { + date, + callback: (tag: string) => { + resolve(tag); + }, + }); + }); +}; + +export const useShowPostSelector = (day: dayjs.Dayjs) => { + return useCallback(() => { + return showPostSelector(day); + }, [day]); +}; + +export const PostSelector: FC<{ + onClose: () => void; + onSelect: (tag: string) => void; + date: dayjs.Dayjs; +}> = (props) => { + const { onClose, onSelect, date } = props; + const fetch = useFetch(); + const fetchOldPosts = useCallback(() => { + return fetch( + '/posts/old?date=' + date.utc().format('YYYY-MM-DDTHH:mm:00'), + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ).then((res) => res.json()); + }, [date]); + + const onCloseWithEmptyString = useCallback(() => { + onSelect(''); + onClose(); + }, []); + + const select = useCallback((id: string) => () => { + onSelect(`(post:${id})`); + onClose(); + }, []); + + const { isLoading, data } = useSWR('old-posts', fetchOldPosts); + + return ( +
+
+
+
+ +
+ +
+
+ {!!data && data.length > 0 && ( +
+ {data.map((p: any) => ( +
+
+
+ + +
+
{p.integration.name}
+
+
{removeMd(p.content)}
+
Status: {p.state}
+
+ ))} +
+ )} +
+
+
+ ); +}; + +export const postSelector = (date: dayjs.Dayjs): ICommand => ({ + name: 'postselector', + keyCommand: 'postselector', + shortcuts: 'ctrlcmd+p', + prefix: '(post:', + suffix: ')', + buttonProps: { + 'aria-label': 'Add Post Url', + title: 'Add Post Url', + }, + icon: ( + + + + ), + execute: async (state: ExecuteState, api: TextAreaTextApi) => { + const newSelectionRange = selectWord({ + text: state.text, + selection: state.selection, + prefix: state.command.prefix!, + suffix: state.command.suffix, + }); + + let state1 = api.setSelectionRange(newSelectionRange); + state1 = api.setSelectionRange(newSelectionRange); + const media = await showPostSelector(date); + executeCommand({ + api, + selectedText: state1.selectedText, + selection: state.selection, + prefix: media, + suffix: '', + }); + }, +}); diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js index 8c3cb6eb..a4b1c8f9 100644 --- a/apps/frontend/tailwind.config.js +++ b/apps/frontend/tailwind.config.js @@ -3,14 +3,16 @@ const { join } = require('path'); module.exports = { content: [ - ...createGlobPatternsForDependencies(__dirname + '../../../libraries/react-shared-libraries'), - join( - __dirname + '../../../libraries/react-shared-libraries', - '{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}' + ...createGlobPatternsForDependencies( + __dirname + '../../../libraries/react-shared-libraries' ), join( - __dirname, - '{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}' + __dirname + '../../../libraries/react-shared-libraries', + '{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}' + ), + join( + __dirname, + '{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}' ), ...createGlobPatternsForDependencies(__dirname), ], @@ -27,25 +29,27 @@ module.exports = { gray: '#8C8C8C', input: '#131B2C', inputText: '#64748B', - tableBorder: '#1F2941' + tableBorder: '#1F2941', }, gridTemplateColumns: { - '13': 'repeat(13, minmax(0, 1fr));' + 13: 'repeat(13, minmax(0, 1fr));', }, backgroundImage: { loginBox: 'url(/auth/login-box.png)', - loginBg: 'url(/auth/bg-login.png)' + loginBg: 'url(/auth/bg-login.png)', }, animation: { fade: 'fadeOut 0.5s ease-in-out', overflow: 'overFlow 0.5s ease-in-out forwards', overflowReverse: 'overFlowReverse 0.5s ease-in-out forwards', + fadeDown: 'fadeDown 4s ease-in-out forwards', }, boxShadow: { - yellow: '0 0 60px 20px #6b6237' + yellow: '0 0 60px 20px #6b6237', + green: '0px 0px 50px rgba(60, 124, 90, 0.3)' }, // that is actual animation - keyframes: theme => ({ + keyframes: (theme) => ({ fadeOut: { '0%': { opacity: 0, transform: 'translateY(30px)' }, '100%': { opacity: 1, transform: 'translateY(0)' }, @@ -60,10 +64,15 @@ module.exports = { '99%': { overflow: 'visible' }, '100%': { overflow: 'hidden' }, }, - }) + fadeDown: { + '0%': { opacity: 0, transform: 'translateY(-30px)' }, + '10%': { opacity: 1, transform: 'translateY(0)' }, + '85%': { opacity: 1, transform: 'translateY(0)' }, + '90%': { opacity: 1, transform: 'translateY(10px)' }, + '100%': { opacity: 0, transform: 'translateY(-30px)' }, + }, + }), }, }, - plugins: [ - require('tailwind-scrollbar') - ], -}; \ No newline at end of file + plugins: [require('tailwind-scrollbar')], +}; diff --git a/libraries/helpers/src/utils/timer.ts b/libraries/helpers/src/utils/timer.ts new file mode 100644 index 00000000..31d05a1a --- /dev/null +++ b/libraries/helpers/src/utils/timer.ts @@ -0,0 +1 @@ +export const timer = (ms: number) => new Promise(res => setTimeout(res, ms)); \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts index dd77663f..3b8ae102 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -1,43 +1,83 @@ -import {PrismaRepository} from "@gitroom/nestjs-libraries/database/prisma/prisma.service"; -import {Injectable} from "@nestjs/common"; +import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service'; +import { Injectable } from '@nestjs/common'; +import dayjs from 'dayjs'; @Injectable() export class IntegrationRepository { - constructor( - private _integration: PrismaRepository<'integration'> - ) { - } + constructor(private _integration: PrismaRepository<'integration'>) {} - createIntegration(org: string, name: string, picture: string, type: 'article' | 'social' , internalId: string, provider: string, token: string, refreshToken = '', expiresIn = 999999999) { - return this._integration.model.integration.create({ - data: { - type: type as any, - name, - providerIdentifier: provider, - token, - picture, - refreshToken, - ...expiresIn ? {tokenExpiration: new Date(Date.now() + expiresIn * 1000)} :{}, - internalId, - organizationId: org, - } - }) - } + createOrUpdateIntegration( + org: string, + name: string, + picture: string, + type: 'article' | 'social', + internalId: string, + provider: string, + token: string, + refreshToken = '', + expiresIn = 999999999 + ) { + return this._integration.model.integration.upsert({ + where: { + organizationId_internalId: { + internalId, + organizationId: org, + }, + }, + create: { + type: type as any, + name, + providerIdentifier: provider, + token, + picture, + refreshToken, + ...(expiresIn + ? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) } + : {}), + internalId, + organizationId: org, + }, + update: { + type: type as any, + name, + providerIdentifier: provider, + token, + picture, + refreshToken, + ...(expiresIn + ? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) } + : {}), + internalId, + organizationId: org, + }, + }); + } - getIntegrationById(org: string, id: string) { - return this._integration.model.integration.findFirst({ - where: { - organizationId: org, - id - } - }); - } + needsToBeRefreshed() { + return this._integration.model.integration.findMany({ + where: { + tokenExpiration: { + lte: dayjs().add(1, 'day').toDate(), + }, + deletedAt: null, + }, + }); + } - getIntegrationsList(org: string) { - return this._integration.model.integration.findMany({ - where: { - organizationId: org - } - }); - } -} \ No newline at end of file + getIntegrationById(org: string, id: string) { + return this._integration.model.integration.findFirst({ + where: { + organizationId: org, + id, + }, + }); + } + + getIntegrationsList(org: string) { + return this._integration.model.integration.findMany({ + where: { + organizationId: org, + }, + }); + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index 8286a5e6..9e1c8bdd 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -1,21 +1,66 @@ -import {Injectable} from "@nestjs/common"; -import {IntegrationRepository} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository"; +import { Injectable } from '@nestjs/common'; +import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository'; +import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; @Injectable() export class IntegrationService { - constructor( - private _integrationRepository: IntegrationRepository, - ) { - } - createIntegration(org: string, name: string, picture: string, type: 'article' | 'social' , internalId: string, provider: string, token: string, refreshToken = '', expiresIn?: number) { - return this._integrationRepository.createIntegration(org, name, picture, type, internalId, provider, token, refreshToken, expiresIn); - } + constructor( + private _integrationRepository: IntegrationRepository, + private _integrationManager: IntegrationManager + ) {} + createOrUpdateIntegration( + org: string, + name: string, + picture: string, + type: 'article' | 'social', + internalId: string, + provider: string, + token: string, + refreshToken = '', + expiresIn?: number + ) { + return this._integrationRepository.createOrUpdateIntegration( + org, + name, + picture, + type, + internalId, + provider, + token, + refreshToken, + expiresIn + ); + } - getIntegrationsList(org: string) { - return this._integrationRepository.getIntegrationsList(org); - } + getIntegrationsList(org: string) { + return this._integrationRepository.getIntegrationsList(org); + } - getIntegrationById(org: string, id: string) { - return this._integrationRepository.getIntegrationById(org, id); + getIntegrationById(org: string, id: string) { + return this._integrationRepository.getIntegrationById(org, id); + } + + async refreshTokens() { + const integrations = await this._integrationRepository.needsToBeRefreshed(); + for (const integration of integrations) { + const provider = this._integrationManager.getSocialIntegration( + integration.providerIdentifier + ); + + const { refreshToken, accessToken, expiresIn } = + await provider.refreshToken(integration.refreshToken!); + + await this.createOrUpdateIntegration( + integration.organizationId, + integration.name, + integration.picture!, + 'social', + integration.internalId, + integration.providerIdentifier, + accessToken, + refreshToken, + expiresIn + ); } + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index bd787658..9a3bdfa5 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -13,6 +13,52 @@ dayjs.extend(isoWeek); export class PostsRepository { constructor(private _post: PrismaRepository<'post'>) {} + getOldPosts(orgId: string, date: string) { + return this._post.model.post.findMany({ + where: { + organizationId: orgId, + publishDate: { + lte: dayjs(date).toDate(), + }, + deletedAt: null, + parentPostId: null, + }, + orderBy: { + publishDate: 'desc', + }, + select: { + id: true, + content: true, + publishDate: true, + releaseURL: true, + state: true, + integration: { + select: { + id: true, + name: true, + providerIdentifier: true, + picture: true, + }, + }, + }, + }); + } + + getPostUrls(orgId: string, ids: string[]) { + return this._post.model.post.findMany({ + where: { + organizationId: orgId, + id: { + in: ids, + }, + }, + select: { + id: true, + releaseURL: true, + }, + }); + } + getPosts(orgId: string, query: GetPostsDto) { const date = dayjs().year(query.year).isoWeek(query.week); @@ -107,7 +153,12 @@ export class PostsRepository { }); } - async createOrUpdatePost(state: 'draft' | 'schedule', orgId: string, date: string, body: PostBody) { + async createOrUpdatePost( + state: 'draft' | 'schedule', + orgId: string, + date: string, + body: PostBody + ) { const posts: Post[] = []; const uuid = uuidv4(); @@ -137,7 +188,7 @@ export class PostsRepository { : {}), content: value.content, group: uuid, - state: state === 'draft' ? 'DRAFT' as const : 'QUEUE' as const, + state: state === 'draft' ? ('DRAFT' as const) : ('QUEUE' as const), image: JSON.stringify(value.image), settings: JSON.stringify(body.settings), organization: { diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index e76fe0d6..494e7835 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -4,7 +4,7 @@ import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post. import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client'; import dayjs from 'dayjs'; import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; -import { Integration, Post } from '@prisma/client'; +import { Integration, Post, Media } from '@prisma/client'; import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto'; type PostWithConditionals = Post & { @@ -55,6 +55,10 @@ export class PostsService { }; } + async getOldPosts(orgId: string, date: string) { + return this._postRepository.getOldPosts(orgId, date); + } + async post(id: string) { const [firstPost, ...morePosts] = await this.getPostsRecursively(id, true); if (!firstPost) { @@ -71,6 +75,25 @@ export class PostsService { return this.postSocial(firstPost.integration!, [firstPost, ...morePosts]); } + private async updateTags(orgId: string, post: Post[]): Promise { + 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[]); + } + private async postSocial(integration: Integration, posts: Post[]) { const getIntegration = this._integrationManager.getSocialIntegration( integration.providerIdentifier @@ -79,13 +102,24 @@ export class PostsService { return; } + const newPosts = await this.updateTags(integration.organizationId, posts); + const publishedPosts = await getIntegration.post( integration.internalId, integration.token, - posts.map((p) => ({ + newPosts.map((p) => ({ id: p.id, message: p.content, settings: JSON.parse(p.settings || '{}'), + media: (JSON.parse(p.image || '[]') as Media[]).map((m) => ({ + url: + process.env.FRONTEND_URL + + '/' + + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY + + m.path, + type: 'image', + path: process.env.UPLOAD_DIRECTORY + m.path, + })), })) ); @@ -105,12 +139,15 @@ export class PostsService { if (!getIntegration) { return; } + + const newPosts = await this.updateTags(integration.organizationId, posts); + const { postId, releaseURL } = await getIntegration.post( integration.token, - posts.map((p) => p.content).join('\n\n'), - JSON.parse(posts[0].settings || '{}') + newPosts.map((p) => p.content).join('\n\n'), + JSON.parse(newPosts[0].settings || '{}') ); - await this._postRepository.updatePost(posts[0].id, postId, releaseURL); + await this._postRepository.updatePost(newPosts[0].id, postId, releaseURL); } async deletePost(orgId: string, group: string) { @@ -135,16 +172,16 @@ export class PostsService { 'post', previousPost ? previousPost : posts?.[0]?.id ); - if (body.type === 'schedule') { - // this._workerServiceProducer.emit('post', { - // id: posts[0].id, - // options: { - // delay: dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'), - // }, - // payload: { - // id: posts[0].id, - // }, - // }); + if (body.type === 'schedule' && dayjs(body.date).isAfter(dayjs())) { + this._workerServiceProducer.emit('post', { + id: posts[0].id, + options: { + delay: 0, //dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'), + }, + payload: { + id: posts[0].id, + }, + }); } } } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 5ea45440..fbd5a97a 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -141,7 +141,12 @@ model Integration { tokenExpiration DateTime? refreshToken String? posts Post[] + deletedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt + @@index([updatedAt]) + @@index([deletedAt]) @@unique([organizationId, internalId]) } diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts index 0a835efb..9cf51fa1 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts @@ -1,32 +1,46 @@ -import {ArrayMaxSize, IsArray, IsDefined, IsOptional, IsString, Matches, MinLength, ValidateNested} from "class-validator"; -import {MediaDto} from "@gitroom/nestjs-libraries/dtos/media/media.dto"; -import {Type} from "class-transformer"; -import {DevToTagsSettings} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.tags.settings"; +import { + ArrayMaxSize, + IsArray, + IsDefined, + IsOptional, + IsString, + Matches, + MinLength, + ValidateIf, + ValidateNested, +} from 'class-validator'; +import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto'; +import { Type } from 'class-transformer'; +import { DevToTagsSettings } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.tags.settings'; export class DevToSettingsDto { - @IsString() - @MinLength(2) - @IsDefined() - title: string; + @IsString() + @MinLength(2) + @IsDefined() + title: string; - @IsOptional() - @ValidateNested() - @Type(() => MediaDto) - main_image?: MediaDto; + @IsOptional() + @ValidateNested() + @Type(() => MediaDto) + main_image?: MediaDto; - @IsOptional() - @IsString() - @Matches(/^(|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/, { - message: 'Invalid URL' - }) - canonical?: string; + @IsOptional() + @IsString() + @ValidateIf((o) => o.canonical && o.canonical.indexOf('(post:') === -1) + @Matches( + /^(|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/, + { + message: 'Invalid URL', + } + ) + canonical?: string; - @IsString() - @IsOptional() - organization?: string; + @IsString() + @IsOptional() + organization?: string; - @IsArray() - @ArrayMaxSize(4) - @IsOptional() - tags: DevToTagsSettings[]; -} \ No newline at end of file + @IsArray() + @ArrayMaxSize(4) + @IsOptional() + tags: DevToTagsSettings[]; +} diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/hashnode.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/hashnode.settings.dto.ts index b6bfbcd7..abf5ae94 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/hashnode.settings.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/hashnode.settings.dto.ts @@ -1,12 +1,5 @@ import { - ArrayMinSize, - IsArray, - IsDefined, - IsOptional, - IsString, - Matches, - MinLength, - ValidateNested, + ArrayMinSize, IsArray, IsDefined, IsOptional, IsString, Matches, MinLength, ValidateIf, ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto'; @@ -37,6 +30,7 @@ export class HashnodeSettingsDto { @IsOptional() @IsString() + @ValidateIf((o) => o.canonical && o.canonical.indexOf('(post:') === -1) @Matches( /^(|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/, { diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/medium.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/medium.settings.dto.ts index 86018772..1982bedb 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/medium.settings.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/medium.settings.dto.ts @@ -1,37 +1,51 @@ -import {ArrayMaxSize, IsArray, IsDefined, IsOptional, IsString, Matches, MinLength, ValidateNested} from "class-validator"; +import { + ArrayMaxSize, + IsArray, + IsDefined, + IsOptional, + IsString, + Matches, + MinLength, + ValidateIf, + ValidateNested, +} from 'class-validator'; export class MediumTagsSettings { - @IsString() - value: string; + @IsString() + value: string; - @IsString() - label: string; + @IsString() + label: string; } export class MediumSettingsDto { - @IsString() - @MinLength(2) - @IsDefined() - title: string; + @IsString() + @MinLength(2) + @IsDefined() + title: string; - @IsString() - @MinLength(2) - @IsDefined() - subtitle: string; + @IsString() + @MinLength(2) + @IsDefined() + subtitle: string; - @IsOptional() - @IsString() - @Matches(/^(|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/, { - message: 'Invalid URL' - }) - canonical?: string; + @IsOptional() + @IsString() + @ValidateIf((o) => o.canonical && o.canonical.indexOf('(post:') === -1) + @Matches( + /^(|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/, + { + message: 'Invalid URL', + } + ) + canonical?: string; - @IsString() - @IsOptional() - publication?: string; + @IsString() + @IsOptional() + publication?: string; - @IsArray() - @ArrayMaxSize(4) - @IsOptional() - tags: MediumTagsSettings[]; -} \ No newline at end of file + @IsArray() + @ArrayMaxSize(4) + @IsOptional() + tags: MediumTagsSettings[]; +} diff --git a/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts b/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts index 0ba8f2d7..d6a07502 100644 --- a/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts @@ -76,7 +76,6 @@ export class DevToProvider implements ArticleProvider { article: { title: settings.title, body_markdown: content, - published: false, main_image: settings?.main_image?.path ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}${settings?.main_image?.path}` : undefined, diff --git a/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts b/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts index 188b0561..b07af7ef 100644 --- a/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts @@ -39,7 +39,7 @@ export class MediumProvider implements ArticleProvider { async post(token: string, content: string, settings: MediumSettingsDto) { const { id } = await this.authenticate(token); - const { data, ...all } = await ( + const { data } = await ( await fetch( settings?.publication ? `https://api.medium.com/v1/publications/${settings?.publication}/posts` @@ -64,7 +64,6 @@ export class MediumProvider implements ArticleProvider { ) ).json(); - console.log(all); return { postId: data.id, releaseURL: data.url, diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index ac5c258a..1237ff66 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -5,6 +5,9 @@ import { SocialProvider, } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { readFileSync } from 'fs'; +import sharp from 'sharp'; +import { lookup } from 'mime-types'; export class LinkedinProvider implements SocialProvider { identifier = 'linkedin'; @@ -110,13 +113,86 @@ export class LinkedinProvider implements SocialProvider { }; } + private async uploadPicture( + accessToken: string, + personId: string, + picture: any + ) { + const { + value: { uploadUrl, image }, + } = await ( + await fetch( + 'https://api.linkedin.com/rest/images?action=initializeUpload', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Restli-Protocol-Version': '2.0.0', + 'LinkedIn-Version': '202402', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + initializeUploadRequest: { + owner: `urn:li:person:${personId}`, + }, + }), + } + ) + ).json(); + + await fetch(uploadUrl, { + method: 'PUT', + headers: { + 'X-Restli-Protocol-Version': '2.0.0', + 'LinkedIn-Version': '202402', + Authorization: `Bearer ${accessToken}`, + }, + body: picture, + }); + + return image; + } + async post( id: string, accessToken: string, postDetails: PostDetails[] ): Promise { const [firstPost, ...restPosts] = postDetails; - console.log('posting'); + + const uploadAll = ( + await Promise.all( + postDetails.flatMap((p) => + p?.media?.flatMap(async (m) => { + return { + id: await this.uploadPicture( + accessToken, + id, + await sharp(readFileSync(m.path), { + animated: lookup(m.path) === 'image/gif', + }) + .resize({ + width: 1000, + }) + .toBuffer() + ), + postId: p.id, + }; + }) + ) + ) + ).reduce((acc, val) => { + if (!val?.id) { + return acc; + } + acc[val.postId] = acc[val.postId] || []; + acc[val.postId].push(val.id); + + return acc; + }, {} as Record); + + const media_ids = (uploadAll[firstPost.id] || []).filter((f) => f); + const data = await fetch('https://api.linkedin.com/v2/posts', { method: 'POST', headers: { @@ -133,29 +209,25 @@ export class LinkedinProvider implements SocialProvider { targetEntities: [], thirdPartyDistributionChannels: [], }, + content: { + ...(media_ids.length === 0 + ? {} + : media_ids.length === 1 + ? { + media: { + id: media_ids[0], + }, + } + : { + multiImage: { + images: media_ids.map((id) => ({ + id, + })), + }, + }), + }, lifecycleState: 'PUBLISHED', isReshareDisabledByAuthor: false, - // content: { - // // contentEntities: [ - // // { - // // entityLocation: 'URL_OF_THE_CONTENT_TO_SHARE', - // // thumbnails: [ - // // { - // // resolvedUrl: 'URL_OF_THE_THUMBNAIL_IMAGE', - // // }, - // // ], - // // }, - // // ], - // title: firstPost.message, - // }, - // distribution: { - // linkedInDistributionTarget: {}, - // }, - // owner: `urn:li:person:${id}`, - // subject: firstPost.message, - // text: { - // text: firstPost.message, - // }, }), }); @@ -169,25 +241,27 @@ export class LinkedinProvider implements SocialProvider { }, ]; for (const post of restPosts) { - const {object} = await (await fetch( - `https://api.linkedin.com/v2/socialActions/${decodeURIComponent( - topPostId - )}/comments`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - actor: `urn:li:person:${id}`, - object: topPostId, - message: { - text: post.message, + const { object } = await ( + await fetch( + `https://api.linkedin.com/v2/socialActions/${decodeURIComponent( + topPostId + )}/comments`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, }, - }), - } - )).json() + body: JSON.stringify({ + actor: `urn:li:person:${id}`, + object: topPostId, + message: { + text: post.message, + }, + }), + } + ) + ).json(); ids.push({ status: 'posted', diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts index 682fafb6..a6a260c5 100644 --- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts @@ -5,6 +5,9 @@ import { SocialProvider, } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { RedditSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto'; +import { timer } from '@gitroom/helpers/utils/timer'; +import { groupBy } from 'lodash'; export class RedditProvider implements SocialProvider { identifier = 'reddit'; @@ -108,24 +111,107 @@ export class RedditProvider implements SocialProvider { async post( id: string, accessToken: string, - postDetails: PostDetails[] + postDetails: PostDetails[] ): Promise { const [post, ...rest] = postDetails; - const response = await fetch('https://oauth.reddit.com/api/submit', { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - title: 'test', - kind: 'self', - text: post.message, - sr: '/r/gitroom', - }), - }); - return []; + const valueArray: PostResponse[] = []; + for (const firstPostSettings of post.settings.subreddit) { + const { + json: { + data: { id, name, url }, + }, + } = await ( + await fetch('https://oauth.reddit.com/api/submit', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + api_type: 'json', + title: firstPostSettings.value.type, + kind: + firstPostSettings.value.type === 'media' + ? 'image' + : firstPostSettings.value.type, + ...(firstPostSettings.value.flair + ? { flair_id: firstPostSettings.value.flair.id } + : {}), + ...(firstPostSettings.value.type === 'link' + ? { + url: firstPostSettings.value.url, + } + : {}), + ...(firstPostSettings.value.type === 'media' + ? { + url: `${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}${firstPostSettings.value.media[0].path}`, + } + : {}), + text: post.message, + sr: firstPostSettings.value.subreddit, + }), + }) + ).json(); + + valueArray.push({ + postId: id, + releaseURL: url, + id: post.id, + status: 'published', + }); + + for (const comment of rest) { + const { + json: { + data: { + things: [ + { + data: { id: commentId, permalink }, + }, + ], + }, + }, + } = await ( + await fetch('https://oauth.reddit.com/api/comment', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + text: comment.message, + thing_id: name, + api_type: 'json', + }), + }) + ).json(); + + // console.log(JSON.stringify(allTop, null, 2), JSON.stringify(allJson, null, 2), JSON.stringify(allData, null, 2)); + + valueArray.push({ + postId: commentId, + releaseURL: 'https://www.reddit.com' + permalink, + id: comment.id, + status: 'published', + }); + + if (rest.length > 1) { + await timer(5000); + } + } + + if (post.settings.subreddit.length > 1) { + await timer(5000); + } + } + + return Object.values(groupBy(valueArray, (p) => p.id)).map((p) => ({ + id: p[0].id, + postId: p.map((p) => p.postId).join(','), + releaseURL: p.map((p) => p.releaseURL).join(','), + status: 'published', + })); } async subreddits(accessToken: string, data: any) { @@ -144,12 +230,16 @@ export class RedditProvider implements SocialProvider { ) ).json(); - console.log(children); - return children.filter(({data} : {data: any}) => data.subreddit_type === "public").map(({ data: { title, url, id } }: any) => ({ - title, - name: url, - id, - })); + return children + .filter( + ({ data }: { data: any }) => + data.subreddit_type === 'public' && data.submission_type !== 'image' + ) + .map(({ data: { title, url, id } }: any) => ({ + title, + name: url, + id, + })); } private getPermissions(submissionType: string, allow_images: string) { @@ -162,9 +252,9 @@ export class RedditProvider implements SocialProvider { permissions.push('link'); } - if (submissionType === "any" || allow_images) { - permissions.push('media'); - } + // if (submissionType === 'any' || allow_images) { + // permissions.push('media'); + // } return permissions; } @@ -182,36 +272,43 @@ export class RedditProvider implements SocialProvider { }) ).json(); - const { - is_flair_required, - } = await ( - await fetch(`https://oauth.reddit.com/api/v1/${data.subreddit.split('/r/')[1]}/post_requirements`, { - method: 'GET', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }) + const { is_flair_required } = await ( + await fetch( + `https://oauth.reddit.com/api/v1/${ + data.subreddit.split('/r/')[1] + }/post_requirements`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ) ).json(); const newData = await ( - await fetch(`https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`, { - method: 'GET', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }) + await fetch( + `https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ) ).json(); return { subreddit: data.subreddit, allow: this.getPermissions(submission_type, allow_images), is_flair_required, - flairs: newData?.map?.((p: any) => ({ - id: p.id, - name: p.text - })) || [] - } + flairs: + newData?.map?.((p: any) => ({ + id: p.id, + name: p.text, + })) || [], + }; } } diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts index aa43b38b..02bb22b3 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -30,10 +30,10 @@ export type PostResponse = { status: string; // Status of the operation or initial post status }; -export type PostDetails = { +export type PostDetails = { id: string; message: string; - settings: object; + settings: T; media?: MediaContent[]; poll?: PollDetails; }; @@ -46,7 +46,7 @@ export type PollDetails = { export type MediaContent = { type: 'image' | 'video'; // Type of the media content url: string; // URL of the media file, if it's already hosted somewhere - file?: File; // The actual media file to upload, if not hosted + path: string; }; export interface SocialProvider extends IAuthenticator, ISocialMediaIntegration { diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 43da34d3..5d7076a3 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -5,6 +5,9 @@ import { PostResponse, SocialProvider, } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; +import { readFileSync } from 'fs'; +import { lookup } from 'mime-types'; +import sharp from 'sharp'; export class XProvider implements SocialProvider { identifier = 'x'; @@ -35,66 +38,125 @@ export class XProvider implements SocialProvider { async generateAuthUrl() { const client = new TwitterApi({ - clientId: process.env.TWITTER_CLIENT_ID!, - clientSecret: process.env.TWITTER_CLIENT_SECRET!, + // clientId: process.env.TWITTER_CLIENT_ID!, + // clientSecret: process.env.TWITTER_CLIENT_SECRET!, + appKey: process.env.X_API_KEY!, + appSecret: process.env.X_API_SECRET!, }); - const { url, codeVerifier, state } = client.generateOAuth2AuthLink( - process.env.FRONTEND_URL + '/integrations/social/x', - { scope: ['tweet.read', 'users.read', 'tweet.write', 'offline.access'] } - ); + const { url, oauth_token, oauth_token_secret } = + await client.generateAuthLink( + process.env.FRONTEND_URL + '/integrations/social/x', + { + authAccessType: 'write', + linkMode: 'authenticate', + forceLogin: false, + } + ); return { url, - codeVerifier, - state, + codeVerifier: oauth_token + ':' + oauth_token_secret, + state: oauth_token, }; } async authenticate(params: { code: string; codeVerifier: string }) { - const startingClient = new TwitterApi({ - clientId: process.env.TWITTER_CLIENT_ID!, - clientSecret: process.env.TWITTER_CLIENT_SECRET!, - }); - const { accessToken, refreshToken, expiresIn, client } = - await startingClient.loginWithOAuth2({ - code: params.code, - codeVerifier: params.codeVerifier, - redirectUri: process.env.FRONTEND_URL + '/integrations/social/x', - }); + const { code, codeVerifier } = params; + const [oauth_token, oauth_token_secret] = codeVerifier.split(':'); - const { - data: { id, name, profile_image_url }, - } = await client.v2.me({ - 'user.fields': 'profile_image_url', + const startingClient = new TwitterApi({ + appKey: process.env.X_API_KEY!, + appSecret: process.env.X_API_SECRET!, + accessToken: oauth_token, + accessSecret: oauth_token_secret, }); + const { accessToken, client, accessSecret } = await startingClient.login( + code + ); + + const { id, name, profile_image_url_https } = await client.currentUser( + true + ); return { - id, - accessToken, + id: String(id), + accessToken: accessToken + ':' + accessSecret, name, - refreshToken, - expiresIn, - picture: profile_image_url, + refreshToken: '', + expiresIn: 999999999, + picture: profile_image_url_https, }; } async post( id: string, accessToken: string, - postDetails: PostDetails[], + postDetails: PostDetails[] ): Promise { - const client = new TwitterApi(accessToken); - const {data: {username}} = await client.v2.me({ - "user.fields": "username" + const [accessTokenSplit, accessSecretSplit] = accessToken.split(':'); + const client = new TwitterApi({ + appKey: process.env.X_API_KEY!, + appSecret: process.env.X_API_SECRET!, + accessToken: accessTokenSplit, + accessSecret: accessSecretSplit, }); - const ids: Array<{postId: string, id: string, releaseURL: string}> = []; + const { + data: { username }, + } = await client.v2.me({ + 'user.fields': 'username', + }); + + // upload everything before, you don't want it to fail between the posts + const uploadAll = ( + await Promise.all( + postDetails.flatMap((p) => + p?.media?.flatMap(async (m) => { + return { + id: await client.v1.uploadMedia( + await sharp(readFileSync(m.path), { + animated: lookup(m.path) === 'image/gif', + }) + .resize({ + width: 1000, + }) + .gif() + .toBuffer(), + { + mimeType: lookup(m.path) || '', + } + ), + postId: p.id, + }; + }) + ) + ) + ).reduce((acc, val) => { + if (!val?.id) { + return acc; + } + + acc[val.postId] = acc[val.postId] || []; + acc[val.postId].push(val.id); + + return acc; + }, {} as Record); + + const ids: Array<{ postId: string; id: string; releaseURL: string }> = []; for (const post of postDetails) { + const media_ids = (uploadAll[post.id] || []).filter((f) => f); + const { data }: { data: { id: string } } = await client.v2.tweet({ text: post.message, + ...(media_ids.length ? { media: { media_ids } } : {}), ...(ids.length ? { reply: { in_reply_to_tweet_id: ids[ids.length - 1].postId } } : {}), }); - ids.push({postId: data.id, id: post.id, releaseURL: `https://twitter.com/${username}/status/${data.id}`}); + + ids.push({ + postId: data.id, + id: post.id, + releaseURL: `https://twitter.com/${username}/status/${data.id}`, + }); } return ids.map((p) => ({ diff --git a/libraries/react-shared-libraries/src/form/canonical.tsx b/libraries/react-shared-libraries/src/form/canonical.tsx new file mode 100644 index 00000000..f98e719e --- /dev/null +++ b/libraries/react-shared-libraries/src/form/canonical.tsx @@ -0,0 +1,79 @@ +'use client'; + +import React, { + DetailedHTMLProps, + FC, + InputHTMLAttributes, + useCallback, + useMemo, +} from 'react'; +import { clsx } from 'clsx'; +import { useFormContext } from 'react-hook-form'; +import dayjs from 'dayjs'; +import { useShowPostSelector } from '../../../../apps/frontend/src/components/post-url-selector/post.url.selector'; + +export const Canonical: FC< + DetailedHTMLProps, HTMLInputElement> & { + error?: any; + date: dayjs.Dayjs; + disableForm?: boolean; + label: string; + name: string; + } +> = (props) => { + const { label, date, className, disableForm, error, ...rest } = props; + const form = useFormContext(); + const err = useMemo(() => { + if (error) return error; + if (!form || !form.formState.errors[props?.name!]) return; + return form?.formState?.errors?.[props?.name!]?.message! as string; + }, [form?.formState?.errors?.[props?.name!]?.message, error]); + + const postSelector = useShowPostSelector(date); + + const onPostSelector = useCallback(async () => { + const id = await postSelector(); + if (disableForm) { + // @ts-ignore + return rest.onChange({ + // @ts-ignore + target: { value: id, name: props.name }, + }); + } + + return form.setValue(props.name, id); + }, [form]); + + return ( +
+
+
{label}
+
+ + + +
+
+ +
{err || <> }
+
+ ); +}; diff --git a/libraries/react-shared-libraries/src/form/input.tsx b/libraries/react-shared-libraries/src/form/input.tsx index 22290f67..1251b12b 100644 --- a/libraries/react-shared-libraries/src/form/input.tsx +++ b/libraries/react-shared-libraries/src/form/input.tsx @@ -1,7 +1,7 @@ "use client"; import {DetailedHTMLProps, FC, InputHTMLAttributes, useMemo} from "react"; -import clsx from "clsx"; +import {clsx} from "clsx"; import {useFormContext} from "react-hook-form"; export const Input: FC, HTMLInputElement> & {error?: any, disableForm?: boolean, label: string, name: string}> = (props) => { diff --git a/libraries/react-shared-libraries/src/toaster/toaster.tsx b/libraries/react-shared-libraries/src/toaster/toaster.tsx new file mode 100644 index 00000000..46e5921e --- /dev/null +++ b/libraries/react-shared-libraries/src/toaster/toaster.tsx @@ -0,0 +1,88 @@ +'use client'; +import { useCallback, useEffect, useState } from 'react'; +import EventEmitter from 'events'; + +const toaster = new EventEmitter(); +export const Toaster = () => { + const [showToaster, setShowToaster] = useState(false); + const [toasterText, setToasterText] = useState(''); + useEffect(() => { + toaster.on('show', (text: string) => { + setToasterText(text); + setShowToaster(true); + setTimeout(() => { + setShowToaster(false); + }, 4200); + }); + return () => { + toaster.removeAllListeners(); + }; + }, []); + + if (!showToaster) { + return <>; + } + + return ( +
+
+ + + +
+
{toasterText}
+ + + + + + + + + + + + +
+ ); +}; + +export const useToaster = () => { + return { + show: useCallback((text: string) => { + toaster.emit('show', text); + }, []), + }; +}; diff --git a/package-lock.json b/package-lock.json index 76ef029f..9b396591 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@types/jsonwebtoken": "^9.0.5", "@types/lodash": "^4.14.202", "@types/md5": "^2.3.5", + "@types/mime-types": "^2.1.4", "@types/remove-markdown": "^0.3.4", "@types/stripe": "^8.0.417", "@uiw/react-md-editor": "^4.0.3", @@ -48,6 +49,7 @@ "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "md5": "^2.3.0", + "mime-types": "^2.1.35", "multer": "^1.4.5-lts.1", "nestjs-command": "^3.1.4", "next": "13.4.4", @@ -66,6 +68,7 @@ "reflect-metadata": "^0.1.13", "remove-markdown": "^0.5.0", "rxjs": "^7.8.0", + "sharp": "^0.33.2", "simple-statistics": "^7.8.3", "stripe": "^14.14.0", "sweetalert2": "^11.10.5", @@ -2400,6 +2403,15 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -3138,6 +3150,437 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz", + "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.1" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz", + "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.1" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=11", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz", + "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=10.13", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz", + "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz", + "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz", + "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz", + "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz", + "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz", + "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz", + "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.1" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz", + "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.1" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz", + "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.1" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz", + "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.1" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz", + "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.1" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz", + "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.1" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz", + "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^0.45.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz", + "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz", + "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -6938,6 +7381,11 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -9753,6 +10201,18 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -9769,6 +10229,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -21186,6 +21655,75 @@ "node": ">=8" } }, + "node_modules/sharp": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", + "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "semver": "^7.5.4" + }, + "engines": { + "libvips": ">=8.15.1", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.2", + "@img/sharp-darwin-x64": "0.33.2", + "@img/sharp-libvips-darwin-arm64": "1.0.1", + "@img/sharp-libvips-darwin-x64": "1.0.1", + "@img/sharp-libvips-linux-arm": "1.0.1", + "@img/sharp-libvips-linux-arm64": "1.0.1", + "@img/sharp-libvips-linux-s390x": "1.0.1", + "@img/sharp-libvips-linux-x64": "1.0.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.1", + "@img/sharp-libvips-linuxmusl-x64": "1.0.1", + "@img/sharp-linux-arm": "0.33.2", + "@img/sharp-linux-arm64": "0.33.2", + "@img/sharp-linux-s390x": "0.33.2", + "@img/sharp-linux-x64": "0.33.2", + "@img/sharp-linuxmusl-arm64": "0.33.2", + "@img/sharp-linuxmusl-x64": "0.33.2", + "@img/sharp-wasm32": "0.33.2", + "@img/sharp-win32-ia32": "0.33.2", + "@img/sharp-win32-x64": "0.33.2" + } + }, + "node_modules/sharp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -21248,6 +21786,19 @@ "node": "*" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", diff --git a/package.json b/package.json index 5361ba85..307ff662 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/jsonwebtoken": "^9.0.5", "@types/lodash": "^4.14.202", "@types/md5": "^2.3.5", + "@types/mime-types": "^2.1.4", "@types/remove-markdown": "^0.3.4", "@types/stripe": "^8.0.417", "@uiw/react-md-editor": "^4.0.3", @@ -48,6 +49,7 @@ "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "md5": "^2.3.0", + "mime-types": "^2.1.35", "multer": "^1.4.5-lts.1", "nestjs-command": "^3.1.4", "next": "13.4.4", @@ -66,6 +68,7 @@ "reflect-metadata": "^0.1.13", "remove-markdown": "^0.5.0", "rxjs": "^7.8.0", + "sharp": "^0.33.2", "simple-statistics": "^7.8.3", "stripe": "^14.14.0", "sweetalert2": "^11.10.5",