From 1e2d45f8867065d50fffa57aa2b55cf6a403a12a Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 31 Jul 2025 20:30:26 +0700 Subject: [PATCH 001/282] feat: mention --- .../src/integrations/social/social.integrations.interface.ts | 3 +++ 1 file changed, 3 insertions(+) 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 f666752b..1c9115e9 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -132,4 +132,7 @@ export interface SocialProvider externalUrl?: ( url: string ) => Promise<{ client_id: string; client_secret: string }>; + mention?: ( + query: string + ) => Promise<{ id: string; name: string; picture: string }[]>; } From 2756f28d72f6259dc02685ac10e35522e33b7e14 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 11:14:11 +0700 Subject: [PATCH 002/282] feat: mention --- apps/frontend/src/app/global.scss | 41 +++ .../src/components/new-launch/editor.tsx | 109 ++++---- .../new-launch/mention.component.tsx | 241 ++++++++++++++++++ .../src/utils/strip.html.validation.ts | 2 +- .../src/integrations/social.abstract.ts | 5 + .../integrations/social/bluesky.provider.ts | 117 ++++++--- .../integrations/social/linkedin.provider.ts | 24 ++ .../social/social.integrations.interface.ts | 4 +- .../src/integrations/social/x.provider.ts | 36 ++- package.json | 5 +- pnpm-lock.yaml | 42 ++- 11 files changed, 536 insertions(+), 90 deletions(-) create mode 100644 apps/frontend/src/components/new-launch/mention.component.tsx diff --git a/apps/frontend/src/app/global.scss b/apps/frontend/src/app/global.scss index 4a047bb9..2227ab94 100644 --- a/apps/frontend/src/app/global.scss +++ b/apps/frontend/src/app/global.scss @@ -626,3 +626,44 @@ html[dir='rtl'] [dir='ltr'] { .mantine-Overlay-root { background: rgba(65, 64, 66, 0.3) !important; } + +.dropdown-menu { + @apply shadow-menu; + background: var(--new-bgColorInner); + border: 1px solid var(--new-bgLineColor); + border-radius: 18px; + display: flex; + flex-direction: column; + overflow: auto; + position: relative; + + button { + align-items: center; + background-color: transparent; + display: flex; + text-align: left; + width: 100%; + padding: 10px; + + &:hover, + &:hover.is-selected { + background-color: var(--new-bgLineColor); + } + } +} + +.tiptap { + :first-child { + margin-top: 0; + } + + .mention { + background-color: var(--purple-light); + border-radius: 0.4rem; + box-decoration-break: clone; + color: #ae8afc; + &::after { + content: '\200B'; + } + } +} diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index e0a02494..f71a0ae9 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -50,6 +50,12 @@ import { BulletList, ListItem } from '@tiptap/extension-list'; import { Bullets } from '@gitroom/frontend/components/new-launch/bullets.component'; import Heading from '@tiptap/extension-heading'; import { HeadingComponent } from '@gitroom/frontend/components/new-launch/heading.component'; +import Mention from '@tiptap/extension-mention'; +import { suggestion } from '@gitroom/frontend/components/new-launch/mention.component'; +import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function'; +import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { useDebouncedCallback } from 'use-debounce'; const InterceptBoldShortcut = Extension.create({ name: 'preventBoldWithUnderline', @@ -79,51 +85,6 @@ const InterceptUnderlineShortcut = Extension.create({ }, }); -const Span = Node.create({ - name: 'mention', - - inline: true, - group: 'inline', - selectable: false, - atom: true, - - addAttributes() { - return { - linkedinId: { - default: null, - }, - label: { - default: '', - }, - }; - }, - - parseHTML() { - return [ - { - tag: 'span[data-linkedin-id]', - }, - ]; - }, - - renderHTML({ HTMLAttributes }) { - return [ - 'span', - mergeAttributes( - // Exclude linkedinId from HTMLAttributes to avoid duplication - Object.fromEntries( - Object.entries(HTMLAttributes).filter(([key]) => key !== 'linkedinId') - ), - { - 'data-linkedin-id': HTMLAttributes.linkedinId, - class: 'mention', - } - ), - `@${HTMLAttributes.label}`, - ]; - }, -}); - export const EditorWrapper: FC<{ totalPosts: number; value: string; @@ -717,6 +678,43 @@ export const OnlyEditor = forwardRef< paste?: (event: ClipboardEvent | File[]) => void; } >(({ editorType, value, onChange, paste }, ref) => { + const fetch = useFetch(); + const { internal } = useLaunchStore( + useShallow((state) => ({ + internal: state.internal.find((p) => p.integration.id === state.current), + })) + ); + + const loadList = useCallback( + async (query: string) => { + if (query.length < 2) { + return []; + } + + if (!internal?.integration.id) { + return []; + } + + try { + const load = await fetch('/integrations/function', { + method: 'POST', + body: JSON.stringify({ + name: 'mention', + id: internal.integration.id, + data: { query }, + }), + }); + + const result = await load.json(); + return result; + } catch (error) { + console.error('Error loading mentions:', error); + return []; + } + }, + [internal, fetch] + ); + const editor = useEditor({ extensions: [ Document, @@ -726,9 +724,28 @@ export const OnlyEditor = forwardRef< Bold, InterceptBoldShortcut, InterceptUnderlineShortcut, - Span, BulletList, ListItem, + ...(internal?.integration?.id + ? [ + Mention.configure({ + HTMLAttributes: { + class: 'mention', + }, + renderHTML({ options, node }) { + return [ + 'span', + mergeAttributes(options.HTMLAttributes, { + 'data-mention-id': node.attrs.id || '', + 'data-mention-label': node.attrs.label || '', + }), + `@${node.attrs.label}`, + ]; + }, + suggestion: suggestion(loadList), + }), + ] + : []), Heading.configure({ levels: [1, 2, 3], }), diff --git a/apps/frontend/src/components/new-launch/mention.component.tsx b/apps/frontend/src/components/new-launch/mention.component.tsx new file mode 100644 index 00000000..90a14a9b --- /dev/null +++ b/apps/frontend/src/components/new-launch/mention.component.tsx @@ -0,0 +1,241 @@ +import React, { FC, useEffect, useImperativeHandle, useState } from 'react'; +import { computePosition, flip, shift } from '@floating-ui/dom'; +import { posToDOMRect, ReactRenderer } from '@tiptap/react'; +import { timer } from '@gitroom/helpers/utils/timer'; + +// Debounce utility for TipTap +const debounce = ( + func: (...args: any[]) => Promise, + wait: number +) => { + let timeout: NodeJS.Timeout; + return (...args: any[]): Promise => { + clearTimeout(timeout); + return new Promise((resolve) => { + timeout = setTimeout(async () => { + try { + const result = await func(...args); + resolve(result); + } catch (error) { + console.error('Debounced function error:', error); + resolve([] as T); + } + }, wait); + }); + }; +}; + +const MentionList: FC = (props: any) => { + const [selectedIndex, setSelectedIndex] = useState(0); + + const selectItem = (index: number) => { + const item = props.items[index]; + + if (item) { + props.command(item); + } + }; + + const upHandler = () => { + setSelectedIndex( + (selectedIndex + props.items.length - 1) % props.items.length + ); + }; + + const downHandler = () => { + setSelectedIndex((selectedIndex + 1) % props.items.length); + }; + + const enterHandler = () => { + selectItem(selectedIndex); + }; + + useEffect(() => setSelectedIndex(0), [props.items]); + + useImperativeHandle(props.ref, () => ({ + onKeyDown: ({ event }: { event: any }) => { + if (event.key === 'ArrowUp') { + upHandler(); + return true; + } + + if (event.key === 'ArrowDown') { + downHandler(); + return true; + } + + if (event.key === 'Enter') { + enterHandler(); + return true; + } + + return false; + }, + })); + + return ( +
+ {props?.items?.none ? ( +
+ We don't have autocomplete for this social media +
+ ) : props?.loading ? ( +
+ Loading... +
+ ) : props?.items ? ( + props.items.map((item: any, index: any) => ( + + )) + ) : ( +
Loading...
+ )} +
+ ); +}; + +const updatePosition = (editor: any, element: any) => { + const virtualElement = { + getBoundingClientRect: () => + posToDOMRect( + editor.view, + editor.state.selection.from, + editor.state.selection.to + ), + }; + + computePosition(virtualElement, element, { + placement: 'bottom-start', + strategy: 'absolute', + middleware: [shift(), flip()], + }).then(({ x, y, strategy }) => { + element.style.width = 'max-content'; + element.style.position = strategy; + element.style.left = `${x}px`; + element.style.top = `${y}px`; + element.style.zIndex = '1000'; + }); +}; + +export const suggestion = ( + loadList: ( + query: string + ) => Promise<{ image: string; label: string; id: string }[]> +) => { + // Create debounced version of loadList once + const debouncedLoadList = debounce(loadList, 500); + let component: any; + + return { + items: async ({ query }: { query: string }) => { + if (!query || query.length < 2) { + return []; + } + + try { + component.updateProps({ loading: true }); + const result = await debouncedLoadList(query); + console.log(result); + return result; + } catch (error) { + console.error('Error in suggestion items:', error); + return []; + } + }, + + render: () => { + let currentQuery = ''; + let isLoadingQuery = false; + + return { + onBeforeStart: (props: any) => { + component = new ReactRenderer(MentionList, { + props: { + ...props, + loading: true, + }, + editor: props.editor, + }); + component.updateProps({ ...props, loading: true }); + updatePosition(props.editor, component.element); + }, + onStart: (props: any) => { + currentQuery = props.query || ''; + isLoadingQuery = currentQuery.length >= 2; + + if (!props.clientRect) { + return; + } + + component.element.style.position = 'absolute'; + component.element.style.zIndex = '1000'; + + const container = + document.querySelector('.mantine-Paper-root') || document.body; + container.appendChild(component.element); + + updatePosition(props.editor, component.element); + component.updateProps({ ...props, loading: true }); + }, + + onUpdate(props: any) { + const newQuery = props.query || ''; + const queryChanged = newQuery !== currentQuery; + currentQuery = newQuery; + + // If query changed and is valid, we're loading until results come in + if (queryChanged && newQuery.length >= 2) { + isLoadingQuery = true; + } + + // If we have results, we're no longer loading + if (props.items && props.items.length > 0) { + isLoadingQuery = false; + } + + // Show loading if we have a valid query but no results yet + const shouldShowLoading = + isLoadingQuery && + newQuery.length >= 2 && + (!props.items || props.items.length === 0); + + component.updateProps({ ...props, loading: false }); + + if (!props.clientRect) { + return; + } + + updatePosition(props.editor, component.element); + }, + + onKeyDown(props: any) { + if (props.event.key === 'Escape') { + component.destroy(); + + return true; + } + + return component.ref?.onKeyDown(props); + }, + + onExit() { + component.element.remove(); + component.destroy(); + }, + }; + }, + }; +}; diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts index 3745c7b3..4b76ab75 100644 --- a/libraries/helpers/src/utils/strip.html.validation.ts +++ b/libraries/helpers/src/utils/strip.html.validation.ts @@ -194,7 +194,7 @@ export const stripHtmlValidation = ( export const convertLinkedinMention = (value: string) => { return value.replace( - /(.+?)<\/span>/gi, + /(.*?)<\/span>/gi, (match, id, name) => { return `@[${name.replace('@', '')}](${id})`; } diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index dc45d39b..a9311d94 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -1,5 +1,6 @@ import { timer } from '@gitroom/helpers/utils/timer'; import { concurrencyService } from '@gitroom/helpers/utils/concurrency.service'; +import { Integration } from '@prisma/client'; export class RefreshToken { constructor( @@ -31,6 +32,10 @@ export abstract class SocialAbstract { return undefined; } + public async mention(token: string, d: { query: string }, id: string, integration: Integration): Promise<{ id: string; label: string; image: string }[] | {none: true}> { + return {none: true}; + } + async runInConcurrent(func: (...args: any[]) => Promise) { const value = await concurrencyService(this.identifier.split('-')[0], async () => { try { diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 026d4970..18d526f0 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -9,13 +9,13 @@ import { RefreshToken, SocialAbstract, } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -import { - BskyAgent, - RichText, +import { + BskyAgent, + RichText, AppBskyEmbedVideo, AppBskyVideoDefs, AtpAgent, - BlobRef + BlobRef, } from '@atproto/api'; import dayjs from 'dayjs'; import { Integration } from '@prisma/client'; @@ -59,16 +59,19 @@ async function reduceImageBySize(url: string, maxSizeKB = 976) { } } -async function uploadVideo(agent: AtpAgent, videoPath: string): Promise { - const { data: serviceAuth } = await agent.com.atproto.server.getServiceAuth( - { - aud: `did:web:${agent.dispatchUrl.host}`, - lxm: "com.atproto.repo.uploadBlob", - exp: Date.now() / 1000 + 60 * 30, // 30 minutes - }, - ); +async function uploadVideo( + agent: AtpAgent, + videoPath: string +): Promise { + const { data: serviceAuth } = await agent.com.atproto.server.getServiceAuth({ + aud: `did:web:${agent.dispatchUrl.host}`, + lxm: 'com.atproto.repo.uploadBlob', + exp: Date.now() / 1000 + 60 * 30, // 30 minutes + }); - async function downloadVideo(url: string): Promise<{ video: Buffer, size: number }> { + async function downloadVideo( + url: string + ): Promise<{ video: Buffer; size: number }> { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch video: ${response.statusText}`); @@ -81,35 +84,37 @@ async function uploadVideo(agent: AtpAgent, videoPath: string): Promise setTimeout(resolve, 1000)); } - - console.log("posting video..."); + + console.log('posting video...'); return { - $type: "app.bsky.embed.video", + $type: 'app.bsky.embed.video', video: blob, } satisfies AppBskyEmbedVideo.Main; } @@ -243,8 +248,10 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { const cidUrl = [] as { cid: string; url: string; rev: string }[]; for (const post of postDetails) { // Separate images and videos - const imageMedia = post.media?.filter((p) => p.path.indexOf('mp4') === -1) || []; - const videoMedia = post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || []; + const imageMedia = + post.media?.filter((p) => p.path.indexOf('mp4') === -1) || []; + const videoMedia = + post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || []; // Upload images const images = await Promise.all( @@ -313,7 +320,11 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { if (postDetails?.[0]?.settings?.active_thread_finisher) { const rt = new RichText({ - text: stripHtmlValidation('normal', postDetails?.[0]?.settings?.thread_finisher, true), + text: stripHtmlValidation( + 'normal', + postDetails?.[0]?.settings?.thread_finisher, + true + ), }); await rt.detectFacets(agent); @@ -487,4 +498,34 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { return true; } + + override async mention( + token: string, + d: { query: string }, + id: string, + integration: Integration + ) { + const agent = new BskyAgent({ + service: 'https://bsky.social', + }); + + const body = JSON.parse( + AuthService.fixedDecryption(integration.customInstanceDetails!) + ); + + await agent.login({ + identifier: body.identifier, + password: body.password, + }); + + const list = await agent.searchActors({ + q: d.query + }); + + return list.data.actors.map(p => ({ + label: p.displayName, + id: p.handle, + image: p.avatar + })) + } } diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 0f479501..cb484979 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -715,4 +715,28 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { }, }); } + + async mention(token: string, data: { query: string }) { + const { elements } = await ( + await fetch( + `https://api.linkedin.com/v2/organizations?q=vanityName&vanityName=${encodeURIComponent( + data.query + )}&projection=(elements*(id,localizedName,logoV2(original~:playableStreams)))`, + { + headers: { + 'X-Restli-Protocol-Version': '2.0.0', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202504', + Authorization: `Bearer ${token}`, + }, + } + ) + ).json(); + + return elements.map((p: any) => ({ + id: String(p.id), + label: p.localizedName, + image: p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier || '', + })); + } } 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 1c9115e9..cd14254f 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -133,6 +133,6 @@ export interface SocialProvider url: string ) => Promise<{ client_id: string; client_secret: string }>; mention?: ( - query: string - ) => Promise<{ id: string; name: string; picture: string }[]>; + token: string, data: { query: string }, id: string, integration: Integration + ) => Promise<{ id: string; label: string; image: string }[] | {none: true}>; } diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 574c648e..36c18669 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -315,7 +315,10 @@ export class XProvider extends SocialAbstract implements SocialProvider { const media_ids = (uploadAll[post.id] || []).filter((f) => f); // @ts-ignore - const { data }: { data: { id: string } } = await this.runInConcurrent( async () => client.v2.tweet({ + const { data }: { data: { id: string } } = await this.runInConcurrent( + async () => + // @ts-ignore + client.v2.tweet({ ...(!postDetails?.[0]?.settings?.who_can_reply_post || postDetails?.[0]?.settings?.who_can_reply_post === 'everyone' ? {} @@ -492,4 +495,35 @@ export class XProvider extends SocialAbstract implements SocialProvider { } return []; } + + override async mention(token: string, d: { query: string }) { + const [accessTokenSplit, accessSecretSplit] = token.split(':'); + const client = new TwitterApi({ + appKey: process.env.X_API_KEY!, + appSecret: process.env.X_API_SECRET!, + accessToken: accessTokenSplit, + accessSecret: accessSecretSplit, + }); + + try { + const data = await client.v2.userByUsername(d.query, { + 'user.fields': ['username', 'name', 'profile_image_url'], + }); + + if (!data?.data?.username) { + return []; + } + + return [ + { + id: data.data.username, + image: data.data.profile_image_url, + label: data.data.name, + }, + ]; + } catch (err) { + console.log(err); + } + return []; + } } diff --git a/package.json b/package.json index 4e9eb4f6..6d046ffe 100644 --- a/package.json +++ b/package.json @@ -86,12 +86,14 @@ "@tiptap/extension-heading": "^3.0.7", "@tiptap/extension-history": "^3.0.7", "@tiptap/extension-list": "^3.0.7", + "@tiptap/extension-mention": "^3.0.7", "@tiptap/extension-paragraph": "^3.0.6", "@tiptap/extension-text": "^3.0.6", "@tiptap/extension-underline": "^3.0.6", "@tiptap/pm": "^3.0.6", "@tiptap/react": "^3.0.6", "@tiptap/starter-kit": "^3.0.6", + "@tiptap/suggestion": "^3.0.7", "@types/bcrypt": "^5.0.2", "@types/concat-stream": "^2.0.3", "@types/facebook-nodejs-business-sdk": "^20.0.2", @@ -207,11 +209,12 @@ "tailwind-scrollbar": "^3.1.0", "tailwindcss": "3.4.17", "tailwindcss-rtl": "^0.9.0", + "tippy.js": "^6.3.7", "tldts": "^6.1.47", "transloadit": "^3.0.2", "tslib": "^2.3.0", "tweetnacl": "^1.0.3", - "twitter-api-v2": "^1.23.2", + "twitter-api-v2": "^1.24.0", "twitter-text": "^3.1.0", "use-debounce": "^10.0.0", "utf-8-validate": "^5.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4fa529f..cc4ed2d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,9 @@ importers: '@tiptap/extension-list': specifier: ^3.0.7 version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) + '@tiptap/extension-mention': + specifier: ^3.0.7 + version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)(@tiptap/suggestion@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)) '@tiptap/extension-paragraph': specifier: ^3.0.6 version: 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6)) @@ -156,6 +159,9 @@ importers: '@tiptap/starter-kit': specifier: ^3.0.6 version: 3.0.6 + '@tiptap/suggestion': + specifier: ^3.0.7 + version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 @@ -501,6 +507,9 @@ importers: tailwindcss-rtl: specifier: ^0.9.0 version: 0.9.0 + tippy.js: + specifier: ^6.3.7 + version: 6.3.7 tldts: specifier: ^6.1.47 version: 6.1.86 @@ -514,7 +523,7 @@ importers: specifier: ^1.0.3 version: 1.0.3 twitter-api-v2: - specifier: ^1.23.2 + specifier: ^1.24.0 version: 1.24.0 twitter-text: specifier: ^3.1.0 @@ -5921,6 +5930,13 @@ packages: '@tiptap/core': ^3.0.7 '@tiptap/pm': ^3.0.7 + '@tiptap/extension-mention@3.0.7': + resolution: {integrity: sha512-PHEx6NdmarjvPPvTd8D9AqK1JIaVYTsnQLxJUERakOLzujgUCToZ7FpMQDhPj97YLvF0t3jeyjZOPmFuj5kw4w==} + peerDependencies: + '@tiptap/core': ^3.0.7 + '@tiptap/pm': ^3.0.7 + '@tiptap/suggestion': ^3.0.7 + '@tiptap/extension-ordered-list@3.0.6': resolution: {integrity: sha512-9SbeGO6kGKoX8GwhaSgpFNCGxlzfGu5otK5DE+Unn5F8/gIYGBJkXTZE1tj8XzPmH6lWhmKJQPudANnW6yuKqg==} peerDependencies: @@ -5966,6 +5982,12 @@ packages: '@tiptap/starter-kit@3.0.6': resolution: {integrity: sha512-7xqcx5hwa+o0J6vpqJRSQNxKHOO6/vSwwicmaHxZ4zdGtlUjJrdreeYaaUpCf0wvpBT1DAQlRnancuD6DJkkPg==} + '@tiptap/suggestion@3.0.7': + resolution: {integrity: sha512-HSMvzAejdvcnVaRZOhXJWAvQqaQs3UYDZaA0ZnzgiJ/sNSbtTyn9XVbX6MfVNYrbtBua4iKaXuJwp6CP0KdHQg==} + peerDependencies: + '@tiptap/core': ^3.0.7 + '@tiptap/pm': ^3.0.7 + '@tokenizer/inflate@0.2.7': resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} engines: {node: '>=18'} @@ -14277,6 +14299,9 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + tlds@1.259.0: resolution: {integrity: sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==} hasBin: true @@ -22336,6 +22361,12 @@ snapshots: '@tiptap/core': 3.0.6(@tiptap/pm@3.0.6) '@tiptap/pm': 3.0.6 + '@tiptap/extension-mention@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)(@tiptap/suggestion@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))': + dependencies: + '@tiptap/core': 3.0.6(@tiptap/pm@3.0.6) + '@tiptap/pm': 3.0.6 + '@tiptap/suggestion': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) + '@tiptap/extension-ordered-list@3.0.6(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))': dependencies: '@tiptap/extension-list': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) @@ -22424,6 +22455,11 @@ snapshots: '@tiptap/extensions': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) '@tiptap/pm': 3.0.6 + '@tiptap/suggestion@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)': + dependencies: + '@tiptap/core': 3.0.6(@tiptap/pm@3.0.6) + '@tiptap/pm': 3.0.6 + '@tokenizer/inflate@0.2.7': dependencies: debug: 4.4.1(supports-color@5.5.0) @@ -33414,6 +33450,10 @@ snapshots: tinyspy@3.0.2: {} + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + tlds@1.259.0: {} tldts-core@6.1.86: {} From 449e2acab1d41606896f3645c74fbcaeb31bbe19 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 12:42:21 +0700 Subject: [PATCH 003/282] feat: mentions --- .../src/api/routes/integrations.controller.ts | 53 ++++++++++++++++++- .../src/components/new-launch/editor.tsx | 22 +------- .../new-launch/mention.component.tsx | 47 +++++++++------- .../src/utils/strip.html.validation.ts | 30 ++++++----- .../integrations/integration.repository.ts | 49 ++++++++++++++++- .../integrations/integration.service.ts | 17 +++++- .../database/prisma/posts/posts.repository.ts | 2 +- .../database/prisma/posts/posts.service.ts | 19 +++++-- .../src/database/prisma/schema.prisma | 12 +++++ .../integrations/social/bluesky.provider.ts | 12 +++-- .../integrations/social/linkedin.provider.ts | 6 ++- .../social/social.integrations.interface.ts | 1 + .../src/integrations/social/x.provider.ts | 4 ++ 13 files changed, 210 insertions(+), 64 deletions(-) diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index cf9a6609..d658fa6e 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -37,6 +37,7 @@ import { AuthorizationActions, Sections, } from '@gitroom/backend/services/auth/permissions/permission.exception.class'; +import { uniqBy } from 'lodash'; @ApiTags('Integrations') @Controller('/integrations') @@ -246,11 +247,59 @@ export class IntegrationsController { ) { return this._integrationService.setTimes(org.id, id, body); } + + @Post('/mentions') + async mentions( + @GetOrgFromRequest() org: Organization, + @Body() body: IntegrationFunctionDto + ) { + const getIntegration = await this._integrationService.getIntegrationById( + org.id, + body.id + ); + if (!getIntegration) { + throw new Error('Invalid integration'); + } + + const list = await this._integrationService.getMentions( + getIntegration.providerIdentifier, + body?.data?.query + ); + + let newList = []; + try { + newList = await this.functionIntegration(org, body); + } catch (err) {} + + if (newList.length) { + await this._integrationService.insertMentions( + getIntegration.providerIdentifier, + newList.map((p: any) => ({ + name: p.label, + username: p.id, + image: p.image, + })) + ); + } + + return uniqBy( + [ + ...list.map((p) => ({ + id: p.username, + image: p.image, + label: p.name, + })), + ...newList, + ], + (p) => p.id + ); + } + @Post('/function') async functionIntegration( @GetOrgFromRequest() org: Organization, @Body() body: IntegrationFunctionDto - ) { + ): Promise { const getIntegration = await this._integrationService.getIntegrationById( org.id, body.id @@ -266,8 +315,10 @@ export class IntegrationsController { throw new Error('Invalid provider'); } + // @ts-ignore if (integrationProvider[body.name]) { try { + // @ts-ignore const load = await integrationProvider[body.name]( getIntegration.token, body.data, diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index f71a0ae9..017d7b4c 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -511,23 +511,6 @@ export const Editor: FC<{ [props.value, id] ); - const addLinkedinTag = useCallback((text: string) => { - const id = text.split('(')[1].split(')')[0]; - const name = text.split('[')[1].split(']')[0]; - - editorRef?.current?.editor - .chain() - .focus() - .insertContent({ - type: 'mention', - attrs: { - linkedinId: id, - label: name, - }, - }) - .run(); - }, []); - return (
@@ -559,9 +542,6 @@ export const Editor: FC<{ > {'\uD83D\uDE00'}
- {identifier === 'linkedin' || identifier === 'linkedin-page' ? ( - - ) : null}
{ }, })); + if (props?.stop) { + return null; + } + return (
{props?.items?.none ? ( @@ -84,22 +88,26 @@ const MentionList: FC = (props: any) => { Loading...
) : props?.items ? ( - props.items.map((item: any, index: any) => ( - - )) + props.items.length === 0 ? ( +
No results found
+ ) : ( + props.items.map((item: any, index: any) => ( + + )) + ) ) : (
Loading...
)} @@ -142,11 +150,12 @@ export const suggestion = ( return { items: async ({ query }: { query: string }) => { if (!query || query.length < 2) { + component.updateProps({ loading: true, stop: true }); return []; } try { - component.updateProps({ loading: true }); + component.updateProps({ loading: true, stop: false }); const result = await debouncedLoadList(query); console.log(result); return result; @@ -169,7 +178,7 @@ export const suggestion = ( }, editor: props.editor, }); - component.updateProps({ ...props, loading: true }); + component.updateProps({ ...props, loading: true, stop: false }); updatePosition(props.editor, component.element); }, onStart: (props: any) => { @@ -212,7 +221,7 @@ export const suggestion = ( newQuery.length >= 2 && (!props.items || props.items.length === 0); - component.updateProps({ ...props, loading: false }); + component.updateProps({ ...props, loading: false, stop: false }); if (!props.clientRect) { return; diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts index 4b76ab75..5ca9d725 100644 --- a/libraries/helpers/src/utils/strip.html.validation.ts +++ b/libraries/helpers/src/utils/strip.html.validation.ts @@ -135,7 +135,8 @@ export const stripHtmlValidation = ( type: 'none' | 'normal' | 'markdown' | 'html', value: string, replaceBold = false, - none = false + none = false, + convertMentionFunction?: (idOrHandle: string, name: string) => string, ): string => { if (type === 'html') { return striptags(value, [ @@ -171,18 +172,16 @@ export const stripHtmlValidation = ( } if (replaceBold) { - const processedHtml = convertLinkedinMention( + const processedHtml = convertMention( convertToAscii( html - .replace(/
    /, "\n
      ") - .replace(/<\/ul>\n/, "
    ") - .replace( - /([.\s\S]*?)<\/li.*?>/gm, - (match, p1) => { + .replace(/
      /, '\n
        ') + .replace(/<\/ul>\n/, '
      ') + .replace(/([.\s\S]*?)<\/li.*?>/gm, (match, p1) => { return `
    • - ${p1.replace(/\n/gm, '')}\n

    • `; - } - ) - ) + }) + ), + convertMentionFunction ); return striptags(processedHtml, ['h1', 'h2', 'h3']); @@ -192,11 +191,18 @@ export const stripHtmlValidation = ( return striptags(html, ['ul', 'li', 'h1', 'h2', 'h3']); }; -export const convertLinkedinMention = (value: string) => { +export const convertMention = ( + value: string, + process?: (idOrHandle: string, name: string) => string +) => { + if (!process) { + return value; + } + return value.replace( /(.*?)<\/span>/gi, (match, id, name) => { - return `@[${name.replace('@', '')}](${id})`; + return `` + process(id, name) + ``; } ); }; 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 c6541cdc..b432f246 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -15,9 +15,56 @@ export class IntegrationRepository { private _posts: PrismaRepository<'post'>, private _plugs: PrismaRepository<'plugs'>, private _exisingPlugData: PrismaRepository<'exisingPlugData'>, - private _customers: PrismaRepository<'customer'> + private _customers: PrismaRepository<'customer'>, + private _mentions: PrismaRepository<'mentions'> ) {} + getMentions(platform: string, q: string) { + return this._mentions.model.mentions.findMany({ + where: { + platform, + OR: [ + { + name: { + contains: q, + mode: 'insensitive', + }, + }, + { + username: { + contains: q, + mode: 'insensitive', + }, + }, + ], + }, + orderBy: { + name: 'asc', + }, + take: 100, + select: { + name: true, + username: true, + image: true, + }, + }); + } + + insertMentions( + platform: string, + mentions: { name: string; username: string; image: string }[] + ) { + return this._mentions.model.mentions.createMany({ + data: mentions.map((mention) => ({ + platform, + name: mention.name, + username: mention.username, + image: mention.image, + })), + skipDuplicates: true, + }); + } + updateProviderSettings(org: string, id: string, settings: string) { return this._integration.model.integration.update({ where: { 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 2d7db625..d7519354 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -46,6 +46,17 @@ export class IntegrationService { return true; } + getMentions(platform: string, q: string) { + return this._integrationRepository.getMentions(platform, q); + } + + insertMentions( + platform: string, + mentions: { name: string; username: string; image: string }[] + ) { + return this._integrationRepository.insertMentions(platform, mentions); + } + async setTimes( orgId: string, integrationId: string, @@ -163,7 +174,11 @@ export class IntegrationService { await this.informAboutRefreshError(orgId, integration); } - async informAboutRefreshError(orgId: string, integration: Integration, err = '') { + async informAboutRefreshError( + orgId: string, + integration: Integration, + err = '' + ) { await this._notificationService.inAppNotification( orgId, `Could not refresh your ${integration.providerIdentifier} channel ${err}`, 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 8d4b2287..f6db7693 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -394,7 +394,7 @@ export class PostsRepository { where: { orgId: orgId, name: { - in: tags.map((tag) => tag.label).filter(f => f), + in: tags.map((tag) => tag.label).filter((f) => f), }, }, }); 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 7bb42cb8..620d54c5 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -378,7 +378,9 @@ export class PostsService { return post; } - const ids = (extract || []).map((e) => e.replace('(post:', '').replace(')', '')); + 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 || ''; @@ -467,7 +469,13 @@ export class PostsService { await Promise.all( (newPosts || []).map(async (p) => ({ id: p.id, - message: stripHtmlValidation(getIntegration.editor, p.content, true), + message: stripHtmlValidation( + getIntegration.editor, + p.content, + true, + false, + getIntegration.mentionFormat + ), settings: JSON.parse(p.settings || '{}'), media: await this.updateMedia( p.id, @@ -535,7 +543,12 @@ export class PostsService { throw err; } - throw new BadBody(integration.providerIdentifier, JSON.stringify(err), {} as any, ''); + throw new BadBody( + integration.providerIdentifier, + JSON.stringify(err), + {} as any, + '' + ); } } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index b63c40a3..6027caa2 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -658,6 +658,18 @@ model Errors { @@index([createdAt]) } +model Mentions { + name String + username String + platform String + image String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@id([name, username, platform, image]) + @@index([createdAt]) +} + enum OrderStatus { PENDING ACCEPTED diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 18d526f0..589103c4 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -519,13 +519,17 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { }); const list = await agent.searchActors({ - q: d.query + q: d.query, }); - return list.data.actors.map(p => ({ + return list.data.actors.map((p) => ({ label: p.displayName, id: p.handle, - image: p.avatar - })) + image: p.avatar, + })); + } + + mentionFormat(idOrHandle: string, name: string) { + return `@${idOrHandle}`; } } diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index cb484979..98c2aba4 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -716,7 +716,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { }); } - async mention(token: string, data: { query: string }) { + override async mention(token: string, data: { query: string }) { const { elements } = await ( await fetch( `https://api.linkedin.com/v2/organizations?q=vanityName&vanityName=${encodeURIComponent( @@ -739,4 +739,8 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { image: p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier || '', })); } + + mentionFormat(idOrHandle: string, name: string) { + return `@[${name.replace('@', '')}](urn:li:organization:${idOrHandle})`; + } } 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 cd14254f..20add654 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -135,4 +135,5 @@ export interface SocialProvider mention?: ( token: string, data: { query: string }, id: string, integration: Integration ) => Promise<{ id: string; label: string; image: string }[] | {none: true}>; + mentionFormat?(idOrHandle: string, name: string): string; } diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 36c18669..87fe9972 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -526,4 +526,8 @@ export class XProvider extends SocialAbstract implements SocialProvider { } return []; } + + mentionFormat(idOrHandle: string, name: string) { + return `@${idOrHandle}`; + } } From 58abf6eb00f861314476e654ab1f8bc5d4a3d748 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 13:45:52 +0700 Subject: [PATCH 004/282] feat: mentions --- .../src/api/routes/integrations.controller.ts | 4 +-- .../integrations/integration.repository.ts | 3 ++ .../integrations/social/threads.provider.ts | 30 +++++++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index d658fa6e..5393f385 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -278,7 +278,7 @@ export class IntegrationsController { name: p.label, username: p.id, image: p.image, - })) + })).filter((f: any) => f.name) ); } @@ -292,7 +292,7 @@ export class IntegrationsController { ...newList, ], (p) => p.id - ); + ).filter(f => f.label && f.image && f.id); } @Post('/function') 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 b432f246..ae576223 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -54,6 +54,9 @@ export class IntegrationRepository { platform: string, mentions: { name: string; username: string; image: string }[] ) { + if (mentions.length === 0) { + return []; + } return this._mentions.model.mentions.createMany({ data: mentions.map((mention) => ({ platform, diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts index 37ed96fa..6ae04186 100644 --- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts @@ -13,6 +13,7 @@ import { capitalize, chunk } from 'lodash'; import { Plug } from '@gitroom/helpers/decorators/plug.decorator'; import { Integration } from '@prisma/client'; import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation'; +import { TwitterApi } from 'twitter-api-v2'; export class ThreadsProvider extends SocialAbstract implements SocialProvider { identifier = 'threads'; @@ -23,6 +24,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { 'threads_content_publish', 'threads_manage_replies', 'threads_manage_insights', + // 'threads_profile_discovery', ]; editor = 'normal' as const; @@ -413,8 +415,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { { id: makeId(10), media: [], - message: - postDetails?.[0]?.settings?.thread_finisher, + message: postDetails?.[0]?.settings?.thread_finisher, settings: {}, }, lastReplyId, @@ -526,4 +527,29 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { return false; } + + // override async mention( + // token: string, + // data: { query: string }, + // id: string, + // integration: Integration + // ) { + // const p = await ( + // await fetch( + // `https://graph.threads.net/v1.0/profile_lookup?username=${data.query}&access_token=${integration.token}` + // ) + // ).json(); + // + // return [ + // { + // id: String(p.id), + // label: p.name, + // image: p.profile_picture_url, + // }, + // ]; + // } + // + // mentionFormat(idOrHandle: string, name: string) { + // return `@${idOrHandle}`; + // } } From f2b96c27e01d289953ffd531672217dc0cb55a9d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 16:32:49 +0700 Subject: [PATCH 005/282] feat: linkedin fix --- .../src/integrations/social/linkedin.provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 98c2aba4..c3cb915b 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -718,7 +718,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { override async mention(token: string, data: { query: string }) { const { elements } = await ( - await fetch( + await this.fetch( `https://api.linkedin.com/v2/organizations?q=vanityName&vanityName=${encodeURIComponent( data.query )}&projection=(elements*(id,localizedName,logoV2(original~:playableStreams)))`, From 428932b328cadd161856f1319a6cfab95232dd91 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 16:33:54 +0700 Subject: [PATCH 006/282] feat: dynamic bluesky --- .../src/integrations/social/bluesky.provider.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 589103c4..83f437e1 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -505,14 +505,14 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { id: string, integration: Integration ) { - const agent = new BskyAgent({ - service: 'https://bsky.social', - }); - const body = JSON.parse( AuthService.fixedDecryption(integration.customInstanceDetails!) ); + const agent = new BskyAgent({ + service: body.service, + }); + await agent.login({ identifier: body.identifier, password: body.password, From f8dd1ae912ddef7d91cd554eee342edf5f124210 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 16:37:27 +0700 Subject: [PATCH 007/282] Feat: small fixes --- apps/frontend/src/components/new-launch/mention.component.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/frontend/src/components/new-launch/mention.component.tsx b/apps/frontend/src/components/new-launch/mention.component.tsx index 1926d899..8de4fb55 100644 --- a/apps/frontend/src/components/new-launch/mention.component.tsx +++ b/apps/frontend/src/components/new-launch/mention.component.tsx @@ -96,7 +96,7 @@ const MentionList: FC = (props: any) => { className={`flex gap-[10px] w-full p-2 text-left rounded hover:bg-gray-100 ${ index === selectedIndex ? 'bg-blue-100' : '' }`} - key={index} + key={item.id || index} onClick={() => selectItem(index)} > Date: Fri, 1 Aug 2025 17:26:47 +0700 Subject: [PATCH 008/282] feat: fix missing image --- apps/backend/src/api/routes/integrations.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index 5393f385..da43888a 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -275,9 +275,9 @@ export class IntegrationsController { await this._integrationService.insertMentions( getIntegration.providerIdentifier, newList.map((p: any) => ({ - name: p.label, - username: p.id, - image: p.image, + name: p.label || '', + username: p.id || '', + image: p.image || '', })).filter((f: any) => f.name) ); } From 0165d73ce9792feb2389a886877f4d2c864af60f Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 17:38:49 +0700 Subject: [PATCH 009/282] feat: prevent command crash --- .../frontend/src/components/new-launch/bold.text.tsx | 6 +++--- .../src/components/new-launch/bullets.component.tsx | 2 +- apps/frontend/src/components/new-launch/editor.tsx | 12 ++++++------ .../src/components/new-launch/heading.component.tsx | 2 +- apps/frontend/src/components/new-launch/u.text.tsx | 6 +++--- apps/frontend/src/components/signature.tsx | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/frontend/src/components/new-launch/bold.text.tsx b/apps/frontend/src/components/new-launch/bold.text.tsx index 5f0f7662..d0478a81 100644 --- a/apps/frontend/src/components/new-launch/bold.text.tsx +++ b/apps/frontend/src/components/new-launch/bold.text.tsx @@ -75,9 +75,9 @@ export const BoldText: FC<{ currentValue: string; }> = ({ editor }) => { const mark = () => { - editor.commands.unsetUnderline(); - editor.commands.toggleBold(); - editor.commands.focus(); + editor?.commands?.unsetUnderline(); + editor?.commands?.toggleBold(); + editor?.commands?.focus(); }; return (
      = ({ editor }) => { const bullet = () => { - editor.commands.toggleBulletList(); + editor?.commands?.toggleBulletList(); }; return (
      { // For example, toggle bold while removing underline - this.editor.commands.unsetUnderline(); - return this.editor.commands.toggleBold(); + this?.editor?.commands?.unsetUnderline(); + return this?.editor?.commands?.toggleBold(); }, }; }, @@ -78,8 +78,8 @@ const InterceptUnderlineShortcut = Extension.create({ return { 'Mod-u': () => { // For example, toggle bold while removing underline - this.editor.commands.unsetBold(); - return this.editor.commands.toggleUnderline(); + this?.editor?.commands?.unsetBold(); + return this?.editor?.commands?.toggleUnderline(); }, }; }, @@ -505,8 +505,8 @@ export const Editor: FC<{ const addText = useCallback( (emoji: string) => { - editorRef?.current?.editor.commands.insertContent(emoji); - editorRef?.current?.editor.commands.focus(); + editorRef?.current?.editor?.commands?.insertContent(emoji); + editorRef?.current?.editor?.commands?.focus(); }, [props.value, id] ); diff --git a/apps/frontend/src/components/new-launch/heading.component.tsx b/apps/frontend/src/components/new-launch/heading.component.tsx index f8338f34..ed117943 100644 --- a/apps/frontend/src/components/new-launch/heading.component.tsx +++ b/apps/frontend/src/components/new-launch/heading.component.tsx @@ -7,7 +7,7 @@ export const HeadingComponent: FC<{ currentValue: string; }> = ({ editor }) => { const setHeading = (level: number) => () => { - editor.commands.toggleHeading({ level }) + editor?.commands?.toggleHeading({ level }) }; return ( diff --git a/apps/frontend/src/components/new-launch/u.text.tsx b/apps/frontend/src/components/new-launch/u.text.tsx index 7c6d2a17..3c0315a0 100644 --- a/apps/frontend/src/components/new-launch/u.text.tsx +++ b/apps/frontend/src/components/new-launch/u.text.tsx @@ -75,9 +75,9 @@ export const UText: FC<{ currentValue: string; }> = ({ editor }) => { const mark = () => { - editor.commands.unsetBold(); - editor.commands.toggleUnderline(); - editor.commands.focus(); + editor?.commands?.unsetBold(); + editor?.commands?.toggleUnderline(); + editor?.commands?.focus(); }; return (
      { - editor?.commands.insertContent("\n\n" + val); - editor?.commands.focus(); + editor?.commands?.insertContent("\n\n" + val); + editor?.commands?.focus(); setShowModal(false); }; return ( From 9724b127c8896458d912230da7178a5eaa44b903 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 17:40:29 +0700 Subject: [PATCH 010/282] feat: prevent crash --- apps/frontend/src/components/media/media.component.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index 12d858f3..4d3e6ed3 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -245,6 +245,10 @@ export const MediaBox: FC<{ const dragAndDrop = useCallback( async (event: ClipboardEvent | File[]) => { + if (!ref?.current?.setOptions) { + return ; + } + // @ts-ignore const clipboardItems = event.map((p) => ({ kind: 'file', From 795fdd6b3fad8d2c391d100e5c67905c3fa0e8db Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 18:42:16 +0700 Subject: [PATCH 011/282] feat: prevent passthrough, because it sends an error --- apps/backend/src/api/routes/auth.controller.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index d75697ea..f576f38f 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -41,7 +41,7 @@ export class AuthController { async register( @Req() req: Request, @Body() body: CreateOrgUserDto, - @Res({ passthrough: true }) response: Response, + @Res({ passthrough: false }) response: Response, @RealIP() ip: string, @UserAgent() userAgent: string ) { @@ -114,7 +114,7 @@ export class AuthController { async login( @Req() req: Request, @Body() body: LoginUserDto, - @Res({ passthrough: true }) response: Response, + @Res({ passthrough: false }) response: Response, @RealIP() ip: string, @UserAgent() userAgent: string ) { @@ -204,11 +204,11 @@ export class AuthController { @Post('/activate') async activate( @Body('code') code: string, - @Res({ passthrough: true }) response: Response + @Res({ passthrough: false }) response: Response ) { const activate = await this._authService.activate(code); if (!activate) { - return response.status(200).send({ can: false }); + return response.status(200).json({ can: false }); } response.cookie('auth', activate, { @@ -228,16 +228,18 @@ export class AuthController { } response.header('onboarding', 'true'); - return response.status(200).send({ can: true }); + + return response.status(200).json({ can: true }); } @Post('/oauth/:provider/exists') async oauthExists( @Body('code') code: string, @Param('provider') provider: string, - @Res({ passthrough: true }) response: Response + @Res({ passthrough: false }) response: Response ) { const { jwt, token } = await this._authService.checkExists(provider, code); + if (token) { return response.json({ token }); } From 641531e8b3baf702d04167e88e9e83d7da529447 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 18:58:42 +0700 Subject: [PATCH 012/282] feat: prevent email with plug --- apps/backend/src/services/auth/auth.service.ts | 5 ++++- apps/frontend/src/components/auth/register.tsx | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/services/auth/auth.service.ts b/apps/backend/src/services/auth/auth.service.ts index de53a531..31e889e6 100644 --- a/apps/backend/src/services/auth/auth.service.ts +++ b/apps/backend/src/services/auth/auth.service.ts @@ -36,10 +36,13 @@ export class AuthService { addToOrg?: boolean | { orgId: string; role: 'USER' | 'ADMIN'; id: string } ) { if (provider === Provider.LOCAL) { + if (process.env.DISALLOW_PLUS && body.email.includes('+')) { + throw new Error('Email with plus sign is not allowed'); + } const user = await this._userService.getUserByEmail(body.email); if (body instanceof CreateOrgUserDto) { if (user) { - throw new Error('User already exists'); + throw new Error('Email already exists'); } if (!(await this.canRegister(provider))) { diff --git a/apps/frontend/src/components/auth/register.tsx b/apps/frontend/src/components/auth/register.tsx index 48e7b5ab..13d925d2 100644 --- a/apps/frontend/src/components/auth/register.tsx +++ b/apps/frontend/src/components/auth/register.tsx @@ -116,7 +116,7 @@ export function RegisterAfter({ ...data, }), }) - .then((response) => { + .then(async (response) => { setLoading(false); if (response.status === 200) { fireEvents('register'); @@ -129,7 +129,7 @@ export function RegisterAfter({ }); } else { form.setError('email', { - message: getHelpfulReasonForRegistrationFailure(response.status), + message: await response.text(), }); } }) From 2146bf626a916e1ec3e994719be5bd42c8dc31fa Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 20:23:12 +0700 Subject: [PATCH 013/282] feat: attempt to fix sentry sourcemaps --- apps/frontend/next.config.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/apps/frontend/next.config.js b/apps/frontend/next.config.js index 2fd3b0f2..98586f80 100644 --- a/apps/frontend/next.config.js +++ b/apps/frontend/next.config.js @@ -8,6 +8,8 @@ const nextConfig = { }, reactStrictMode: false, transpilePackages: ['crypto-hash'], + // Enable production sourcemaps for Sentry + productionBrowserSourceMaps: true, images: { remotePatterns: [ { @@ -42,9 +44,36 @@ const nextConfig = { ]; }, }; + export default withSentryConfig(nextConfig, { org: process.env.SENTRY_ORG, project: process.env.SENTRY_PROJECT, authToken: process.env.SENTRY_AUTH_TOKEN, + + // Sourcemap configuration + sourcemaps: { + disable: false, // Enable sourcemap upload (default: false) + assets: ["**/*.js", "**/*.js.map"], // Files to upload + ignore: ["**/node_modules/**"], // Exclude node_modules + deleteSourcemapsAfterUpload: true, // Delete sourcemaps after upload for security + }, + + // Release configuration (optional but recommended) + release: { + create: true, // Create release in Sentry + finalize: true, // Finalize release after build + }, + + // Additional configuration telemetry: false, + silent: process.env.NODE_ENV === 'production', // Reduce build logs in production + debug: process.env.NODE_ENV === 'development', // Enable debug in development + + // Error handling for CI/CD + errorHandler: (error) => { + console.warn("Sentry build error occurred:", error); + // Don't fail the build if Sentry upload fails + // Remove the next line if you want builds to fail on Sentry errors + return; + }, }); From bff204f03636f408bb7a448f4e9c299727519d54 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 20:44:40 +0700 Subject: [PATCH 014/282] feat: sourcemaps --- apps/frontend/next.config.js | 55 +++++++++++++++++++++++++++--------- apps/frontend/package.json | 1 + 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/apps/frontend/next.config.js b/apps/frontend/next.config.js index 98586f80..c9f57d12 100644 --- a/apps/frontend/next.config.js +++ b/apps/frontend/next.config.js @@ -10,6 +10,17 @@ const nextConfig = { transpilePackages: ['crypto-hash'], // Enable production sourcemaps for Sentry productionBrowserSourceMaps: true, + + // Custom webpack config to ensure sourcemaps are generated properly + webpack: (config, { buildId, dev, isServer, defaultLoaders }) => { + // Enable sourcemaps for both client and server in production + if (!dev) { + config.devtool = isServer ? 'source-map' : 'hidden-source-map'; + } + + return config; + }, + images: { remotePatterns: [ { @@ -50,30 +61,48 @@ export default withSentryConfig(nextConfig, { project: process.env.SENTRY_PROJECT, authToken: process.env.SENTRY_AUTH_TOKEN, - // Sourcemap configuration + // Sourcemap configuration optimized for monorepo sourcemaps: { - disable: false, // Enable sourcemap upload (default: false) - assets: ["**/*.js", "**/*.js.map"], // Files to upload - ignore: ["**/node_modules/**"], // Exclude node_modules - deleteSourcemapsAfterUpload: true, // Delete sourcemaps after upload for security + disable: false, + // More comprehensive asset patterns for monorepo + assets: [ + ".next/static/**/*.js", + ".next/static/**/*.js.map", + ".next/server/**/*.js", + ".next/server/**/*.js.map", + ], + ignore: [ + "**/node_modules/**", + "**/*hot-update*", + "**/_buildManifest.js", + "**/_ssgManifest.js", + "**/*.test.js", + "**/*.spec.js", + ], + deleteSourcemapsAfterUpload: true, }, - // Release configuration (optional but recommended) + // Release configuration release: { - create: true, // Create release in Sentry - finalize: true, // Finalize release after build + create: true, + finalize: true, + // Use git commit hash for releases in monorepo + name: process.env.VERCEL_GIT_COMMIT_SHA || process.env.GITHUB_SHA || undefined, }, + // NextJS specific optimizations for monorepo + widenClientFileUpload: true, + // Additional configuration telemetry: false, - silent: process.env.NODE_ENV === 'production', // Reduce build logs in production - debug: process.env.NODE_ENV === 'development', // Enable debug in development + silent: process.env.NODE_ENV === 'production', + debug: process.env.NODE_ENV === 'development', // Error handling for CI/CD errorHandler: (error) => { - console.warn("Sentry build error occurred:", error); - // Don't fail the build if Sentry upload fails - // Remove the next line if you want builds to fail on Sentry errors + console.warn("Sentry build error occurred:", error.message); + console.warn("This might be due to missing Sentry environment variables or network issues"); + // Don't fail the build if Sentry upload fails in monorepo context return; }, }); diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 219b42c5..85f5f408 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "dotenv -e ../../.env -- next dev -p 4200", "build": "next build", + "build:sentry": "dotenv -e ../../.env -- next build", "start": "dotenv -e ../../.env -- next start -p 4200", "pm2": "pm2 start pnpm --name frontend -- start" }, From dd61e0ebf13bf863d043b0cf27b46d3b949f9aa9 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 22:03:43 +0700 Subject: [PATCH 015/282] feat: telegram formatted text --- .../src/components/new-launch/editor.tsx | 2 +- .../src/utils/strip.html.validation.ts | 5 +- .../integrations/social/telegram.provider.ts | 24 +++++++--- package.json | 3 +- pnpm-lock.yaml | 48 +++++++++---------- 5 files changed, 46 insertions(+), 36 deletions(-) diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 818012d2..333cb308 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -524,7 +524,7 @@ export const Editor: FC<{ editor={editorRef?.current?.editor} currentValue={props.value!} /> - {(editorType === 'markdown' || editorType === 'html') && ( + {(editorType === 'markdown' || editorType === 'html') && identifier !== 'telegram' && ( <> ') === -1 && !none) { diff --git a/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts index ff8ad4c4..22803f2e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts @@ -11,6 +11,7 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab import mime from 'mime'; import TelegramBot from 'node-telegram-bot-api'; import { Integration } from '@prisma/client'; +import striptags from 'striptags'; const telegramBot = new TelegramBot(process.env.TELEGRAM_TOKEN!); // Added to support local storage posting @@ -23,7 +24,7 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider { isBetweenSteps = false; isWeb3 = true; scopes = [] as string[]; - editor = 'markdown' as const; + editor = 'html' as const; async refreshToken(refresh_token: string): Promise { return { @@ -145,7 +146,14 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider { for (const message of postDetails) { let messageId: number | null = null; const mediaFiles = message.media || []; - const text = message.message || ''; + const text = striptags(message.message || '', [ + 'u', + 'strong', + 'p', + ]) + .replace(//g, '') + .replace(/<\/strong>/g, '') + .replace(/

      (.*?)<\/p>/g, '$1\n') // check if media is local to modify url const processedMedia = mediaFiles.map((media) => { let mediaUrl = media.path; @@ -176,7 +184,9 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider { }); // if there's no media, bot sends a text message only if (processedMedia.length === 0) { - const response = await telegramBot.sendMessage(accessToken, text); + const response = await telegramBot.sendMessage(accessToken, text, { + parse_mode: 'HTML', + }); messageId = response.message_id; } // if there's only one media, bot sends the media with the text message as caption @@ -187,20 +197,20 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider { ? await telegramBot.sendVideo( accessToken, media.media, - { caption: text, parse_mode: 'Markdown' }, + { caption: text, parse_mode: 'HTML' }, media.fileOptions ) : media.type === 'photo' ? await telegramBot.sendPhoto( accessToken, media.media, - { caption: text, parse_mode: 'Markdown' }, + { caption: text, parse_mode: 'HTML' }, media.fileOptions ) : await telegramBot.sendDocument( accessToken, media.media, - { caption: text, parse_mode: 'Markdown' }, + { caption: text, parse_mode: 'HTML' }, media.fileOptions ); messageId = response.message_id; @@ -213,7 +223,7 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider { type: m.type === 'document' ? 'document' : m.type, // Documents are not allowed in media groups media: m.media, caption: i === 0 && index === 0 ? text : undefined, - parse_mode: 'Markdown' + parse_mode: 'HTML', })); const response = await telegramBot.sendMediaGroup( diff --git a/package.json b/package.json index 6d046ffe..0af0cab8 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "@types/sha256": "^0.2.2", "@types/stripe": "^8.0.417", "@types/striptags": "^0.0.5", + "@types/turndown": "^5.0.5", "@types/yup": "^0.32.0", "@uidotdev/usehooks": "^2.4.1", "@uiw/react-md-editor": "^4.0.3", @@ -167,7 +168,6 @@ "next": "^14.2.30", "next-plausible": "^3.12.0", "node-fetch": "^3.3.2", - "node-html-markdown": "^1.3.0", "node-telegram-bot-api": "^0.66.0", "nodemailer": "^6.9.15", "nostr-tools": "^2.10.4", @@ -213,6 +213,7 @@ "tldts": "^6.1.47", "transloadit": "^3.0.2", "tslib": "^2.3.0", + "turndown": "^7.2.0", "tweetnacl": "^1.0.3", "twitter-api-v2": "^1.24.0", "twitter-text": "^3.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc4ed2d8..6dea25fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,6 +207,9 @@ importers: '@types/striptags': specifier: ^0.0.5 version: 0.0.5 + '@types/turndown': + specifier: ^5.0.5 + version: 5.0.5 '@types/yup': specifier: ^0.32.0 version: 0.32.0 @@ -381,9 +384,6 @@ importers: node-fetch: specifier: ^3.3.2 version: 3.3.2 - node-html-markdown: - specifier: ^1.3.0 - version: 1.3.0 node-telegram-bot-api: specifier: ^0.66.0 version: 0.66.0(request@2.88.2) @@ -519,6 +519,9 @@ importers: tslib: specifier: ^2.3.0 version: 2.8.1 + turndown: + specifier: ^7.2.0 + version: 7.2.0 tweetnacl: specifier: ^1.0.3 version: 1.0.3 @@ -2998,6 +3001,9 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@modelcontextprotocol/sdk@1.15.0': resolution: {integrity: sha512-67hnl/ROKdb03Vuu0YOr+baKTvf1/5YBHBm9KnZdjdAh8hjt4FRCPD5ucwxGB237sBpzlqQsLy1PFu7z/ekZ9Q==} engines: {node: '>=18'} @@ -6396,6 +6402,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/turndown@5.0.5': + resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -9729,10 +9738,6 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} @@ -11908,13 +11913,6 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-html-markdown@1.3.0: - resolution: {integrity: sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==} - engines: {node: '>=10.0.0'} - - node-html-parser@6.1.13: - resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} - node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -14529,6 +14527,9 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + turndown@7.2.0: + resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==} + tus-js-client@2.3.2: resolution: {integrity: sha512-5a2rm7gp+G7Z+ZB0AO4PzD/dwczB3n1fZeWO5W8AWLJ12RRk1rY4Aeb2VAYX9oKGE+/rGPrdxoFPA/vDSVKnpg==} @@ -18710,6 +18711,8 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@mixmark-io/domino@2.2.0': {} + '@modelcontextprotocol/sdk@1.15.0': dependencies: ajv: 6.12.6 @@ -22970,6 +22973,8 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@types/turndown@5.0.5': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -27597,8 +27602,6 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 - he@1.2.0: {} - header-case@2.0.4: dependencies: capital-case: 1.0.4 @@ -30509,15 +30512,6 @@ snapshots: node-gyp-build@4.8.4: {} - node-html-markdown@1.3.0: - dependencies: - node-html-parser: 6.1.13 - - node-html-parser@6.1.13: - dependencies: - css-select: 5.2.2 - he: 1.2.0 - node-int64@0.4.0: {} node-mock-http@1.0.1: {} @@ -33683,6 +33677,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + turndown@7.2.0: + dependencies: + '@mixmark-io/domino': 2.2.0 + tus-js-client@2.3.2: dependencies: buffer-from: 1.1.2 From d6152022390e87077e7c03950e0e7dcb57484858 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 22:36:36 +0700 Subject: [PATCH 016/282] feat: tiktok validatity causes problems --- .../components/new-launch/providers/tiktok/tiktok.provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/new-launch/providers/tiktok/tiktok.provider.tsx b/apps/frontend/src/components/new-launch/providers/tiktok/tiktok.provider.tsx index f64d8262..03c1fcfd 100644 --- a/apps/frontend/src/components/new-launch/providers/tiktok/tiktok.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/tiktok/tiktok.provider.tsx @@ -148,7 +148,7 @@ const TikTokSettings: FC<{ return (

      - + {/**/} {isTitle && ( )} From 56c56320813455421c202519aa866667dd2dc512 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 1 Aug 2025 18:22:31 +0200 Subject: [PATCH 017/282] testing build selfhosted --- .github/workflows/build.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bc472bae..c214aa41 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,7 +7,8 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: self-hosted + strategy: matrix: From 7ff90b37c506ab70c44d5ec6c3417da64b38d747 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 1 Aug 2025 18:29:32 +0200 Subject: [PATCH 018/282] Update build.yaml --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c214aa41..f4a729b1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: self-hosted + runs-on: 32GB strategy: From 5495bd408ff25583b77482605744dbae9b663974 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 1 Aug 2025 18:30:52 +0200 Subject: [PATCH 019/282] revert last commit --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f4a729b1..41464863 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: 32GB + runs-on: ubuntu-latest strategy: From aa23caa0354f5eaa8717c4b769c2f20dd24e409c Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 1 Aug 2025 22:55:13 +0200 Subject: [PATCH 020/282] feat/allow network requests to be captured --- .../src/sentry/initialize.sentry.client.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libraries/react-shared-libraries/src/sentry/initialize.sentry.client.ts b/libraries/react-shared-libraries/src/sentry/initialize.sentry.client.ts index 70e626d7..39caf6e3 100644 --- a/libraries/react-shared-libraries/src/sentry/initialize.sentry.client.ts +++ b/libraries/react-shared-libraries/src/sentry/initialize.sentry.client.ts @@ -1,6 +1,11 @@ import * as Sentry from '@sentry/nextjs'; import { initializeSentryBasic } from '@gitroom/react/sentry/initialize.sentry.next.basic'; +// Import or define frontendUrl, backendUrl and internalBackendUrl +const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:4200'; +const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3000'; +const internalBackendUrl = process.env.BACKEND_INTERNAL_URL || 'http://localhost:3000'; + export const initializeSentryClient = (environment: string, dsn: string) => initializeSentryBasic(environment, dsn, { integrations: [ @@ -9,6 +14,15 @@ export const initializeSentryClient = (environment: string, dsn: string) => Sentry.replayIntegration({ maskAllText: true, maskAllInputs: true, + + // Allow (Internal) API and Frontend requests to be captured + networkDetailAllowUrls: [ + new RegExp(`^${frontendUrl}(?:/.*)?$`), + new RegExp(`^${backendUrl}(?:/.*)?$`), + new RegExp(`^${internalBackendUrl}(?:/.*)?$`) + ], + networkRequestHeaders: ['X-Custom-Header'], + networkResponseHeaders: ['X-Custom-Header'], }), Sentry.feedbackIntegration({ // Disable the injection of the default widget From a67f6b3918cece042fff0ab11c3856eb7c0d0d2d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 2 Aug 2025 12:05:24 +0700 Subject: [PATCH 021/282] feat: client revert --- .../src/sentry/initialize.sentry.client.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/libraries/react-shared-libraries/src/sentry/initialize.sentry.client.ts b/libraries/react-shared-libraries/src/sentry/initialize.sentry.client.ts index 39caf6e3..70e626d7 100644 --- a/libraries/react-shared-libraries/src/sentry/initialize.sentry.client.ts +++ b/libraries/react-shared-libraries/src/sentry/initialize.sentry.client.ts @@ -1,11 +1,6 @@ import * as Sentry from '@sentry/nextjs'; import { initializeSentryBasic } from '@gitroom/react/sentry/initialize.sentry.next.basic'; -// Import or define frontendUrl, backendUrl and internalBackendUrl -const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:4200'; -const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3000'; -const internalBackendUrl = process.env.BACKEND_INTERNAL_URL || 'http://localhost:3000'; - export const initializeSentryClient = (environment: string, dsn: string) => initializeSentryBasic(environment, dsn, { integrations: [ @@ -14,15 +9,6 @@ export const initializeSentryClient = (environment: string, dsn: string) => Sentry.replayIntegration({ maskAllText: true, maskAllInputs: true, - - // Allow (Internal) API and Frontend requests to be captured - networkDetailAllowUrls: [ - new RegExp(`^${frontendUrl}(?:/.*)?$`), - new RegExp(`^${backendUrl}(?:/.*)?$`), - new RegExp(`^${internalBackendUrl}(?:/.*)?$`) - ], - networkRequestHeaders: ['X-Custom-Header'], - networkResponseHeaders: ['X-Custom-Header'], }), Sentry.feedbackIntegration({ // Disable the injection of the default widget From e46e661f021a32f406c7dc9afe280802310b821d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 2 Aug 2025 12:47:32 +0700 Subject: [PATCH 022/282] feat: fix interval --- apps/frontend/src/components/new-launch/add.edit.modal.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/frontend/src/components/new-launch/add.edit.modal.tsx b/apps/frontend/src/components/new-launch/add.edit.modal.tsx index 7898c823..8498ff4f 100644 --- a/apps/frontend/src/components/new-launch/add.edit.modal.tsx +++ b/apps/frontend/src/components/new-launch/add.edit.modal.tsx @@ -116,6 +116,7 @@ export const AddEditModalInnerInner: FC = (props) => { internal, setTags, setEditor, + setRepeater, } = useLaunchStore( useShallow((state) => ({ reset: state.reset, @@ -126,11 +127,15 @@ export const AddEditModalInnerInner: FC = (props) => { internal: state.internal, setTags: state.setTags, setEditor: state.setEditor, + setRepeater: state.setRepeater })) ); useEffect(() => { if (existingData.integration) { + if (existingData?.posts?.[0]?.intervalInDays) { + setRepeater(existingData.posts[0].intervalInDays); + } setTags( // @ts-ignore existingData?.posts?.[0]?.tags?.map((p: any) => ({ From da77c227c1ac5f9cd292b5154d7fc22847cf57d7 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 2 Aug 2025 14:21:10 +0700 Subject: [PATCH 023/282] feat: error handling that is not an http request --- .../src/integrations/social.abstract.ts | 35 +++++++++++++------ .../src/integrations/social/x.provider.ts | 15 ++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index a9311d94..193c3ee9 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -32,21 +32,30 @@ export abstract class SocialAbstract { return undefined; } - public async mention(token: string, d: { query: string }, id: string, integration: Integration): Promise<{ id: string; label: string; image: string }[] | {none: true}> { - return {none: true}; + public async mention( + token: string, + d: { query: string }, + id: string, + integration: Integration + ): Promise<{ id: string; label: string; image: string }[] | { none: true }> { + return { none: true }; } async runInConcurrent(func: (...args: any[]) => Promise) { - const value = await concurrencyService(this.identifier.split('-')[0], async () => { - try { - return await func(); - } catch (err) { - return {type: 'error', value: err}; + const value = await concurrencyService( + this.identifier.split('-')[0], + async () => { + try { + return await func(); + } catch (err) { + const handle = this.handleErrors(JSON.stringify(err)); + return { err: true, ...(handle || {}) }; + } } - }); + ); - if (value && value.type === 'error') { - throw value.value; + if (value && value?.err && value?.value) { + throw new BadBody('', JSON.stringify({}), {} as any, value.value || ''); } return value; @@ -78,7 +87,11 @@ export abstract class SocialAbstract { json = '{}'; } - if (request.status === 500 || json.includes('rate_limit_exceeded') || json.includes('Rate limit')) { + if ( + request.status === 500 || + json.includes('rate_limit_exceeded') || + json.includes('Rate limit') + ) { await timer(5000); return this.fetch(url, options, identifier, totalRetries + 1); } diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 87fe9972..98e9db98 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -28,6 +28,21 @@ export class XProvider extends SocialAbstract implements SocialProvider { editor = 'normal' as const; + override handleErrors(body: string): + | { + type: 'refresh-token' | 'bad-body'; + value: string; + } + | undefined { + if (body.includes('The Tweet contains an invalid URL.')) { + return { + type: 'bad-body', + value: 'The Tweet contains a URL that is not allowed on X', + }; + } + return undefined; + } + @Plug({ identifier: 'x-autoRepostPost', title: 'Auto Repost Posts', From 69e39b91b365e976bc177c6d85ba740b7837e75c Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 2 Aug 2025 14:26:30 +0700 Subject: [PATCH 024/282] feat: better erroring for X --- .../src/integrations/social/x.provider.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 98e9db98..72d1210a 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -34,6 +34,20 @@ export class XProvider extends SocialAbstract implements SocialProvider { value: string; } | undefined { + if (body.includes('usage-capped')) { + return { + type: 'refresh-token', + value: + 'Posting failed - capped reached. Please try again later', + }; + } + if (body.includes('duplicate-rules')) { + return { + type: 'refresh-token', + value: + 'You have already posted this post, please wait before posting again', + }; + } if (body.includes('The Tweet contains an invalid URL.')) { return { type: 'bad-body', From 94064dfa78fe9fd95af0630a1a77bd2863756a91 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 2 Aug 2025 17:54:26 +0700 Subject: [PATCH 025/282] feat: video made with ai --- .../new-launch/providers/tiktok/tiktok.provider.tsx | 13 +++++++++---- .../src/dtos/posts/providers-settings/tiktok.dto.ts | 10 ++++++++-- .../src/integrations/social/tiktok.provider.ts | 1 + .../src/translation/locales/en/translation.json | 1 + 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/components/new-launch/providers/tiktok/tiktok.provider.tsx b/apps/frontend/src/components/new-launch/providers/tiktok/tiktok.provider.tsx index 03c1fcfd..29c83f10 100644 --- a/apps/frontend/src/components/new-launch/providers/tiktok/tiktok.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/tiktok/tiktok.provider.tsx @@ -149,9 +149,7 @@ const TikTokSettings: FC<{ return (
      {/**/} - {isTitle && ( - - )} + {isTitle && } + {dateMetrics.map((metric) => ( + + ))} + + +
      Current Timezone
      + +
      + ); +}; + +export default MetricComponent; diff --git a/apps/frontend/src/components/settings/settings.component.tsx b/apps/frontend/src/components/settings/settings.component.tsx deleted file mode 100644 index 41312008..00000000 --- a/apps/frontend/src/components/settings/settings.component.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client'; - -import { GithubComponent } from '@gitroom/frontend/components/settings/github.component'; -import { useCallback, useEffect } from 'react'; -import { useUser } from '@gitroom/frontend/components/layout/user.context'; -import { TeamsComponent } from '@gitroom/frontend/components/settings/teams.component'; -import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; -import useSWR from 'swr'; -import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; -import { useRouter } from 'next/navigation'; -import { useVariables } from '@gitroom/react/helpers/variable.context'; -import { useT } from '@gitroom/react/translation/get.transation.service.client'; -export const SettingsComponent = () => { - const { isGeneral } = useVariables(); - const user = useUser(); - const router = useRouter(); - const fetch = useFetch(); - const load = useCallback(async (path: string) => { - const { github } = await (await fetch('/settings/github')).json(); - if (!github) { - return false; - } - const emptyOnes = github.find((p: { login: string }) => !p.login); - const { organizations } = emptyOnes - ? await (await fetch(`/settings/organizations/${emptyOnes.id}`)).json() - : { - organizations: [], - }; - return { - github, - organizations, - }; - }, []); - const { isLoading: isLoadingSettings, data: loadAll } = useSWR( - 'load-all', - load - ); - useEffect(() => { - if (!isLoadingSettings && !loadAll) { - router.push('/'); - } - }, [loadAll, isLoadingSettings]); - - const t = useT(); - - if (isLoadingSettings) { - return ; - } - if (!loadAll) { - return null; - } - return ( -
      - {!isGeneral && ( -
      -

      - {t('your_git_repository', 'Your Git Repository')} -

      -
      - {t( - 'connect_your_github_repository_to_receive_updates_and_analytics', - 'Connect your GitHub repository to receive updates and analytics' - )} -
      - - {/*
      */} - {/*
      */} - {/* */} - {/*
      */} - {/*
      Show news with everybody in Gitroom
      */} - {/*
      */} -
      - )} - {!!user?.tier?.team_members && } -
      - ); -}; diff --git a/apps/frontend/src/components/standalone-modal/standalone.modal.tsx b/apps/frontend/src/components/standalone-modal/standalone.modal.tsx index 35a49da6..fe160dc2 100644 --- a/apps/frontend/src/components/standalone-modal/standalone.modal.tsx +++ b/apps/frontend/src/components/standalone-modal/standalone.modal.tsx @@ -7,6 +7,7 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import dayjs from 'dayjs'; import { useParams } from 'next/navigation'; import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal'; +import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone'; export const StandaloneModal: FC = () => { const fetch = useFetch(); const params = useParams<{ platform: string }>(); @@ -17,7 +18,7 @@ export const StandaloneModal: FC = () => { const loadDate = useCallback(async () => { if (params.platform === 'all') { - return dayjs().utc().format('YYYY-MM-DDTHH:mm:ss'); + return newDayjs().utc().format('YYYY-MM-DDTHH:mm:ss'); } return (await (await fetch('/posts/find-slot')).json()).date; }, []); diff --git a/libraries/react-shared-libraries/src/translation/locales/en/translation.json b/libraries/react-shared-libraries/src/translation/locales/en/translation.json index c3f7d0a9..da85902b 100644 --- a/libraries/react-shared-libraries/src/translation/locales/en/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/en/translation.json @@ -13,6 +13,7 @@ "video_made_with_ai": "Video made with AI", "please_add_at_least": "Please add at least 20 chars", "send_invitation_via_email": "Send invitation via email?", + "global_settings": "Global Settings", "copy_id": "Copy Channel ID", "team_members": "Team Members", "invite_your_assistant_or_team_member_to_manage_your_account": "Invite your assistant or team member to manage your account", diff --git a/package.json b/package.json index ee298420..5bed657f 100644 --- a/package.json +++ b/package.json @@ -212,6 +212,7 @@ "tailwind-scrollbar": "^3.1.0", "tailwindcss": "3.4.17", "tailwindcss-rtl": "^0.9.0", + "timezones-list": "^3.1.0", "tippy.js": "^6.3.7", "tldts": "^6.1.47", "transloadit": "^3.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03a22762..35bd4f27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -516,6 +516,9 @@ importers: tailwindcss-rtl: specifier: ^0.9.0 version: 0.9.0 + timezones-list: + specifier: ^3.1.0 + version: 3.1.0 tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -14263,6 +14266,9 @@ packages: thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + timezones-list@3.1.0: + resolution: {integrity: sha512-PcDBt9tae330KTOIufK/wArTlJp+unuuRcG0EEu+4oLHZACHefKQyP2D51gMZID+urye92mHND60KRVuDDAmbA==} + tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} @@ -33423,6 +33429,8 @@ snapshots: thunky@1.1.0: optional: true + timezones-list@3.1.0: {} + tiny-case@1.0.3: {} tiny-inflate@1.0.3: {} From 1de9a1f605f0c262977529b83df25a0f72c1332d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 20:40:01 +0700 Subject: [PATCH 058/282] feat: remove logs --- .../src/components/launches/general.preview.component.tsx | 2 -- .../nestjs-libraries/src/integrations/social/x.provider.ts | 4 ---- 2 files changed, 6 deletions(-) diff --git a/apps/frontend/src/components/launches/general.preview.component.tsx b/apps/frontend/src/components/launches/general.preview.component.tsx index 5b0a5090..403293c8 100644 --- a/apps/frontend/src/components/launches/general.preview.component.tsx +++ b/apps/frontend/src/components/launches/general.preview.component.tsx @@ -27,7 +27,6 @@ export const GeneralPreviewComponent: FC<{ true ); - console.log('newConetnt', newContent); const { start, end } = textSlicer( integration?.identifier || '', props.maximumCharacters || 10000, @@ -46,7 +45,6 @@ export const GeneralPreviewComponent: FC<{ }) + ``; - console.log(finalValue); return { text: finalValue, images: p.image }; }); diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index a92f2271..c73431e2 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -467,7 +467,6 @@ export class XProvider extends SocialAbstract implements SocialProvider { return []; } - console.log(tweets.map((p) => p.id)); const data = await client.v2.tweets( tweets.map((p) => p.id), { @@ -503,9 +502,6 @@ export class XProvider extends SocialAbstract implements SocialProvider { } ); - console.log(metrics); - console.log(JSON.stringify(data, null, 2)); - return Object.entries(metrics).map(([key, value]) => ({ label: key.replace('_count', '').replace('_', ' ').toUpperCase(), percentageChange: 5, From 326c1b60c6639e144bbc5ba1721c3e16c22691f3 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 20:50:07 +0700 Subject: [PATCH 059/282] feat: fix timezone --- .../src/components/layout/set.timezone.tsx | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/frontend/src/components/layout/set.timezone.tsx b/apps/frontend/src/components/layout/set.timezone.tsx index 0e406ef9..e0a3a604 100644 --- a/apps/frontend/src/components/layout/set.timezone.tsx +++ b/apps/frontend/src/components/layout/set.timezone.tsx @@ -6,22 +6,12 @@ import utc from 'dayjs/plugin/utc'; dayjs.extend(timezone); dayjs.extend(utc); -const {utc: originalUtc} = dayjs; - -dayjs.utc = new Proxy(originalUtc, { - apply(target, thisArg, args) { - const result = target.apply(thisArg, args); - - // Attach `.local()` method to the returned Dayjs object - result.local = function () { - return result.tz(getTimezone()); - }; - - return result; - }, -}); +const { utc: originalUtc } = dayjs; export const getTimezone = () => { + if (typeof window === 'undefined') { + return dayjs.tz.guess(); + } return localStorage.getItem('timezone') || dayjs.tz.guess(); }; @@ -31,6 +21,18 @@ export const newDayjs = (config?: ConfigType) => { const SetTimezone: FC = () => { useEffect(() => { + dayjs.utc = new Proxy(originalUtc, { + apply(target, thisArg, args) { + const result = target.apply(thisArg, args); + + // Attach `.local()` method to the returned Dayjs object + result.local = function () { + return result.tz(getTimezone()); + }; + + return result; + }, + }); if (localStorage.getItem('timezone')) { dayjs.tz.setDefault(getTimezone()); } From 9e8792804556dca4765f1ef727044635cabd42a0 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 21:13:38 +0700 Subject: [PATCH 060/282] feat: fix youtube title --- .../components/new-launch/manage.modal.tsx | 1 + .../youtube.settings.dto.ts | 9 +- .../src/integrations/social.abstract.ts | 1 + .../integrations/social/youtube.provider.ts | 137 +++++++++--------- 4 files changed, 70 insertions(+), 78 deletions(-) diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index f1657155..41dd7e4e 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -142,6 +142,7 @@ export const ManageModal: FC = (props) => { (type: 'draft' | 'now' | 'schedule') => async () => { setLoading(true); const checkAllValid = await ref.current.checkAllValid(); + console.log(checkAllValid); if (type !== 'draft') { const notEnoughChars = checkAllValid.filter((p: any) => { return p.values.some((a: any) => { diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/youtube.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/youtube.settings.dto.ts index 0cf68af4..f2a7045f 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/youtube.settings.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/youtube.settings.dto.ts @@ -1,11 +1,5 @@ import { - IsArray, - IsDefined, - IsIn, - IsOptional, - IsString, - MinLength, - ValidateNested, + IsArray, IsDefined, IsIn, IsOptional, IsString, MaxLength, MinLength, ValidateNested } from 'class-validator'; import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto'; import { Type } from 'class-transformer'; @@ -21,6 +15,7 @@ export class YoutubeTagsSettings { export class YoutubeSettingsDto { @IsString() @MinLength(2) + @MaxLength(100) @IsDefined() title: string; diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 0d46f04b..93cadf14 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -50,6 +50,7 @@ export abstract class SocialAbstract { try { return await func(); } catch (err) { + console.log(err); const handle = this.handleErrors(JSON.stringify(err)); return { err: true, ...(handle || {}) }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts index d8327ba3..a9f0c369 100644 --- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -65,6 +65,55 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { editor = 'normal' as const; + override handleErrors(body: string): + | { + type: 'refresh-token' | 'bad-body'; + value: string; + } + | undefined { + if (body.includes('invalidTitle')) { + return { + type: 'bad-body', + value: + 'We have uploaded your video but we could not set the title. Title is too long.', + }; + } + + if (body.includes('failedPrecondition')) { + return { + type: 'bad-body', + value: + 'We have uploaded your video but we could not set the thumbnail. Thumbnail size is too large.', + }; + } + + if (body.includes('uploadLimitExceeded')) { + return { + type: 'bad-body', + value: + 'You have reached your daily upload limit, please try again tomorrow.', + }; + } + + if (body.includes('youtubeSignupRequired')) { + return { + type: 'bad-body', + value: + 'You have to link your youtube account to your google account first.', + }; + } + + if (body.includes('youtube.thumbnail')) { + return { + type: 'bad-body', + value: + 'Your account is not verified, we have uploaded your video but we could not set the thumbnail. Please verify your account and try again.', + }; + } + + return undefined; + } + async refreshToken(refresh_token: string): Promise { const { client, oauth2 } = clientAndYoutube(); client.setCredentials({ refresh_token }); @@ -153,9 +202,8 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { responseType: 'stream', }); - let all: GaxiosResponse; - try { - all = await this.runInConcurrent(async () => + const all: GaxiosResponse = await this.runInConcurrent( + async () => youtubeClient.videos.insert({ part: ['id', 'snippet', 'status'], notifySubscribers: true, @@ -175,76 +223,23 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { body: response.data, }, }) - ); - } catch (err: any) { - if ( - err.response?.data?.error?.errors?.[0]?.reason === 'failedPrecondition' - ) { - throw new BadBody( - 'youtube', - JSON.stringify(err.response.data), - JSON.stringify(err.response.data), - 'We have uploaded your video but we could not set the thumbnail. Thumbnail size is too large.' - ); - } - if ( - err.response?.data?.error?.errors?.[0]?.reason === 'uploadLimitExceeded' - ) { - throw new BadBody( - 'youtube', - JSON.stringify(err.response.data), - JSON.stringify(err.response.data), - 'You have reached your daily upload limit, please try again tomorrow.' - ); - } - if ( - err.response?.data?.error?.errors?.[0]?.reason === - 'youtubeSignupRequired' - ) { - throw new BadBody( - 'youtube', - JSON.stringify(err.response.data), - JSON.stringify(err.response.data), - 'You have to link your youtube account to your google account first.' - ); - } - - throw new BadBody( - 'youtube', - JSON.stringify(err.response.data), - JSON.stringify(err.response.data), - 'An error occurred while uploading your video, please try again later.' - ); - } + ); if (settings?.thumbnail?.path) { - try { - await this.runInConcurrent(async () => - youtubeClient.thumbnails.set({ - videoId: all?.data?.id!, - media: { - body: ( - await axios({ - url: settings?.thumbnail?.path, - method: 'GET', - responseType: 'stream', - }) - ).data, - }, - }) - ); - } catch (err: any) { - if ( - err.response?.data?.error?.errors?.[0]?.domain === 'youtube.thumbnail' - ) { - throw new BadBody( - '', - JSON.stringify(err.response.data), - JSON.stringify(err.response.data), - 'Your account is not verified, we have uploaded your video but we could not set the thumbnail. Please verify your account and try again.' - ); - } - } + await this.runInConcurrent(async () => + youtubeClient.thumbnails.set({ + videoId: all?.data?.id!, + media: { + body: ( + await axios({ + url: settings?.thumbnail?.path, + method: 'GET', + responseType: 'stream', + }) + ).data, + }, + }) + ); } return [ From d5979ed6c8db684d94f77150f5af48b49a04f63b Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 21:31:15 +0700 Subject: [PATCH 061/282] feat: better performance for timezone --- .../src/components/layout/set.timezone.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/frontend/src/components/layout/set.timezone.tsx b/apps/frontend/src/components/layout/set.timezone.tsx index e0a3a604..1835b96e 100644 --- a/apps/frontend/src/components/layout/set.timezone.tsx +++ b/apps/frontend/src/components/layout/set.timezone.tsx @@ -21,18 +21,16 @@ export const newDayjs = (config?: ConfigType) => { const SetTimezone: FC = () => { useEffect(() => { - dayjs.utc = new Proxy(originalUtc, { - apply(target, thisArg, args) { - const result = target.apply(thisArg, args); + dayjs.utc = (config?: ConfigType, format?: string, strict?: boolean) => { + const result = originalUtc(config, format, strict); - // Attach `.local()` method to the returned Dayjs object - result.local = function () { - return result.tz(getTimezone()); - }; + // Attach `.local()` method to the returned Dayjs object + result.local = function () { + return result.tz(getTimezone()); + }; - return result; - }, - }); + return result; + }; if (localStorage.getItem('timezone')) { dayjs.tz.setDefault(getTimezone()); } From 3837c1ed98e68ee72e913084c9692f441ab01a1a Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 21:56:57 +0700 Subject: [PATCH 062/282] feat: set a higher stalled value, we upload a lot of version and that might cause problems --- libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts index 79a22cb0..71216428 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts @@ -33,6 +33,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy { }); }, { + maxStalledCount: 10, concurrency: 10, connection: ioRedis, removeOnComplete: { From 849becc2819d43b27595d51bf3fb0fd7989134e5 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 22:05:19 +0700 Subject: [PATCH 063/282] feat: post pending --- apps/cron/src/cron.module.ts | 3 +- .../cron/src/tasks/post.now.pending.queues.ts | 41 +++++++++++++++++++ .../database/prisma/posts/posts.repository.ts | 20 ++++++++- .../database/prisma/posts/posts.service.ts | 6 ++- 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 apps/cron/src/tasks/post.now.pending.queues.ts diff --git a/apps/cron/src/cron.module.ts b/apps/cron/src/cron.module.ts index 3a010154..01eb6ed7 100644 --- a/apps/cron/src/cron.module.ts +++ b/apps/cron/src/cron.module.ts @@ -5,6 +5,7 @@ import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bu import { SentryModule } from '@sentry/nestjs/setup'; import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception'; import { CheckMissingQueues } from '@gitroom/cron/tasks/check.missing.queues'; +import { PostNowPendingQueues } from '@gitroom/cron/tasks/post.now.pending.queues'; @Module({ imports: [ @@ -14,6 +15,6 @@ import { CheckMissingQueues } from '@gitroom/cron/tasks/check.missing.queues'; BullMqModule, ], controllers: [], - providers: [FILTER, CheckMissingQueues], + providers: [FILTER, CheckMissingQueues, PostNowPendingQueues], }) export class CronModule {} diff --git a/apps/cron/src/tasks/post.now.pending.queues.ts b/apps/cron/src/tasks/post.now.pending.queues.ts new file mode 100644 index 00000000..3138a25c --- /dev/null +++ b/apps/cron/src/tasks/post.now.pending.queues.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; +import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client'; + +@Injectable() +export class PostNowPendingQueues { + constructor( + private _postService: PostsService, + private _workerServiceProducer: BullMqClient + ) {} + @Cron('*/16 * * * *') + async handleCron() { + const list = await this._postService.checkPending15minutesBack(); + const notExists = ( + await Promise.all( + list.map(async (p) => ({ + id: p.id, + publishDate: p.publishDate, + isJob: + (await this._workerServiceProducer + .getQueue('post') + .getJobState(p.id)) === 'delayed', + })) + ) + ).filter((p) => !p.isJob); + + for (const job of notExists) { + this._workerServiceProducer.emit('post', { + id: job.id, + options: { + delay: 0, + }, + payload: { + id: job.id, + delay: 0, + }, + }); + } + } +} 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 8598b8ac..9508a26c 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -27,12 +27,30 @@ export class PostsRepository { private _errors: PrismaRepository<'errors'> ) {} + checkPending15minutesBack() { + return this._post.model.post.findMany({ + where: { + publishDate: { + lte: dayjs.utc().subtract(15, 'minute').toDate(), + gte: dayjs.utc().subtract(30, 'minute').toDate(), + }, + state: 'QUEUE', + deletedAt: null, + parentPostId: null, + }, + select: { + id: true, + publishDate: true, + }, + }); + } + searchForMissingThreeHoursPosts() { return this._post.model.post.findMany({ where: { publishDate: { gte: dayjs.utc().toDate(), - lt: dayjs.utc().add(3, 'hour').toDate() + lt: dayjs.utc().add(3, 'hour').toDate(), }, state: 'QUEUE', deletedAt: null, 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 06b362dd..fea33271 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -44,7 +44,6 @@ type PostWithConditionals = Post & { childrenPost: Post[]; }; - @Injectable() export class PostsService { private storage = UploadFactory.createStorage(); @@ -62,6 +61,9 @@ export class PostsService { private openaiService: OpenaiService ) {} + checkPending15minutesBack() { + return this._postRepository.checkPending15minutesBack(); + } searchForMissingThreeHoursPosts() { return this._postRepository.searchForMissingThreeHoursPosts(); } @@ -479,7 +481,7 @@ export class PostsService { p.content, true, false, - !(/<\/?[a-z][\s\S]*>/i.test(p.content)), + !/<\/?[a-z][\s\S]*>/i.test(p.content), getIntegration.mentionFormat ), settings: JSON.parse(p.settings || '{}'), From 3197c69f197b641326d629d3571b803c62f4ac76 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 22:43:12 +0700 Subject: [PATCH 064/282] feat: instagram higher concurrency --- libraries/nestjs-libraries/src/integrations/social.abstract.ts | 1 + .../src/integrations/social/instagram.provider.ts | 2 +- .../src/integrations/social/instagram.standalone.provider.ts | 2 +- .../src/integrations/social/pinterest.provider.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 93cadf14..3c3aedfd 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -92,6 +92,7 @@ export abstract class SocialAbstract { } if ( + request.status === 429 || request.status === 500 || json.includes('rate_limit_exceeded') || json.includes('Rate limit') diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 131a0f72..3927c3d1 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -29,7 +29,7 @@ export class InstagramProvider 'instagram_manage_comments', 'instagram_manage_insights', ]; - override maxConcurrentJob = 2; // Instagram/Facebook has moderate rate limits + override maxConcurrentJob = 10; editor = 'normal' as const; async refreshToken(refresh_token: string): Promise { diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts index 24c5e09a..a74ab431 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts @@ -26,7 +26,7 @@ export class InstagramStandaloneProvider 'instagram_business_manage_comments', 'instagram_business_manage_insights', ]; - override maxConcurrentJob = 1; // Instagram standalone has stricter limits + override maxConcurrentJob = 10; // Instagram standalone has stricter limits editor = 'normal' as const; diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index 0c574dc5..8ee14294 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -220,7 +220,7 @@ export class PinterestProvider }) ).json(); - await timer(3000); + await timer(30000); statusCode = mediafile.status; } From 0d231545ecd4789b11abfe96b46ea5e63929ef84 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 23:33:55 +0700 Subject: [PATCH 065/282] feat: promise race for long requests --- .../helpers/src/utils/concurrency.service.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/libraries/helpers/src/utils/concurrency.service.ts b/libraries/helpers/src/utils/concurrency.service.ts index c92b3915..99ee82fa 100644 --- a/libraries/helpers/src/utils/concurrency.service.ts +++ b/libraries/helpers/src/utils/concurrency.service.ts @@ -19,17 +19,19 @@ export const concurrency = async ( maxConcurrent, datastore: 'ioredis', connection, + minTime: 1000, }); let load: T; try { - load = await mapper[strippedIdentifier].schedule( - { expiration: 120_000 }, - async () => { - const res = await func(); - await timer(1000); - return res; - } - ); + load = await mapper[strippedIdentifier].schedule(async () => { + return await Promise.race([ + new Promise(async (res) => { + await timer(300000); + res(true as T); + }), + func(), + ]); + }); } catch (err) {} return load; From 889a031e0e7e578c8c6484fb7504d5975b2293b7 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 23:42:03 +0700 Subject: [PATCH 066/282] feat: add expiration for stuck jobs --- .../helpers/src/utils/concurrency.service.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/libraries/helpers/src/utils/concurrency.service.ts b/libraries/helpers/src/utils/concurrency.service.ts index 99ee82fa..88417d95 100644 --- a/libraries/helpers/src/utils/concurrency.service.ts +++ b/libraries/helpers/src/utils/concurrency.service.ts @@ -23,15 +23,18 @@ export const concurrency = async ( }); let load: T; try { - load = await mapper[strippedIdentifier].schedule(async () => { - return await Promise.race([ - new Promise(async (res) => { - await timer(300000); - res(true as T); - }), - func(), - ]); - }); + load = await mapper[strippedIdentifier].schedule( + { expiration: 600000 }, + async () => { + return await Promise.race([ + new Promise(async (res) => { + await timer(300000); + res(true as T); + }), + func(), + ]); + } + ); } catch (err) {} return load; From 460cef7b32b16e061a9d706d0238ef7771c49778 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 23:42:28 +0700 Subject: [PATCH 067/282] feat: new id --- libraries/helpers/src/utils/concurrency.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/helpers/src/utils/concurrency.service.ts b/libraries/helpers/src/utils/concurrency.service.ts index 88417d95..c23f61e1 100644 --- a/libraries/helpers/src/utils/concurrency.service.ts +++ b/libraries/helpers/src/utils/concurrency.service.ts @@ -15,7 +15,7 @@ export const concurrency = async ( ) => { const strippedIdentifier = identifier.toLowerCase().split('-')[0]; mapper[strippedIdentifier] ??= new Bottleneck({ - id: strippedIdentifier + '-concurrency', + id: strippedIdentifier + '-concurrency-new', maxConcurrent, datastore: 'ioredis', connection, From 780ec93a7678edc4658e2d29eed71e3a03f52922 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 23:44:01 +0700 Subject: [PATCH 068/282] feat: new id --- libraries/nestjs-libraries/src/integrations/social.abstract.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 3c3aedfd..81614f43 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -91,6 +91,7 @@ export abstract class SocialAbstract { json = '{}'; } + if ( request.status === 429 || request.status === 500 || From 8fd88ebb7f9f05975824a26f91d4b676999e6e62 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 7 Aug 2025 23:44:05 +0700 Subject: [PATCH 069/282] feat: new id --- libraries/nestjs-libraries/src/integrations/social.abstract.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 81614f43..3c3aedfd 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -91,7 +91,6 @@ export abstract class SocialAbstract { json = '{}'; } - if ( request.status === 429 || request.status === 500 || From 3d975ff6c3c13dc2ee092a7003dd23a7cd5b8208 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 02:16:27 +0700 Subject: [PATCH 070/282] feat: fix shorlinking --- .../src/short-linking/short.link.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/nestjs-libraries/src/short-linking/short.link.service.ts b/libraries/nestjs-libraries/src/short-linking/short.link.service.ts index 98a09592..3301606c 100644 --- a/libraries/nestjs-libraries/src/short-linking/short.link.service.ts +++ b/libraries/nestjs-libraries/src/short-linking/short.link.service.ts @@ -5,6 +5,7 @@ import { Injectable } from '@nestjs/common'; import { ShortIo } from './providers/short.io'; import { Kutt } from './providers/kutt'; import { LinkDrip } from './providers/linkdrip'; +import { uniq } from 'lodash'; const getProvider = (): ShortLinking => { if (process.env.DUB_TOKEN) { @@ -36,7 +37,7 @@ export class ShortLinkService { } const mergeMessages = messages.join(' '); - const urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/g; + const urlRegex = /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm; const urls = mergeMessages.match(urlRegex); if (!urls) { // No URLs found, return the original text @@ -53,10 +54,10 @@ export class ShortLinkService { return messages; } - const urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/g; + const urlRegex = /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm; return Promise.all( messages.map(async (text) => { - const urls = text.match(urlRegex); + const urls = uniq(text.match(urlRegex)); if (!urls) { // No URLs found, return the original text return text; From 76db0581168339d65106fd28bb08eb5ab5171a51 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 02:22:25 +0700 Subject: [PATCH 071/282] feat: fix bluesky reply cid --- .../src/integrations/social/bluesky.provider.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index df739a5e..3b6df8c9 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -246,6 +246,8 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { let loadCid = ''; let loadUri = ''; + let replyCid = ''; + let replyUri = ''; const cidUrl = [] as { cid: string; url: string; rev: string }[]; for (const post of postDetails) { // Separate images and videos @@ -305,8 +307,8 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { cid: loadCid, }, parent: { - uri: loadUri, - cid: loadCid, + uri: replyUri, + cid: replyCid, }, }, } @@ -315,6 +317,8 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { loadCid = loadCid || cid; loadUri = loadUri || uri; + replyCid = cid; + replyUri = uri; cidUrl.push({ cid, url: uri, rev: commit.rev }); } From aa12f100c8c04f6e810be9e369896b4b56fa3ec7 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 02:22:47 +0700 Subject: [PATCH 072/282] feat: fix bluesky reply cid --- .../src/integrations/social/bluesky.provider.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 3b6df8c9..c90990fb 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -303,13 +303,13 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { ? { reply: { root: { - uri: loadUri, - cid: loadCid, - }, - parent: { uri: replyUri, cid: replyCid, }, + parent: { + uri: loadUri, + cid: loadCid, + }, }, } : {}), From 620439d175f4733d08973d7de5891fb682487947 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 02:51:37 +0700 Subject: [PATCH 073/282] feat: show max chars on main global edit --- .../src/components/new-launch/editor.tsx | 37 +++++++++++++++++-- .../components/new-launch/manage.modal.tsx | 1 - .../providers/high.order.provider.tsx | 28 ++++++++++++-- .../src/components/new-launch/store.ts | 12 +++++- 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 533e1539..84abfeb9 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -10,6 +10,7 @@ import React, { ClipboardEvent, forwardRef, useImperativeHandle, + Fragment, } from 'react'; import clsx from 'clsx'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; @@ -20,7 +21,10 @@ import { BoldText } from '@gitroom/frontend/components/new-launch/bold.text'; import { UText } from '@gitroom/frontend/components/new-launch/u.text'; import { SignatureBox } from '@gitroom/frontend/components/signature'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; -import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; +import { + SelectedIntegrations, + useLaunchStore, +} from '@gitroom/frontend/components/new-launch/store'; import { useShallow } from 'zustand/react/shallow'; import { AddPostButton } from '@gitroom/frontend/components/new-launch/add.post.button'; import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component'; @@ -54,6 +58,7 @@ import Mention from '@tiptap/extension-mention'; import { suggestion } from '@gitroom/frontend/components/new-launch/mention.component'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { AComponent } from '@gitroom/frontend/components/new-launch/a.component'; +import { capitalize } from 'lodash'; const InterceptBoldShortcut = Extension.create({ name: 'preventBoldWithUnderline', @@ -114,6 +119,8 @@ export const EditorWrapper: FC<{ editor, loadedState, setLoadedState, + selectedIntegration, + chars, } = useLaunchStore( useShallow((state) => ({ internal: state.internal.find((p) => p.integration.id === state.current), @@ -142,6 +149,8 @@ export const EditorWrapper: FC<{ editor: state.editor, loadedState: state.loaded, setLoadedState: state.setLoaded, + selectedIntegration: state.selectedIntegrations, + chars: state.chars, })) ); @@ -357,6 +366,8 @@ export const EditorWrapper: FC<{ totalChars={totalChars} appendImages={appendImages(index)} dummy={dummy} + selectedIntegration={selectedIntegration} + chars={chars} />
      @@ -436,7 +447,9 @@ export const Editor: FC<{ validateChars?: boolean; identifier?: string; totalChars?: number; + selectedIntegration: SelectedIntegrations[]; dummy: boolean; + chars: Record; }> = (props) => { const { editorType = 'normal', @@ -444,11 +457,12 @@ export const Editor: FC<{ pictures, setImages, num, - autoComplete, validateChars, identifier, appendImages, + selectedIntegration, dummy, + chars, } = props; const user = useUser(); const [id] = useState(makeId(10)); @@ -637,7 +651,7 @@ export const Editor: FC<{
      - {(props?.totalChars || 0) > 0 && ( + {(props?.totalChars || 0) > 0 ? (
      {valueWithoutHtml.length}/{props.totalChars}
      + ) : ( +
      + {selectedIntegration?.map((p) => ( + +
      chars?.[p.integration.id] && '!text-red-500'}> + {p.integration.name} ({capitalize(p.integration.identifier)}): +
      +
      chars?.[p.integration.id] && '!text-red-500'}> + {valueWithoutHtml.length}/{chars?.[p.integration.id]} +
      +
      + ))} +
      )}
      diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index 41dd7e4e..f1657155 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -142,7 +142,6 @@ export const ManageModal: FC = (props) => { (type: 'draft' | 'now' | 'schedule') => async () => { setLoading(true); const checkAllValid = await ref.current.checkAllValid(); - console.log(checkAllValid); if (type !== 'draft') { const notEnoughChars = checkAllValid.filter((p: any) => { return p.values.some((a: any) => { diff --git a/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx b/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx index 7a4f2f6f..1a02aa41 100644 --- a/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx @@ -89,6 +89,7 @@ export const withProvider = function (params: { setPostComment, setEditor, dummy, + setChars, } = useLaunchStore( useShallow((state) => ({ date: state.date, @@ -106,6 +107,7 @@ export const withProvider = function (params: { setTotalChars: state.setTotalChars, setPostComment: state.setPostComment, setEditor: state.setEditor, + setChars: state.setChars, selectedIntegration: state.selectedIntegrations.find( (p) => p.integration.id === props.id ), @@ -117,6 +119,17 @@ export const withProvider = function (params: { return; } + setChars( + props.id, + typeof maximumCharacters === 'number' + ? maximumCharacters + : maximumCharacters( + JSON.parse( + selectedIntegration.integration.additionalSettings || '[]' + ) + ) + ); + if (isGlobal) { setPostComment(PostComment.ALL); setTotalChars(0); @@ -245,7 +258,12 @@ export const withProvider = function (params: {
      setTab(0)} - className={clsx("cursor-pointer rounded-[4px] flex-1 overflow-hidden whitespace-nowrap text-center pt-[6px] pb-[5px]", tab !== 0 && !!SettingsComponent ? '' : 'text-textItemFocused bg-boxFocused')} + className={clsx( + 'cursor-pointer rounded-[4px] flex-1 overflow-hidden whitespace-nowrap text-center pt-[6px] pb-[5px]', + tab !== 0 && !!SettingsComponent + ? '' + : 'text-textItemFocused bg-boxFocused' + )} > {t('preview', 'Preview')}
      @@ -255,12 +273,16 @@ export const withProvider = function (params: {
      setTab(1)} - className={clsx("cursor-pointer rounded-[4px] flex-1 overflow-hidden whitespace-nowrap text-center pt-[6px] pb-[5px]", tab !== 1 ? '' : 'text-textItemFocused bg-boxFocused')} + className={clsx( + 'cursor-pointer rounded-[4px] flex-1 overflow-hidden whitespace-nowrap text-center pt-[6px] pb-[5px]', + tab !== 1 ? '' : 'text-textItemFocused bg-boxFocused' + )} > {t('settings', 'Settings')} ( {capitalize( selectedIntegration.integration.identifier.split('-')[0] - )}) + )} + )
      )} diff --git a/apps/frontend/src/components/new-launch/store.ts b/apps/frontend/src/components/new-launch/store.ts index 6d2c2c45..d92d169e 100644 --- a/apps/frontend/src/components/new-launch/store.ts +++ b/apps/frontend/src/components/new-launch/store.ts @@ -19,7 +19,7 @@ interface Internal { integrationValue: Values[]; } -interface SelectedIntegrations { +export interface SelectedIntegrations { settings: any; integration: Integrations; ref?: RefObject; @@ -123,6 +123,8 @@ interface StoreState { setDummy: (dummy: boolean) => void; setEditor: (editor: 'normal' | 'markdown' | 'html') => void; setLoaded?: (loaded: boolean) => void; + setChars: (id: string, chars: number) => void; + chars: Record; } const initialState = { @@ -143,6 +145,7 @@ const initialState = { selectedIntegrations: [] as SelectedIntegrations[], global: [] as Values[], internal: [] as Internal[], + chars: {}, }; export const useLaunchStore = create()((set) => ({ @@ -536,4 +539,11 @@ export const useLaunchStore = create()((set) => ({ set((state) => ({ loaded, })), + setChars: (id: string, chars: number) => + set((state) => ({ + chars: { + ...state.chars, + [id]: chars, + }, + })), })); From 1de661c96f11daa4c7dfa2fccba6320a8bb7a75d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 03:11:47 +0700 Subject: [PATCH 074/282] feat: quick fix --- .../src/components/new-launch/finisher/thread.finisher.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/frontend/src/components/new-launch/finisher/thread.finisher.tsx b/apps/frontend/src/components/new-launch/finisher/thread.finisher.tsx index e959c4c8..b15ec572 100644 --- a/apps/frontend/src/components/new-launch/finisher/thread.finisher.tsx +++ b/apps/frontend/src/components/new-launch/finisher/thread.finisher.tsx @@ -50,6 +50,8 @@ export const ThreadFinisher = () => {
      setValue('thread_finisher', val)} value={value} totalPosts={1} From 9e9ff26444cf7e4818c37c8881cb5d1f94e0a499 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 03:24:55 +0700 Subject: [PATCH 075/282] feat: prevent crashing a schedule --- libraries/helpers/src/utils/concurrency.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/helpers/src/utils/concurrency.service.ts b/libraries/helpers/src/utils/concurrency.service.ts index c23f61e1..bd35f469 100644 --- a/libraries/helpers/src/utils/concurrency.service.ts +++ b/libraries/helpers/src/utils/concurrency.service.ts @@ -31,7 +31,13 @@ export const concurrency = async ( await timer(300000); res(true as T); }), - func(), + new Promise(async (res) => { + try { + return res(await func()); + } catch (err) { + res(err as T); + } + }), ]); } ); From d2146a8ac7616584ecab16a39e35d1c1ffee7734 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 04:05:36 +0700 Subject: [PATCH 076/282] feat: less concurrency --- .../helpers/src/utils/concurrency.service.ts | 16 +++------------- .../src/bull-mq-transport-new/strategy.ts | 2 +- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/libraries/helpers/src/utils/concurrency.service.ts b/libraries/helpers/src/utils/concurrency.service.ts index bd35f469..7d447e87 100644 --- a/libraries/helpers/src/utils/concurrency.service.ts +++ b/libraries/helpers/src/utils/concurrency.service.ts @@ -26,19 +26,9 @@ export const concurrency = async ( load = await mapper[strippedIdentifier].schedule( { expiration: 600000 }, async () => { - return await Promise.race([ - new Promise(async (res) => { - await timer(300000); - res(true as T); - }), - new Promise(async (res) => { - try { - return res(await func()); - } catch (err) { - res(err as T); - } - }), - ]); + try { + return await func(); + } catch (err) {} } ); } catch (err) {} diff --git a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts index 71216428..8cb52190 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts @@ -34,7 +34,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy { }, { maxStalledCount: 10, - concurrency: 10, + concurrency: 5, connection: ioRedis, removeOnComplete: { count: 0, From 2bbbc4728da17d87988a3784ae574f9d481d2b16 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 04:24:43 +0700 Subject: [PATCH 077/282] feat: add comment fix --- .../src/components/new-launch/editor.tsx | 97 +++++++++++-------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 84abfeb9..681589fc 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -368,6 +368,19 @@ export const EditorWrapper: FC<{ dummy={dummy} selectedIntegration={selectedIntegration} chars={chars} + childButton={ + <> + {canEdit ? ( + + ) : ( +
      + )} + + } />
      @@ -417,16 +430,6 @@ export const EditorWrapper: FC<{ )}
      - - {canEdit ? ( - - ) : ( -
      - )}
      ))}
      @@ -450,6 +453,7 @@ export const Editor: FC<{ selectedIntegration: SelectedIntegrations[]; dummy: boolean; chars: Record; + childButton?: React.ReactNode; }> = (props) => { const { editorType = 'normal', @@ -463,6 +467,7 @@ export const Editor: FC<{ selectedIntegration, dummy, chars, + childButton, } = props; const user = useUser(); const [id] = useState(makeId(10)); @@ -524,7 +529,7 @@ export const Editor: FC<{ ); return ( -
      +
      @@ -650,34 +655,48 @@ export const Editor: FC<{
      -
      - {(props?.totalChars || 0) > 0 ? ( -
      props.totalChars && '!text-red-500' - )} - > - {valueWithoutHtml.length}/{props.totalChars} -
      - ) : ( -
      - {selectedIntegration?.map((p) => ( - -
      chars?.[p.integration.id] && '!text-red-500'}> - {p.integration.name} ({capitalize(p.integration.identifier)}): -
      -
      chars?.[p.integration.id] && '!text-red-500'}> - {valueWithoutHtml.length}/{chars?.[p.integration.id]} -
      -
      - ))} -
      - )} +
      +
      {childButton}
      +
      + {(props?.totalChars || 0) > 0 ? ( +
      props.totalChars && '!text-red-500' + )} + > + {valueWithoutHtml.length}/{props.totalChars} +
      + ) : ( +
      + {selectedIntegration?.map((p) => ( + +
      chars?.[p.integration.id] && + '!text-red-500' + } + > + {p.integration.name} ({capitalize(p.integration.identifier)} + ): +
      +
      chars?.[p.integration.id] && + '!text-red-500' + } + > + {valueWithoutHtml.length}/{chars?.[p.integration.id]} +
      +
      + ))} +
      + )} +
      ); From e17261881e3ce65492e59cf8a472822361cf2d13 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 8 Aug 2025 07:55:48 +0200 Subject: [PATCH 078/282] Update initialize.sentry.ts --- libraries/nestjs-libraries/src/sentry/initialize.sentry.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts b/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts index 6c5864be..26c95925 100644 --- a/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts +++ b/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts @@ -26,6 +26,10 @@ export const initializeSentry = (appName: string, allowLogs = false) => { // Add our Profiling integration nodeProfilingIntegration(), Sentry.consoleLoggingIntegration({ levels: ['log', 'error', 'warn'] }), + Sentry.openAIIntegration({ + recordInputs: true, + recordOutputs: true, + )}, ], tracesSampleRate: process.env.NODE_ENV === 'development' ? 1.0 : 0.3, enableLogs: true, From 8c4cebb31a875267823a783e31b3c8a7fa1fc7d6 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 8 Aug 2025 08:00:14 +0200 Subject: [PATCH 079/282] feat/sentry mcp --- libraries/nestjs-libraries/src/mcp/mcp.settings.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/mcp/mcp.settings.ts b/libraries/nestjs-libraries/src/mcp/mcp.settings.ts index 377dd254..6c73d479 100644 --- a/libraries/nestjs-libraries/src/mcp/mcp.settings.ts +++ b/libraries/nestjs-libraries/src/mcp/mcp.settings.ts @@ -5,7 +5,7 @@ import { socialIntegrationList } from '@gitroom/nestjs-libraries/integrations/in export class McpSettings { private _server: McpServer; createServer(organization: string, service: MainMcp) { - this._server = new McpServer( + this._server = Sentry.wrapMcpServerWithSentry(new McpServer( { name: 'Postiz', version: '2.0.0', @@ -17,7 +17,7 @@ export class McpSettings { ', ' )} to schedule you need to have the providerId (you can get it from POSTIZ_PROVIDERS_LIST), user need to specify the schedule date (or now), text, you also can send base64 images and text for the comments. When you get POSTIZ_PROVIDERS_LIST, always display all the options to the user`, } - ); + )); for (const usePrompt of Reflect.getMetadata( 'MCP_PROMPT', From 404d761fabbded97a0f9e1cb166f5973e72c5429 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 8 Aug 2025 08:04:49 +0200 Subject: [PATCH 080/282] fix/sentry init brackets --- libraries/nestjs-libraries/src/sentry/initialize.sentry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts b/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts index 26c95925..4291dbd5 100644 --- a/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts +++ b/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts @@ -29,7 +29,7 @@ export const initializeSentry = (appName: string, allowLogs = false) => { Sentry.openAIIntegration({ recordInputs: true, recordOutputs: true, - )}, + }), ], tracesSampleRate: process.env.NODE_ENV === 'development' ? 1.0 : 0.3, enableLogs: true, From 29806b1dac25b22e8c8e06b9ee8f1854bb676b7d Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 8 Aug 2025 08:11:22 +0200 Subject: [PATCH 081/282] Update mcp.settings.ts --- libraries/nestjs-libraries/src/mcp/mcp.settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/mcp/mcp.settings.ts b/libraries/nestjs-libraries/src/mcp/mcp.settings.ts index 6c73d479..728e9ad4 100644 --- a/libraries/nestjs-libraries/src/mcp/mcp.settings.ts +++ b/libraries/nestjs-libraries/src/mcp/mcp.settings.ts @@ -5,7 +5,7 @@ import { socialIntegrationList } from '@gitroom/nestjs-libraries/integrations/in export class McpSettings { private _server: McpServer; createServer(organization: string, service: MainMcp) { - this._server = Sentry.wrapMcpServerWithSentry(new McpServer( + this._server = new McpServer( { name: 'Postiz', version: '2.0.0', From 118e47cb6ed6da279d48587cd280fdf3e0609d41 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Fri, 8 Aug 2025 08:12:17 +0200 Subject: [PATCH 082/282] Update mcp.settings.ts --- libraries/nestjs-libraries/src/mcp/mcp.settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/mcp/mcp.settings.ts b/libraries/nestjs-libraries/src/mcp/mcp.settings.ts index 728e9ad4..377dd254 100644 --- a/libraries/nestjs-libraries/src/mcp/mcp.settings.ts +++ b/libraries/nestjs-libraries/src/mcp/mcp.settings.ts @@ -17,7 +17,7 @@ export class McpSettings { ', ' )} to schedule you need to have the providerId (you can get it from POSTIZ_PROVIDERS_LIST), user need to specify the schedule date (or now), text, you also can send base64 images and text for the comments. When you get POSTIZ_PROVIDERS_LIST, always display all the options to the user`, } - )); + ); for (const usePrompt of Reflect.getMetadata( 'MCP_PROMPT', From 813420427ffb184c9043b8224f7f452d78ea64df Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 20:49:08 +0700 Subject: [PATCH 083/282] feat: bluesky fix images sizes aspect ratio --- .../src/integrations/social/bluesky.provider.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index c90990fb..621549ad 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -52,7 +52,7 @@ async function reduceImageBySize(url: string, maxSizeKB = 976) { if (width < 10 || height < 10) break; // Prevent overly small dimensions } - return imageBuffer; + return { width, height, buffer: imageBuffer }; } catch (error) { console.error('Error processing image:', error); throw error; @@ -259,9 +259,12 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { // Upload images const images = await Promise.all( imageMedia.map(async (p) => { - return await agent.uploadBlob( - new Blob([await reduceImageBySize(p.path)]) - ); + const { buffer, width, height } = await reduceImageBySize(p.path); + return { + width, + height, + buffer: await agent.uploadBlob(new Blob([buffer])), + }; }) ); @@ -288,7 +291,11 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { $type: 'app.bsky.embed.images', images: images.map((p, index) => ({ alt: imageMedia?.[index]?.alt || '', - image: p.data.blob, + image: p.buffer.data.blob, + aspectRatio: { + width: p.width, + height: p.height, + } })), }; } From 00fd1512c58b5fd6b3d675aee199014d9995c9b2 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 8 Aug 2025 22:02:59 +0700 Subject: [PATCH 084/282] feat: remove concurrency from the long tasks --- .../helpers/src/utils/concurrency.service.ts | 6 +++++- .../src/integrations/social.abstract.ts | 6 ++++-- .../integrations/social/instagram.provider.ts | 12 ++++++++++-- .../integrations/social/pinterest.provider.ts | 17 +++++++++++------ .../src/integrations/social/tiktok.provider.ts | 15 +++++++++++---- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/libraries/helpers/src/utils/concurrency.service.ts b/libraries/helpers/src/utils/concurrency.service.ts index 7d447e87..906e5f52 100644 --- a/libraries/helpers/src/utils/concurrency.service.ts +++ b/libraries/helpers/src/utils/concurrency.service.ts @@ -11,7 +11,8 @@ const mapper = {} as Record; export const concurrency = async ( identifier: string, maxConcurrent = 1, - func: (...args: any[]) => Promise + func: (...args: any[]) => Promise, + ignoreConcurrency = false ) => { const strippedIdentifier = identifier.toLowerCase().split('-')[0]; mapper[strippedIdentifier] ??= new Bottleneck({ @@ -23,6 +24,9 @@ export const concurrency = async ( }); let load: T; try { + if (ignoreConcurrency) { + return await func(); + } load = await mapper[strippedIdentifier].schedule( { expiration: 600000 }, async () => { diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 3c3aedfd..0b4d26b1 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -68,12 +68,14 @@ export abstract class SocialAbstract { url: string, options: RequestInit = {}, identifier = '', - totalRetries = 0 + totalRetries = 0, + ignoreConcurrency = false ): Promise { const request = await concurrency( this.identifier, this.maxConcurrentJob, - () => fetch(url, options) + () => fetch(url, options), + ignoreConcurrency ); if (request.status === 200 || request.status === 201) { diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 3927c3d1..e9bbbae7 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -498,7 +498,11 @@ export class InstagramProvider while (status === 'IN_PROGRESS') { const { status_code } = await ( await this.fetch( - `https://${type}/v20.0/${photoId}?access_token=${accessToken}&fields=status_code` + `https://${type}/v20.0/${photoId}?access_token=${accessToken}&fields=status_code`, + undefined, + '', + 0, + true, ) ).json(); await timer(10000); @@ -558,7 +562,11 @@ export class InstagramProvider while (status === 'IN_PROGRESS') { const { status_code } = await ( await this.fetch( - `https://${type}/v20.0/${containerId}?fields=status_code&access_token=${accessToken}` + `https://${type}/v20.0/${containerId}?fields=status_code&access_token=${accessToken}`, + undefined, + '', + 0, + true ) ).json(); await timer(10000); diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index 8ee14294..1e53ea68 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -37,7 +37,6 @@ export class PinterestProvider value: string; } | undefined { - if (body.indexOf('cover_image_url or cover_image_content_type') > -1) { return { type: 'bad-body' as const, @@ -212,12 +211,18 @@ export class PinterestProvider let statusCode = ''; while (statusCode !== 'succeeded') { const mediafile = await ( - await this.fetch('https://api.pinterest.com/v5/media/' + media_id, { - method: 'GET', - headers: { - Authorization: `Bearer ${accessToken}`, + await this.fetch( + 'https://api.pinterest.com/v5/media/' + media_id, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, }, - }) + '', + 0, + true + ) ).json(); await timer(30000); diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index fe0700f0..2de298c6 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -375,7 +375,10 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { body: JSON.stringify({ publish_id: publishId, }), - } + }, + '', + 0, + true ) ).json(); @@ -399,11 +402,11 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { 'titok-error-upload', JSON.stringify(post), Buffer.from(JSON.stringify(post)), - handleError?.value || '', + handleError?.value || '' ); } - await timer(3000); + await timer(10000); } } @@ -496,7 +499,11 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { photo_cover_index: 0, photo_images: firstPost.media?.map((p) => p.path), }, - post_mode: firstPost?.settings?.content_posting_method === 'DIRECT_POST' ? 'DIRECT_POST' : 'MEDIA_UPLOAD', + post_mode: + firstPost?.settings?.content_posting_method === + 'DIRECT_POST' + ? 'DIRECT_POST' + : 'MEDIA_UPLOAD', media_type: 'PHOTO', }), }), From 85d015f34920c0b101149d532098b29875ef4880 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 00:07:46 +0700 Subject: [PATCH 085/282] feat: check also waiting --- apps/cron/src/tasks/check.missing.queues.ts | 8 +++++--- apps/cron/src/tasks/post.now.pending.queues.ts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/cron/src/tasks/check.missing.queues.ts b/apps/cron/src/tasks/check.missing.queues.ts index c383ab6a..6c9def7f 100644 --- a/apps/cron/src/tasks/check.missing.queues.ts +++ b/apps/cron/src/tasks/check.missing.queues.ts @@ -19,9 +19,11 @@ export class CheckMissingQueues { id: p.id, publishDate: p.publishDate, isJob: - (await this._workerServiceProducer - .getQueue('post') - .getJobState(p.id)) === 'delayed', + ['delayed', 'waiting'].indexOf( + await this._workerServiceProducer + .getQueue('post') + .getJobState(p.id) + ) > -1, })) ) ).filter((p) => !p.isJob); diff --git a/apps/cron/src/tasks/post.now.pending.queues.ts b/apps/cron/src/tasks/post.now.pending.queues.ts index 3138a25c..69105304 100644 --- a/apps/cron/src/tasks/post.now.pending.queues.ts +++ b/apps/cron/src/tasks/post.now.pending.queues.ts @@ -18,9 +18,11 @@ export class PostNowPendingQueues { id: p.id, publishDate: p.publishDate, isJob: - (await this._workerServiceProducer - .getQueue('post') - .getJobState(p.id)) === 'delayed', + ['delayed', 'waiting'].indexOf( + await this._workerServiceProducer + .getQueue('post') + .getJobState(p.id) + ) > -1, })) ) ).filter((p) => !p.isJob); From d698732a6c60ea416113f4d854e265ff9c93edce Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 01:06:36 +0700 Subject: [PATCH 086/282] feat: high concurrency --- .../nestjs-libraries/src/bull-mq-transport-new/strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts index 8cb52190..d6d7efa7 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts @@ -34,7 +34,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy { }, { maxStalledCount: 10, - concurrency: 5, + concurrency: 300, connection: ioRedis, removeOnComplete: { count: 0, From e0fe7d0381b394d54e7c769b11b861361166525d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 01:28:24 +0700 Subject: [PATCH 087/282] feat: expiration --- .../helpers/src/utils/concurrency.service.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/libraries/helpers/src/utils/concurrency.service.ts b/libraries/helpers/src/utils/concurrency.service.ts index 906e5f52..ce2065ae 100644 --- a/libraries/helpers/src/utils/concurrency.service.ts +++ b/libraries/helpers/src/utils/concurrency.service.ts @@ -1,6 +1,7 @@ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; import Bottleneck from 'bottleneck'; import { timer } from '@gitroom/helpers/utils/timer'; +import { BadBody } from '@gitroom/nestjs-libraries/integrations/social.abstract'; const connection = new Bottleneck.IORedisConnection({ client: ioRedis, @@ -23,19 +24,28 @@ export const concurrency = async ( minTime: 1000, }); let load: T; + + if (ignoreConcurrency) { + return await func(); + } + try { - if (ignoreConcurrency) { - return await func(); - } load = await mapper[strippedIdentifier].schedule( - { expiration: 600000 }, + { expiration: 10000 }, async () => { try { return await func(); } catch (err) {} } ); - } catch (err) {} + } catch (err) { + throw new BadBody( + identifier, + JSON.stringify({}), + {} as any, + `Something is wrong with ${identifier}` + ); + } return load; }; From 113003b83285798f4e5edf8a8fe0d5bb5cf41b75 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 02:14:52 +0700 Subject: [PATCH 088/282] feat: more expiration for jobs --- libraries/helpers/src/utils/concurrency.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/helpers/src/utils/concurrency.service.ts b/libraries/helpers/src/utils/concurrency.service.ts index ce2065ae..1e6e4494 100644 --- a/libraries/helpers/src/utils/concurrency.service.ts +++ b/libraries/helpers/src/utils/concurrency.service.ts @@ -31,7 +31,7 @@ export const concurrency = async ( try { load = await mapper[strippedIdentifier].schedule( - { expiration: 10000 }, + { expiration: 60000 }, async () => { try { return await func(); From 4ace1dc8868fdd75deae73dce719dbd95ca142d6 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 02:48:39 +0700 Subject: [PATCH 089/282] feat: more timeout for instagram --- .../src/integrations/social/instagram.provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index e9bbbae7..5a7edf4a 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -505,7 +505,7 @@ export class InstagramProvider true, ) ).json(); - await timer(10000); + await timer(30000); status = status_code; } console.log('in progress3', id); @@ -569,7 +569,7 @@ export class InstagramProvider true ) ).json(); - await timer(10000); + await timer(30000); status = status_code; } From fcfc4109e263bd9b4263043138064f8c12210662 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 02:59:35 +0700 Subject: [PATCH 090/282] feat set maximum stalled as maximum concurrency --- .../nestjs-libraries/src/bull-mq-transport-new/strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts index d6d7efa7..f6068144 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts @@ -33,7 +33,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy { }); }, { - maxStalledCount: 10, + maxStalledCount: 300, concurrency: 300, connection: ioRedis, removeOnComplete: { From e09187db502e5bbb37b6f23762d67de10da1f09e Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 03:01:52 +0700 Subject: [PATCH 091/282] feat set maximum stalled as maximum concurrency --- .../nestjs-libraries/src/bull-mq-transport-new/strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts index f6068144..d6d7efa7 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport-new/strategy.ts @@ -33,7 +33,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy { }); }, { - maxStalledCount: 300, + maxStalledCount: 10, concurrency: 300, connection: ioRedis, removeOnComplete: { From 27f1db769a664ba60b2d381142c6e468faf40f97 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 12:05:09 +0700 Subject: [PATCH 092/282] feat: find missing jobs fix --- .../src/database/prisma/posts/posts.repository.ts | 10 ++++++++++ .../nestjs-libraries/src/database/prisma/schema.prisma | 5 +++++ 2 files changed, 15 insertions(+) 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 9508a26c..96fccaff 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -48,6 +48,11 @@ export class PostsRepository { searchForMissingThreeHoursPosts() { return this._post.model.post.findMany({ where: { + integration: { + refreshNeeded: false, + inBetweenSteps: false, + disabled: false, + }, publishDate: { gte: dayjs.utc().toDate(), lt: dayjs.utc().add(3, 'hour').toDate(), @@ -66,6 +71,11 @@ export class PostsRepository { getOldPosts(orgId: string, date: string) { return this._post.model.post.findMany({ where: { + integration: { + refreshNeeded: false, + inBetweenSteps: false, + disabled: false, + }, organizationId: orgId, publishDate: { lte: dayjs(date).toDate(), diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 6027caa2..0958133a 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -338,9 +338,14 @@ model Integration { @@unique([organizationId, internalId]) @@index([rootInternalId]) @@index([organizationId]) + @@index([providerIdentifier]) @@index([updatedAt]) + @@index([createdAt]) @@index([deletedAt]) @@index([customerId]) + @@index([inBetweenSteps]) + @@index([refreshNeeded]) + @@index([disabled]) } model Signatures { From caf99d38c5d2ff457a3e8b47aa0177ac46100f08 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 18:21:07 +0700 Subject: [PATCH 093/282] feat: images and videos to reddit --- .../billing/main.billing.component.tsx | 2 +- .../helpers/media.settings.component.tsx | 21 +-- .../providers/high.order.provider.tsx | 1 + .../providers/reddit/reddit.provider.tsx | 24 ++- .../new-launch/providers/reddit/subreddit.tsx | 18 -- .../src/components/new-launch/store.ts | 2 +- .../posts/providers-settings/reddit.dto.ts | 7 - .../integrations/social/reddit.provider.ts | 163 ++++++++++++++---- 8 files changed, 169 insertions(+), 69 deletions(-) diff --git a/apps/frontend/src/components/billing/main.billing.component.tsx b/apps/frontend/src/components/billing/main.billing.component.tsx index 00476764..cad4e3bc 100644 --- a/apps/frontend/src/components/billing/main.billing.component.tsx +++ b/apps/frontend/src/components/billing/main.billing.component.tsx @@ -505,7 +505,7 @@ export const MainBillingComponent: FC<{ {t( 'your_subscription_will_be_canceled_at', 'Your subscription will be canceled at' - )} + )}{' '} {newDayjs(subscription.cancelAt).local().format('D MMM, YYYY')}
      {t( diff --git a/apps/frontend/src/components/launches/helpers/media.settings.component.tsx b/apps/frontend/src/components/launches/helpers/media.settings.component.tsx index 581d2088..3cf774d1 100644 --- a/apps/frontend/src/components/launches/helpers/media.settings.component.tsx +++ b/apps/frontend/src/components/launches/helpers/media.settings.component.tsx @@ -106,16 +106,12 @@ export const CreateThumbnail: FC<{ const [isCapturing, setIsCapturing] = useState(false); const handleLoadedMetadata = useCallback(() => { - if (videoRef.current) { - setDuration(videoRef.current.duration); - setIsLoaded(true); - } + setDuration(videoRef?.current?.duration); + setIsLoaded(true); }, []); const handleTimeUpdate = useCallback(() => { - if (videoRef.current) { - setCurrentTime(videoRef.current.currentTime); - } + setCurrentTime(videoRef?.current?.currentTime); }, []); const handleSeek = useCallback((e: React.ChangeEvent) => { @@ -127,8 +123,6 @@ export const CreateThumbnail: FC<{ }, []); const captureFrame = useCallback(async () => { - if (!videoRef.current || !canvasRef.current) return; - setIsCapturing(true); try { @@ -299,7 +293,14 @@ export const MediaComponentInner: FC<{ alt: string; }) => void; media: - | { id: string; name: string; path: string; thumbnail: string; alt: string, thumbnailTimestamp?: number } + | { + id: string; + name: string; + path: string; + thumbnail: string; + alt: string; + thumbnailTimestamp?: number; + } | undefined; }> = (props) => { const { onClose, onSelect, media } = props; diff --git a/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx b/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx index 1a02aa41..eb888169 100644 --- a/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx @@ -54,6 +54,7 @@ export const withProvider = function (params: { value: Array< Array<{ path: string; + thumbnail?: string; }> >, settings: T, diff --git a/apps/frontend/src/components/new-launch/providers/reddit/reddit.provider.tsx b/apps/frontend/src/components/new-launch/providers/reddit/reddit.provider.tsx index 68e4aefe..18ce5cae 100644 --- a/apps/frontend/src/components/new-launch/providers/reddit/reddit.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/reddit/reddit.provider.tsx @@ -111,7 +111,6 @@ const RedditPreview: FC = (props) => {
      {value.title}
      -
      { + if ( + settings?.subreddit?.some( + (p: any, index: number) => + p?.value?.type === 'media' && posts[0].length !== 1 + ) + ) { + return 'When posting a media post, you must attached exactly one media file.'; + } + + if ( + posts.some((p) => + p.some((a) => !a.thumbnail && a.path.indexOf('mp4') > -1) + ) + ) { + return 'You must attach a thumbnail to your video post.'; + } + + return true; + }, maximumCharacters: 10000, }); diff --git a/apps/frontend/src/components/new-launch/providers/reddit/subreddit.tsx b/apps/frontend/src/components/new-launch/providers/reddit/subreddit.tsx index f53db361..9e002e1f 100644 --- a/apps/frontend/src/components/new-launch/providers/reddit/subreddit.tsx +++ b/apps/frontend/src/components/new-launch/providers/reddit/subreddit.tsx @@ -233,24 +233,6 @@ export const Subreddit: FC<{ onChange={setURL} /> )} - {value.type === 'media' && ( -
      -
      -
      - -
      -
      - )} ) : (
      diff --git a/apps/frontend/src/components/new-launch/store.ts b/apps/frontend/src/components/new-launch/store.ts index d92d169e..754c833d 100644 --- a/apps/frontend/src/components/new-launch/store.ts +++ b/apps/frontend/src/components/new-launch/store.ts @@ -11,7 +11,7 @@ import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone'; interface Values { id: string; content: string; - media: { id: string; path: string }[]; + media: { id: string; path: string, thumbnail?: string }[]; } interface Internal { diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts index 1eeef00f..8785ddbe 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts @@ -9,7 +9,6 @@ import { ValidateIf, ValidateNested, } from 'class-validator'; -import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto'; import { Type } from 'class-transformer'; export class RedditFlairDto { @@ -57,12 +56,6 @@ export class RedditSettingsDtoInner { @IsDefined() @ValidateNested() flair: RedditFlairDto; - - @ValidateIf((e) => e.type === 'media') - @ValidateNested({ each: true }) - @Type(() => MediaDto) - @ArrayMinSize(1) - media: MediaDto[]; } export class RedditSettingsValueDto { diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts index 2676706a..c29194c8 100644 --- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts @@ -9,6 +9,12 @@ import { RedditSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/provider import { timer } from '@gitroom/helpers/utils/timer'; import { groupBy } from 'lodash'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; +import { lookup } from 'mime-types'; +import axios from 'axios'; +import WebSocket from 'ws'; + +// @ts-ignore +global.WebSocket = WebSocket; export class RedditProvider extends SocialAbstract implements SocialProvider { override maxConcurrentJob = 1; // Reddit has strict rate limits (1 request per second) @@ -117,6 +123,55 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { }; } + private async uploadFileToReddit(accessToken: string, path: string) { + const mimeType = lookup(path); + const formData = new FormData(); + formData.append('filepath', path.split('/').pop()); + formData.append('mimetype', mimeType || 'application/octet-stream'); + + const { + args: { action, fields }, + } = await ( + await this.fetch( + 'https://oauth.reddit.com/api/media/asset', + { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + body: formData, + }, + 'reddit', + 0, + true + ) + ).json(); + + const { data } = await axios.get(path, { + responseType: 'arraybuffer', + }); + + const upload = (fields as { name: string; value: string }[]).reduce( + (acc, value) => { + acc.append(value.name, value.value); + return acc; + }, + new FormData() + ); + + upload.append( + 'file', + new Blob([Buffer.from(data)], { type: mimeType as string }) + ); + + const d = await fetch('https:' + action, { + method: 'POST', + body: upload, + }); + + return [...(await d.text()).matchAll(/(.*?)<\/Location>/g)][0][1]; + } + async post( id: string, accessToken: string, @@ -131,7 +186,9 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { title: firstPostSettings.value.title || '', kind: firstPostSettings.value.type === 'media' - ? 'image' + ? post.media[0].path.indexOf('mp4') > -1 + ? 'video' + : 'image' : firstPostSettings.value.type, ...(firstPostSettings.value.flair ? { flair_id: firstPostSettings.value.flair.id } @@ -143,22 +200,25 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { : {}), ...(firstPostSettings.value.type === 'media' ? { - url: `${ - firstPostSettings.value.media[0].path.indexOf('http') === -1 - ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/uploads` - : `` - }${firstPostSettings.value.media[0].path}`, + url: await this.uploadFileToReddit( + accessToken, + post.media[0].path + ), + ...(post.media[0].path.indexOf('mp4') > -1 + ? { + video_poster_url: await this.uploadFileToReddit( + accessToken, + post.media[0].thumbnail + ), + } + : {}), } : {}), text: post.message, sr: firstPostSettings.value.subreddit, }; - const { - json: { - data: { id, name, url }, - }, - } = await ( + const all = await ( await this.fetch('https://oauth.reddit.com/api/submit', { method: 'POST', headers: { @@ -169,6 +229,38 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { }) ).json(); + const { id, name, url } = await new Promise<{ + id: string; + name: string; + url: string; + }>((res) => { + if (all?.json?.data?.id) { + res(all.json.data); + } + + const ws = new WebSocket(all.json.data.websocket_url); + ws.on('message', (data: any) => { + setTimeout(() => { + res({ id: '', name: '', url: '' }); + ws.close(); + }, 30_000); + try { + const parsedData = JSON.parse(data.toString()); + if (parsedData?.payload?.redirect) { + const onlyId = parsedData?.payload?.redirect.replace( + /https:\/\/www\.reddit\.com\/r\/.*?\/comments\/(.*?)\/.*/g, + '$1' + ); + res({ + id: onlyId, + name: `t3_${onlyId}`, + url: parsedData?.payload?.redirect, + }); + } + } catch (err) {} + }); + }); + valueArray.push({ postId: id, releaseURL: url, @@ -202,8 +294,6 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { }) ).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, @@ -233,7 +323,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { const { data: { children }, } = await ( - await fetch( + await this.fetch( `https://oauth.reddit.com/subreddits/search?show=public&q=${data.word}&sort=activity&show_users=false&limit=10`, { method: 'GET', @@ -241,7 +331,10 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/x-www-form-urlencoded', }, - } + }, + 'reddit', + 0, + false ) ).json(); @@ -267,28 +360,34 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { permissions.push('link'); } - // if (submissionType === 'any' || allow_images) { - // permissions.push('media'); - // } + if (allow_images) { + permissions.push('media'); + } return permissions; } async restrictions(accessToken: string, data: { subreddit: string }) { const { - data: { submission_type, allow_images }, + data: { submission_type, allow_images, ...all2 }, } = await ( - await fetch(`https://oauth.reddit.com/${data.subreddit}/about`, { - method: 'GET', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', + await this.fetch( + `https://oauth.reddit.com/${data.subreddit}/about`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, }, - }) + 'reddit', + 0, + false + ) ).json(); const { is_flair_required, ...all } = await ( - await fetch( + await this.fetch( `https://oauth.reddit.com/api/v1/${ data.subreddit.split('/r/')[1] }/post_requirements`, @@ -298,7 +397,10 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/x-www-form-urlencoded', }, - } + }, + 'reddit', + 0, + false ) ).json(); @@ -307,7 +409,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { async (res) => { try { const flair = await ( - await fetch( + await this.fetch( `https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`, { method: 'GET', @@ -315,7 +417,10 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/x-www-form-urlencoded', }, - } + }, + 'reddit', + 0, + false ) ).json(); From a8e32dc39993ecdd25972fc8fd184e5143c2d11b Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 20:22:38 +0700 Subject: [PATCH 094/282] feat: suspicious registartion --- .../src/api/routes/integrations.controller.ts | 26 ++++++----- .../launches/add.provider.component.tsx | 11 ++++- .../launches/continue.integration.tsx | 7 ++- .../src/components/layout/layout.settings.tsx | 1 - .../layout/pre-condition.component.tsx | 43 +++++++++++++++++++ .../new-layout/layout.component.tsx | 3 +- .../integrations/integration.repository.ts | 15 ++++++- .../integrations/integration.service.ts | 4 ++ 8 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 apps/frontend/src/components/layout/pre-condition.component.tsx diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index fb3eee3b..8980ab27 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -1,13 +1,5 @@ import { - Body, - Controller, - Delete, - Get, - Param, - Post, - Put, - Query, - UseFilters, + Body, Controller, Delete, Get, HttpException, Param, Post, Put, Query, UseFilters } from '@nestjs/common'; import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto'; @@ -261,7 +253,7 @@ export class IntegrationsController { throw new Error('Invalid integration'); } - let newList: any[] | {none: true} = []; + let newList: any[] | { none: true } = []; try { newList = (await this.functionIntegration(org, body)) || []; } catch (err) { @@ -298,7 +290,7 @@ export class IntegrationsController { image: p.image, label: p.name, })), - ...newList as any[], + ...(newList as any[]), ], (p) => p.id ).filter((f) => f.label && f.id); @@ -487,6 +479,18 @@ export class IntegrationsController { validName = `Channel_${String(id).slice(0, 8)}`; } } + + if ( + process.env.STRIPE_PUBLISHABLE_KEY && + org.isTrailing && + !!(await this._integrationService.checkPreviousConnections( + org.id, + String(id) + )) + ) { + throw new HttpException('', 412); + } + return this._integrationService.createOrUpdateIntegration( additionalSettings, !!integrationProvider.oneTimeToken, diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index a72c4b1a..bf83b7ed 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -1,14 +1,14 @@ 'use client'; import { useModals } from '@mantine/modals'; -import React, { FC, useCallback, useMemo } from 'react'; +import React, { FC, useCallback, useEffect, useMemo } from 'react'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { Input } from '@gitroom/react/form/input'; import { FieldValues, FormProvider, useForm } from 'react-hook-form'; import { Button } from '@gitroom/react/form/button'; import { classValidatorResolver } from '@hookform/resolvers/class-validator'; import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; import { useVariables } from '@gitroom/react/helpers/variable.context'; import { useToaster } from '@gitroom/react/toaster/toaster'; @@ -42,9 +42,16 @@ export const AddProviderButton: FC<{ update?: () => void; }> = (props) => { const { update } = props; + const query = useSearchParams(); const add = useAddProvider(update); const t = useT(); + useEffect(() => { + if (query.get('onboarding')) { + add(); + } + }, []); + return ( + +
      +
      + ); +}; +export const PreConditionComponent: FC = () => { + const modal = useModals(); + const query = useSearchParams(); + useEffect(() => { + if (query.get('precondition')) { + modal.openModal({ + title: '', + withCloseButton: false, + classNames: { + modal: 'text-textColor', + }, + size: 'auto', + children: ( + + + + ), + }); + } + }, []); + return null; +}; diff --git a/apps/frontend/src/components/new-layout/layout.component.tsx b/apps/frontend/src/components/new-layout/layout.component.tsx index 14063dbb..abcfe4c5 100644 --- a/apps/frontend/src/components/new-layout/layout.component.tsx +++ b/apps/frontend/src/components/new-layout/layout.component.tsx @@ -38,6 +38,7 @@ import { ChromeExtensionComponent } from '@gitroom/frontend/components/layout/ch import NotificationComponent from '@gitroom/frontend/components/notifications/notification.component'; import { BillingAfter } from '@gitroom/frontend/components/new-layout/billing.after'; import { OrganizationSelector } from '@gitroom/frontend/components/layout/organization.selector'; +import { PreConditionComponent } from '@gitroom/frontend/components/layout/pre-condition.component'; const jakartaSans = Plus_Jakarta_Sans({ weight: ['600', '500'], @@ -79,8 +80,8 @@ export const LayoutComponent = ({ children }: { children: ReactNode }) => { + - {user.tier !== 'FREE' && }
      ({ @@ -68,6 +68,19 @@ export class IntegrationRepository { }); } + async checkPreviousConnections(org: string, id: string) { + const findIt = await this._integration.model.integration.findFirst({ + where: { + organizationId: { + not: org, + }, + rootInternalId: id.split('_').pop(), + }, + }); + + return findIt; + } + updateProviderSettings(org: string, id: string, settings: string) { return this._integration.model.integration.update({ where: { 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 d7519354..a54758e6 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -73,6 +73,10 @@ export class IntegrationService { ); } + checkPreviousConnections(org: string, id: string) { + return this._integrationRepository.checkPreviousConnections(org, id); + } + async createOrUpdateIntegration( additionalSettings: | { From 2ab65bb375e789bbc182f9a0ad6e9bafa466f756 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 9 Aug 2025 20:29:13 +0700 Subject: [PATCH 095/282] feat: fix billing --- .../src/api/routes/integrations.controller.ts | 13 +++++++++++-- .../prisma/integrations/integration.repository.ts | 11 ++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index 8980ab27..7ae2136d 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -1,5 +1,14 @@ import { - Body, Controller, Delete, Get, HttpException, Param, Post, Put, Query, UseFilters + Body, + Controller, + Delete, + Get, + HttpException, + Param, + Post, + Put, + Query, + UseFilters, } from '@nestjs/common'; import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto'; @@ -483,7 +492,7 @@ export class IntegrationsController { if ( process.env.STRIPE_PUBLISHABLE_KEY && org.isTrailing && - !!(await this._integrationService.checkPreviousConnections( + (await this._integrationService.checkPreviousConnections( org.id, String(id) )) 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 3c97e97c..db9c5f22 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -69,16 +69,17 @@ export class IntegrationRepository { } async checkPreviousConnections(org: string, id: string) { - const findIt = await this._integration.model.integration.findFirst({ + const findIt = await this._integration.model.integration.findMany({ where: { - organizationId: { - not: org, - }, rootInternalId: id.split('_').pop(), }, + select: { + organizationId: true, + id: true, + }, }); - return findIt; + return findIt.some((f) => f.organizationId === org) || findIt.length === 0; } updateProviderSettings(org: string, id: string, settings: string) { From dee9a6d5f07e2832834aeb58daa40e2b0588a830 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 10 Aug 2025 03:14:56 +0700 Subject: [PATCH 096/282] feat: remove timezone --- apps/frontend/src/app/(app)/layout.tsx | 16 ++++----- .../src/components/layout/set.timezone.tsx | 2 +- .../components/settings/metric.component.tsx | 35 ++++++++++--------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/apps/frontend/src/app/(app)/layout.tsx b/apps/frontend/src/app/(app)/layout.tsx index e30d05f2..bea3e05e 100644 --- a/apps/frontend/src/app/(app)/layout.tsx +++ b/apps/frontend/src/app/(app)/layout.tsx @@ -18,13 +18,13 @@ import { FacebookComponent } from '@gitroom/frontend/components/layout/facebook. import { headers } from 'next/headers'; import { headerName } from '@gitroom/react/translation/i18n.config'; import { HtmlComponent } from '@gitroom/frontend/components/layout/html.component'; -import dynamicLoad from 'next/dynamic'; -const SetTimezone = dynamicLoad( - () => import('@gitroom/frontend/components/layout/set.timezone'), - { - ssr: false, - } -); +// import dynamicLoad from 'next/dynamic'; +// const SetTimezone = dynamicLoad( +// () => import('@gitroom/frontend/components/layout/set.timezone'), +// { +// ssr: false, +// } +// ); const jakartaSans = Plus_Jakarta_Sans({ weight: ['600', '500'], @@ -79,7 +79,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) { } > - + {/**/} diff --git a/apps/frontend/src/components/layout/set.timezone.tsx b/apps/frontend/src/components/layout/set.timezone.tsx index 1835b96e..f37b4f0f 100644 --- a/apps/frontend/src/components/layout/set.timezone.tsx +++ b/apps/frontend/src/components/layout/set.timezone.tsx @@ -16,7 +16,7 @@ export const getTimezone = () => { }; export const newDayjs = (config?: ConfigType) => { - return dayjs.tz(config, getTimezone()); + return dayjs(config); }; const SetTimezone: FC = () => { diff --git a/apps/frontend/src/components/settings/metric.component.tsx b/apps/frontend/src/components/settings/metric.component.tsx index d91ae891..23096d49 100644 --- a/apps/frontend/src/components/settings/metric.component.tsx +++ b/apps/frontend/src/components/settings/metric.component.tsx @@ -11,6 +11,7 @@ const dateMetrics = [ import dayjs from 'dayjs'; import timezone from 'dayjs/plugin/timezone'; +import { timezoneSaver } from '@gitroom/frontend/components/layout/set.timezone'; dayjs.extend(timezone); const MetricComponent = () => { @@ -46,23 +47,23 @@ const MetricComponent = () => { ))} -
      Current Timezone
      - + {/*
      Current Timezone
      */} + {/**/} + {/* {timezones.map((metric) => (*/} + {/* */} + {/* {metric.label}*/} + {/* */} + {/* ))}*/} + {/**/}
      ); }; From 9aea717ed22a47c2f8d442909928f06eaf7fd964 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 10 Aug 2025 03:20:32 +0700 Subject: [PATCH 097/282] feat: remove timezone --- apps/frontend/src/components/settings/metric.component.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/frontend/src/components/settings/metric.component.tsx b/apps/frontend/src/components/settings/metric.component.tsx index 23096d49..29efeaf5 100644 --- a/apps/frontend/src/components/settings/metric.component.tsx +++ b/apps/frontend/src/components/settings/metric.component.tsx @@ -11,7 +11,6 @@ const dateMetrics = [ import dayjs from 'dayjs'; import timezone from 'dayjs/plugin/timezone'; -import { timezoneSaver } from '@gitroom/frontend/components/layout/set.timezone'; dayjs.extend(timezone); const MetricComponent = () => { From 07177c7929d20064e21e6fb3bb90c84d3274e4ba Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 10 Aug 2025 11:08:58 +0700 Subject: [PATCH 098/282] feat: fix connections --- .../database/prisma/integrations/integration.repository.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 db9c5f22..6024b921 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -79,7 +79,11 @@ export class IntegrationRepository { }, }); - return findIt.some((f) => f.organizationId === org) || findIt.length === 0; + if (findIt.some((f) => f.organizationId === org)) { + return false; + } + + return findIt.length > 0; } updateProviderSettings(org: string, id: string, settings: string) { From e0fa9f49c6d2c31d6ca3bc5faf65f938c2a2153e Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 10 Aug 2025 18:46:02 +0700 Subject: [PATCH 099/282] feat: normal should not have html tags --- libraries/helpers/src/utils/strip.html.validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts index e5abdab4..7ad3591c 100644 --- a/libraries/helpers/src/utils/strip.html.validation.ts +++ b/libraries/helpers/src/utils/strip.html.validation.ts @@ -232,7 +232,7 @@ export const stripHtmlValidation = ( convertMentionFunction ); - return striptags(processedHtml, ['h1', 'h2', 'h3']); + return striptags(processedHtml); } // Strip all other tags From 3cf7ed409636b498247917445141a41134a8d754 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 11 Aug 2025 19:02:09 +0700 Subject: [PATCH 100/282] feat: shortlink fix --- .../src/short-linking/short.link.service.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/libraries/nestjs-libraries/src/short-linking/short.link.service.ts b/libraries/nestjs-libraries/src/short-linking/short.link.service.ts index 3301606c..b3706713 100644 --- a/libraries/nestjs-libraries/src/short-linking/short.link.service.ts +++ b/libraries/nestjs-libraries/src/short-linking/short.link.service.ts @@ -37,7 +37,8 @@ export class ShortLinkService { } const mergeMessages = messages.join(' '); - const urlRegex = /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm; + const urlRegex = + /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm; const urls = mergeMessages.match(urlRegex); if (!urls) { // No URLs found, return the original text @@ -49,12 +50,20 @@ export class ShortLinkService { ); } - async convertTextToShortLinks(id: string, messages: string[]) { + async convertTextToShortLinks(id: string, messagesList: string[]) { if (ShortLinkService.provider.shortLinkDomain === 'empty') { - return messages; + return messagesList; } - const urlRegex = /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm; + const messages = messagesList.map((text) => { + return text + .replace(/&/g, '&') + .replace(/?/g, '?') + .replace(/#/g, '#'); + }); + + const urlRegex = + /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm; return Promise.all( messages.map(async (text) => { const urls = uniq(text.match(urlRegex)); From 08e7dfbe1dc896642ba3f8f9ad535683d51a58d8 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 11 Aug 2025 19:28:44 +0700 Subject: [PATCH 101/282] feat: fix editor --- .../src/components/new-launch/editor.tsx | 132 ++++++++++-------- 1 file changed, 77 insertions(+), 55 deletions(-) diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 681589fc..6d6c9b09 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -759,66 +759,84 @@ export const OnlyEditor = forwardRef< InterceptUnderlineShortcut, BulletList, ListItem, - Link.configure({ - openOnClick: false, - autolink: true, - defaultProtocol: 'https', - protocols: ['http', 'https'], - isAllowedUri: (url, ctx) => { - try { - // construct URL - const parsedUrl = url.includes(':') - ? new URL(url) - : new URL(`${ctx.defaultProtocol}://${url}`); + ...(editorType === 'html' || editorType === 'markdown' + ? [ + Link.configure({ + openOnClick: false, + autolink: true, + defaultProtocol: 'https', + protocols: ['http', 'https'], + isAllowedUri: (url, ctx) => { + try { + // prevent transforming plain emails like foo@bar.com into links + const trimmed = String(url).trim(); + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (emailPattern.test(trimmed)) { + return false; + } - // use default validation - if (!ctx.defaultValidate(parsedUrl.href)) { - return false; - } + // construct URL + const parsedUrl = url.includes(':') + ? new URL(url) + : new URL(`${ctx.defaultProtocol}://${url}`); - // disallowed protocols - const disallowedProtocols = ['ftp', 'file', 'mailto']; - const protocol = parsedUrl.protocol.replace(':', ''); + // use default validation + if (!ctx.defaultValidate(parsedUrl.href)) { + return false; + } - if (disallowedProtocols.includes(protocol)) { - return false; - } + // disallowed protocols + const disallowedProtocols = ['ftp', 'file', 'mailto']; + const protocol = parsedUrl.protocol.replace(':', ''); - // only allow protocols specified in ctx.protocols - const allowedProtocols = ctx.protocols.map((p) => - typeof p === 'string' ? p : p.scheme - ); + if (disallowedProtocols.includes(protocol)) { + return false; + } - if (!allowedProtocols.includes(protocol)) { - return false; - } + // only allow protocols specified in ctx.protocols + const allowedProtocols = ctx.protocols.map((p) => + typeof p === 'string' ? p : p.scheme + ); - // all checks have passed - return true; - } catch { - return false; - } - }, - shouldAutoLink: (url) => { - try { - // construct URL - const parsedUrl = url.includes(':') - ? new URL(url) - : new URL(`https://${url}`); + if (!allowedProtocols.includes(protocol)) { + return false; + } - // only auto-link if the domain is not in the disallowed list - const disallowedDomains = [ - 'example-no-autolink.com', - 'another-no-autolink.com', - ]; - const domain = parsedUrl.hostname; + // all checks have passed + return true; + } catch { + return false; + } + }, + shouldAutoLink: (url) => { + try { + // prevent auto-linking of plain emails like foo@bar.com + const trimmed = String(url).trim(); + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (emailPattern.test(trimmed)) { + return false; + } - return !disallowedDomains.includes(domain); - } catch { - return false; - } - }, - }), + // construct URL + const parsedUrl = url.includes(':') + ? new URL(url) + : new URL(`https://${url}`); + + // only auto-link if the domain is not in the disallowed list + const disallowedDomains = [ + 'example-no-autolink.com', + 'another-no-autolink.com', + ]; + const domain = parsedUrl.hostname; + + return !disallowedDomains.includes(domain); + } catch { + return false; + } + }, + }), + ] + : []), ...(internal?.integration?.id ? [ Mention.configure({ @@ -839,9 +857,13 @@ export const OnlyEditor = forwardRef< }), ] : []), - Heading.configure({ - levels: [1, 2, 3], - }), + ...(editorType === 'html' || editorType === 'markdown' + ? [ + Heading.configure({ + levels: [1, 2, 3], + }), + ] + : []), History.configure({ depth: 100, // default is 100 newGroupDelay: 100, // default is 500ms From 11c02f8433705aae61c70adf2a52b64565238894 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 12 Aug 2025 13:52:32 +0700 Subject: [PATCH 102/282] feat: fix resend --- .../src/emails/resend.provider.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/libraries/nestjs-libraries/src/emails/resend.provider.ts b/libraries/nestjs-libraries/src/emails/resend.provider.ts index b08c0eed..3cca6da8 100644 --- a/libraries/nestjs-libraries/src/emails/resend.provider.ts +++ b/libraries/nestjs-libraries/src/emails/resend.provider.ts @@ -14,14 +14,18 @@ export class ResendProvider implements EmailInterface { emailFromAddress: string, replyTo?: string ) { - const sends = await resend.emails.send({ - from: `${emailFromName} <${emailFromAddress}>`, - to, - subject, - html, - ...(replyTo && { reply_to: replyTo }), - }); + try { + const sends = await resend.emails.send({ + from: `${emailFromName} <${emailFromAddress}>`, + to, + subject, + html, + ...(replyTo && { reply_to: replyTo }), + }); - return sends; + return sends; + } catch (err) { + console.log(err); + } } } From 8a1e45aa10082612260c4f4c3181cbb2097c2ecf Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 12 Aug 2025 13:55:03 +0700 Subject: [PATCH 103/282] feat: sent false --- libraries/nestjs-libraries/src/emails/resend.provider.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/nestjs-libraries/src/emails/resend.provider.ts b/libraries/nestjs-libraries/src/emails/resend.provider.ts index 3cca6da8..c0519b0f 100644 --- a/libraries/nestjs-libraries/src/emails/resend.provider.ts +++ b/libraries/nestjs-libraries/src/emails/resend.provider.ts @@ -27,5 +27,7 @@ export class ResendProvider implements EmailInterface { } catch (err) { console.log(err); } + + return { sent: false }; } } From 54b2b95cf3824cd9f93f5eb9b9be63269ba113e4 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 12 Aug 2025 14:39:13 +0700 Subject: [PATCH 104/282] feat: connect accounts with not picture --- .../src/integrations/social/bluesky.provider.ts | 2 +- .../src/integrations/social/dev.to.provider.ts | 2 +- .../src/integrations/social/discord.provider.ts | 2 +- .../src/integrations/social/dribbble.provider.ts | 2 +- .../src/integrations/social/facebook.provider.ts | 6 ++---- .../src/integrations/social/farcaster.provider.ts | 2 +- .../src/integrations/social/hashnode.provider.ts | 2 +- .../src/integrations/social/instagram.provider.ts | 6 ++---- .../social/instagram.standalone.provider.ts | 4 ++-- .../src/integrations/social/lemmy.provider.ts | 2 +- .../src/integrations/social/linkedin.provider.ts | 2 +- .../src/integrations/social/mastodon.provider.ts | 2 +- .../src/integrations/social/medium.provider.ts | 2 +- .../src/integrations/social/nostr.provider.ts | 2 +- .../src/integrations/social/pinterest.provider.ts | 2 +- .../src/integrations/social/reddit.provider.ts | 4 ++-- .../src/integrations/social/slack.provider.ts | 2 +- .../src/integrations/social/telegram.provider.ts | 2 +- .../src/integrations/social/threads.provider.ts | 12 ++++-------- .../src/integrations/social/tiktok.provider.ts | 2 +- .../src/integrations/social/vk.provider.ts | 4 ++-- .../src/integrations/social/x.provider.ts | 2 +- .../src/integrations/social/youtube.provider.ts | 4 ++-- 23 files changed, 32 insertions(+), 40 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 621549ad..1803992b 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -213,7 +213,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { accessToken: accessJwt, id: did, name: profile.data.displayName!, - picture: profile.data.avatar!, + picture: profile?.data?.avatar || '', username: profile.data.handle!, }; } catch (e) { diff --git a/libraries/nestjs-libraries/src/integrations/social/dev.to.provider.ts b/libraries/nestjs-libraries/src/integrations/social/dev.to.provider.ts index a8e422b4..53487b3d 100644 --- a/libraries/nestjs-libraries/src/integrations/social/dev.to.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/dev.to.provider.ts @@ -81,7 +81,7 @@ export class DevToProvider extends SocialAbstract implements SocialProvider { accessToken: body.apiKey, id, name, - picture: profile_image, + picture: profile_image || '', username, }; } catch (err) { diff --git a/libraries/nestjs-libraries/src/integrations/social/discord.provider.ts b/libraries/nestjs-libraries/src/integrations/social/discord.provider.ts index 54728649..8796147c 100644 --- a/libraries/nestjs-libraries/src/integrations/social/discord.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/discord.provider.ts @@ -242,7 +242,7 @@ export class DiscordProvider extends SocialAbstract implements SocialProvider { .filter((role: any) => role.name.toLowerCase().includes(data.query.toLowerCase()) ) - .filter((f) => f.name !== '@everyone' && f.name !== '@here'); + .filter((f: any) => f.name !== '@everyone' && f.name !== '@here'); const list = await ( await fetch( diff --git a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts index a8b76758..4e231085 100644 --- a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts @@ -54,7 +54,7 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider { accessToken: access_token, refreshToken: refreshToken, expiresIn: expires_in, - picture: profile_image, + picture: profile_image || '', username, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts index be62e6d3..c788d5b6 100644 --- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts @@ -236,9 +236,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { const { id, name, - picture: { - data: { url }, - }, + picture } = await ( await fetch( `https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}` @@ -251,7 +249,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { accessToken: access_token, refreshToken: access_token, expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), - picture: url, + picture: picture?.data?.url || '', username: '', }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts b/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts index 4bb81bdd..212b4010 100644 --- a/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts @@ -61,7 +61,7 @@ export class FarcasterProvider accessToken: data.signer_uuid, refreshToken: '', expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(), - picture: data.pfp_url, + picture: data?.pfp_url || '', username: data.username, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts b/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts index eee8978e..8fdd768e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts @@ -91,7 +91,7 @@ export class HashnodeProvider extends SocialAbstract implements SocialProvider { accessToken: body.apiKey, id, name, - picture: profilePicture, + picture: profilePicture || '', username, }; } catch (err) { diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 5a7edf4a..2bdf2889 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -364,9 +364,7 @@ export class InstagramProvider const { id, name, - picture: { - data: { url }, - }, + picture } = await ( await fetch( `https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}` @@ -379,7 +377,7 @@ export class InstagramProvider accessToken: access_token, refreshToken: access_token, expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), - picture: url, + picture: picture?.data?.url || '', username: '', }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts index a74ab431..68938b17 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts @@ -41,7 +41,7 @@ export class InstagramStandaloneProvider ) ).json(); - const { user_id, name, username, profile_picture_url } = await ( + const { user_id, name, username, profile_picture_url = '' } = await ( await fetch( `https://graph.instagram.com/v21.0/me?fields=user_id,username,name,profile_picture_url&access_token=${access_token}` ) @@ -53,7 +53,7 @@ export class InstagramStandaloneProvider accessToken: access_token, refreshToken: access_token, expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), - picture: profile_picture_url, + picture: profile_picture_url || '', username, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts b/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts index 5f2e0e6d..6b5a9759 100644 --- a/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts @@ -107,7 +107,7 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider { user.person_view.person.display_name || user.person_view.person.name || '', - picture: user.person_view.person.avatar || '', + picture: user?.person_view?.person?.avatar || '', username: body.identifier || '', }; } catch (e) { diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index aac124f7..d4731735 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -80,7 +80,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { refreshToken, expiresIn: expires_in, name, - picture, + picture: picture || '', username: vanityName, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts b/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts index 413f637c..72957765 100644 --- a/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts @@ -91,7 +91,7 @@ export class MastodonProvider extends SocialAbstract implements SocialProvider { accessToken: tokenInformation.access_token, refreshToken: 'null', expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(), - picture: personalInformation.avatar, + picture: personalInformation?.avatar || '', username: personalInformation.username, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/medium.provider.ts b/libraries/nestjs-libraries/src/integrations/social/medium.provider.ts index db838978..00b91c01 100644 --- a/libraries/nestjs-libraries/src/integrations/social/medium.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/medium.provider.ts @@ -72,7 +72,7 @@ export class MediumProvider extends SocialAbstract implements SocialProvider { accessToken: body.apiKey, id, name, - picture: imageUrl, + picture: imageUrl || '', username, }; } catch (err) { diff --git a/libraries/nestjs-libraries/src/integrations/social/nostr.provider.ts b/libraries/nestjs-libraries/src/integrations/social/nostr.provider.ts index ccfd2871..ec961d60 100644 --- a/libraries/nestjs-libraries/src/integrations/social/nostr.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/nostr.provider.ts @@ -147,7 +147,7 @@ export class NostrProvider extends SocialAbstract implements SocialProvider { accessToken: AuthService.signJWT({ password: body.password }), refreshToken: '', expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(), - picture: user.picture, + picture: user?.picture || '', username: user.name || 'nousername', }; } catch (e) { diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index 1e53ea68..585d188c 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -82,7 +82,7 @@ export class PinterestProvider accessToken: access_token, refreshToken: refreshToken, expiresIn: expires_in, - picture: profile_image, + picture: profile_image || '', username, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts index c29194c8..f249c023 100644 --- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts @@ -59,7 +59,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { accessToken, refreshToken: newRefreshToken, expiresIn, - picture: icon_img.split('?')[0], + picture: icon_img?.split?.('?')?.[0] || '', username: name, }; } @@ -118,7 +118,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { accessToken, refreshToken, expiresIn, - picture: icon_img.split('?')[0], + picture: icon_img?.split?.('?')?.[0] || '', username: name, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts b/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts index 459acff2..762c2bba 100644 --- a/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts @@ -95,7 +95,7 @@ export class SlackProvider extends SocialAbstract implements SocialProvider { accessToken: access_token, refreshToken: 'null', expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(), - picture: user.profile.image_original, + picture: user?.profile?.image_original || '', username: user.name, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts index 0fc8e8c7..ad521f89 100644 --- a/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts @@ -71,7 +71,7 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider { accessToken: String(chat.id), refreshToken: '', expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(), - picture: photo, + picture: photo || '', username: chat.username!, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts index 932466b1..e86ac41f 100644 --- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts @@ -41,9 +41,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { id, name, username, - picture: { - data: { url }, - }, + picture } = await this.fetchPageInformation(access_token); return { @@ -52,7 +50,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { accessToken: access_token, refreshToken: access_token, expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), - picture: url, + picture: picture?.data?.url || '', username: '', }; } @@ -112,9 +110,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { id, name, username, - picture: { - data: { url }, - }, + picture, } = await this.fetchPageInformation(access_token); return { @@ -123,7 +119,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { accessToken: access_token, refreshToken: access_token, expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), - picture: url, + picture: picture?.data?.url || '', username: username, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 2de298c6..85b6418e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -250,7 +250,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { accessToken: access_token, id: open_id.replace(/-/g, ''), name: display_name, - picture: avatar_url, + picture: avatar_url || '', username: username, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/vk.provider.ts b/libraries/nestjs-libraries/src/integrations/social/vk.provider.ts index 12811204..670dd803 100644 --- a/libraries/nestjs-libraries/src/integrations/social/vk.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/vk.provider.ts @@ -65,7 +65,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider { accessToken: access_token, refreshToken: refresh_token + '&&&&' + device_id, expiresIn: dayjs().add(expires_in, 'seconds').unix() - dayjs().unix(), - picture: avatar, + picture: avatar || '', username: first_name.toLowerCase(), }; } @@ -150,7 +150,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider { accessToken: access_token, refreshToken: refresh_token + '&&&&' + device_id, expiresIn: dayjs().add(expires_in, 'seconds').unix() - dayjs().unix(), - picture: avatar, + picture: avatar || '', username: first_name.toLowerCase(), }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index c73431e2..ca32f23c 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -258,7 +258,7 @@ export class XProvider extends SocialAbstract implements SocialProvider { name, refreshToken: '', expiresIn: 999999999, - picture: profile_image_url, + picture: profile_image_url || '', username, additionalSettings: [ { diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts index a9f0c369..7b6e6fcf 100644 --- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -132,7 +132,7 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { refreshToken: credentials.refresh_token!, id: data.id!, name: data.name!, - picture: data.picture!, + picture: data?.picture || '', username: '', }; } @@ -178,7 +178,7 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { refreshToken: tokens.refresh_token!, id: data.id!, name: data.name!, - picture: data.picture!, + picture: data?.picture || '', username: '', }; } From f37c913943c9827872d30469ecacc4558d6a5a41 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 12 Aug 2025 22:59:34 +0700 Subject: [PATCH 105/282] feat: something is wrong console --- libraries/helpers/src/utils/concurrency.service.ts | 1 + .../nestjs-libraries/src/integrations/social/threads.provider.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/helpers/src/utils/concurrency.service.ts b/libraries/helpers/src/utils/concurrency.service.ts index 1e6e4494..4f241493 100644 --- a/libraries/helpers/src/utils/concurrency.service.ts +++ b/libraries/helpers/src/utils/concurrency.service.ts @@ -39,6 +39,7 @@ export const concurrency = async ( } ); } catch (err) { + console.log(err); throw new BadBody( identifier, JSON.stringify({}), diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts index e86ac41f..b4a9a2f0 100644 --- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts @@ -13,7 +13,6 @@ import { capitalize, chunk } from 'lodash'; import { Plug } from '@gitroom/helpers/decorators/plug.decorator'; import { Integration } from '@prisma/client'; import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation'; -import { TwitterApi } from 'twitter-api-v2'; export class ThreadsProvider extends SocialAbstract implements SocialProvider { identifier = 'threads'; From 120a47690ee8f0a7677d6a463f6446080c1b5011 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 12 Aug 2025 23:36:34 +0700 Subject: [PATCH 106/282] feat: send email --- .../src/services/email.service.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/libraries/nestjs-libraries/src/services/email.service.ts b/libraries/nestjs-libraries/src/services/email.service.ts index 0e0943dc..7fdf80fc 100644 --- a/libraries/nestjs-libraries/src/services/email.service.ts +++ b/libraries/nestjs-libraries/src/services/email.service.ts @@ -96,16 +96,21 @@ export class EmailService {
      `; - const sends = await concurrency('send-email', 1, () => - this.emailService.sendEmail( - to, - subject, - modifiedHtml, - process.env.EMAIL_FROM_NAME, - process.env.EMAIL_FROM_ADDRESS, - replyTo - ) - ); - console.log(sends); + try { + const sends = await concurrency('send-email', 1, () => + this.emailService.sendEmail( + to, + subject, + modifiedHtml, + process.env.EMAIL_FROM_NAME, + process.env.EMAIL_FROM_ADDRESS, + replyTo + ) + ); + + console.log(sends); + } catch (err) { + console.log(err); + } } } From fb75b99eed32032ee732473f27f7b25d6668e560 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 13 Aug 2025 01:17:59 +0700 Subject: [PATCH 107/282] feat: remove emails from concurrency --- .../src/services/email.service.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/libraries/nestjs-libraries/src/services/email.service.ts b/libraries/nestjs-libraries/src/services/email.service.ts index 7fdf80fc..8c4fc989 100644 --- a/libraries/nestjs-libraries/src/services/email.service.ts +++ b/libraries/nestjs-libraries/src/services/email.service.ts @@ -97,17 +97,14 @@ export class EmailService { `; try { - const sends = await concurrency('send-email', 1, () => - this.emailService.sendEmail( - to, - subject, - modifiedHtml, - process.env.EMAIL_FROM_NAME, - process.env.EMAIL_FROM_ADDRESS, - replyTo - ) + const sends = await this.emailService.sendEmail( + to, + subject, + modifiedHtml, + process.env.EMAIL_FROM_NAME, + process.env.EMAIL_FROM_ADDRESS, + replyTo ); - console.log(sends); } catch (err) { console.log(err); From 8b73c1ed5659ed6b8cb7777fcfbb16bfa277e9c9 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 13 Aug 2025 11:15:11 +0700 Subject: [PATCH 108/282] feat: stop infinite while --- .../src/integrations/social/bluesky.provider.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 1803992b..8eedd7cc 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -6,6 +6,7 @@ import { } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { + BadBody, RefreshToken, SocialAbstract, } from '@gitroom/nestjs-libraries/integrations/social.abstract'; @@ -119,6 +120,15 @@ async function uploadVideo( if (status.jobStatus.blob) { blob = status.jobStatus.blob; } + + if (status.jobStatus.state === 'JOB_STATE_FAILED') { + throw new BadBody( + 'bluesky', + JSON.stringify({}), + {} as any, + 'Could not upload video, job failed' + ); + } // wait a second await new Promise((resolve) => setTimeout(resolve, 1000)); } @@ -295,7 +305,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { aspectRatio: { width: p.width, height: p.height, - } + }, })), }; } From a3047d1855facafba0f71f722065cb1ca13b8077 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 13 Aug 2025 14:19:16 +0700 Subject: [PATCH 109/282] feat: x error message longer than 2 minutes --- .../src/integrations/social/x.provider.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index ca32f23c..14bff67a 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -38,8 +38,7 @@ export class XProvider extends SocialAbstract implements SocialProvider { if (body.includes('usage-capped')) { return { type: 'refresh-token', - value: - 'Posting failed - capped reached. Please try again later', + value: 'Posting failed - capped reached. Please try again later', }; } if (body.includes('duplicate-rules')) { @@ -55,6 +54,17 @@ export class XProvider extends SocialAbstract implements SocialProvider { value: 'The Tweet contains a URL that is not allowed on X', }; } + if ( + body.includes( + 'This user is not allowed to post a video longer than 2 minutes' + ) + ) { + return { + type: 'bad-body', + value: + 'The video you are trying to post is longer than 2 minutes, which is not allowed for this account', + }; + } return undefined; } From 0df9fd392ec2512532f21667fbd8f942a4726d43 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 13 Aug 2025 14:23:40 +0700 Subject: [PATCH 110/282] feat: bluesky more timer, job state failed --- .../src/integrations/social/bluesky.provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 8eedd7cc..0e3f9e5c 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -129,8 +129,8 @@ async function uploadVideo( 'Could not upload video, job failed' ); } - // wait a second - await new Promise((resolve) => setTimeout(resolve, 1000)); + + await timer(30000); } console.log('posting video...'); From 2db252f97f7e2be8efdbc1e15c8052d1ab75f6cc Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 13 Aug 2025 22:01:20 +0700 Subject: [PATCH 111/282] feat: youtube upload can't have concurrency as it can be a long request --- .../src/integrations/social.abstract.ts | 13 ++++++++++--- .../src/integrations/social/youtube.provider.ts | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 0b4d26b1..b0ef5670 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -38,11 +38,17 @@ export abstract class SocialAbstract { d: { query: string }, id: string, integration: Integration - ): Promise<{ id: string; label: string; image: string, doNotCache?: boolean }[] | { none: true }> { + ): Promise< + | { id: string; label: string; image: string; doNotCache?: boolean }[] + | { none: true } + > { return { none: true }; } - async runInConcurrent(func: (...args: any[]) => Promise) { + async runInConcurrent( + func: (...args: any[]) => Promise, + ignoreConcurrency?: boolean + ) { const value = await concurrency( this.identifier, this.maxConcurrentJob, @@ -54,7 +60,8 @@ export abstract class SocialAbstract { const handle = this.handleErrors(JSON.stringify(err)); return { err: true, ...(handle || {}) }; } - } + }, + ignoreConcurrency ); if (value && value?.err && value?.value) { diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts index 7b6e6fcf..93caff82 100644 --- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -222,7 +222,8 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { media: { body: response.data, }, - }) + }), + true ); if (settings?.thumbnail?.path) { From 5e5ca78e9aff6ea10e9c98e236549465cfda7e10 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 13 Aug 2025 22:08:20 +0700 Subject: [PATCH 112/282] feat: long upload should not be in a concurrenct --- .../integrations/social/linkedin.provider.ts | 36 +++++++++++-------- .../src/integrations/social/x.provider.ts | 32 +++++++++-------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index d4731735..da870fd1 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -254,20 +254,26 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { const etags = []; for (let i = 0; i < picture.length; i += 1024 * 1024 * 2) { - const upload = await this.fetch(sendUrlRequest, { - method: 'PUT', - headers: { - 'X-Restli-Protocol-Version': '2.0.0', - 'LinkedIn-Version': '202501', - Authorization: `Bearer ${accessToken}`, - ...(isVideo - ? { 'Content-Type': 'application/octet-stream' } - : isPdf - ? { 'Content-Type': 'application/pdf' } - : {}), + const upload = await this.fetch( + sendUrlRequest, + { + method: 'PUT', + headers: { + 'X-Restli-Protocol-Version': '2.0.0', + 'LinkedIn-Version': '202501', + Authorization: `Bearer ${accessToken}`, + ...(isVideo + ? { 'Content-Type': 'application/octet-stream' } + : isPdf + ? { 'Content-Type': 'application/pdf' } + : {}), + }, + body: picture.slice(i, i + 1024 * 1024 * 2), }, - body: picture.slice(i, i + 1024 * 1024 * 2), - }); + 'linkedin', + 0, + true + ); etags.push(upload.headers.get('etag')); } @@ -737,7 +743,9 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { return elements.map((p: any) => ({ id: String(p.id), label: p.localizedName, - image: p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier || '', + image: + p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier || + '', })); } diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 14bff67a..1467352e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -317,22 +317,24 @@ export class XProvider extends SocialAbstract implements SocialProvider { postDetails.flatMap((p) => p?.media?.flatMap(async (m) => { return { - id: await this.runInConcurrent(async () => - client.v1.uploadMedia( - m.path.indexOf('mp4') > -1 - ? Buffer.from(await readOrFetch(m.path)) - : await sharp(await readOrFetch(m.path), { - animated: lookup(m.path) === 'image/gif', - }) - .resize({ - width: 1000, + id: await this.runInConcurrent( + async () => + client.v1.uploadMedia( + m.path.indexOf('mp4') > -1 + ? Buffer.from(await readOrFetch(m.path)) + : await sharp(await readOrFetch(m.path), { + animated: lookup(m.path) === 'image/gif', }) - .gif() - .toBuffer(), - { - mimeType: lookup(m.path) || '', - } - ) + .resize({ + width: 1000, + }) + .gif() + .toBuffer(), + { + mimeType: lookup(m.path) || '', + } + ), + true ), postId: p.id, }; From 7b8fa7fb7bf2f9ac75409023dd3bf5c67e06b820 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 14 Aug 2025 11:38:27 +0700 Subject: [PATCH 113/282] feat: retry --- .../nestjs-libraries/src/integrations/social.abstract.ts | 9 ++++++++- .../src/integrations/social/instagram.provider.ts | 9 ++++++++- .../integrations/social/instagram.standalone.provider.ts | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index b0ef5670..778864fb 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -29,7 +29,9 @@ export abstract class SocialAbstract { public handleErrors( body: string - ): { type: 'refresh-token' | 'bad-body'; value: string } | undefined { + ): + | { type: 'refresh-token' | 'bad-body' | 'retry'; value: string } + | undefined { return undefined; } @@ -112,6 +114,11 @@ export abstract class SocialAbstract { const handleError = this.handleErrors(json || '{}'); + if (handleError?.type === 'retry') { + await timer(5000); + return this.fetch(url, options, identifier, totalRetries + 1); + } + if ( request.status === 401 && (handleError?.type === 'refresh-token' || !handleError) diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 2bdf2889..0b7ada53 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -46,11 +46,18 @@ export class InstagramProvider public override handleErrors(body: string): | { - type: 'refresh-token' | 'bad-body'; + type: 'refresh-token' | 'bad-body' | 'retry'; value: string; } | undefined { + if (body.indexOf('An unknown error occurred') > -1) { + return { + type: 'retry' as const, + value: 'An unknown error occurred, please try again later', + }; + } + if (body.indexOf('REVOKED_ACCESS_TOKEN') > -1) { return { type: 'refresh-token' as const, diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts index 68938b17..523bcda2 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts @@ -30,7 +30,7 @@ export class InstagramStandaloneProvider editor = 'normal' as const; - public override handleErrors(body: string): { type: "refresh-token" | "bad-body"; value: string } | undefined { + public override handleErrors(body: string): { type: "refresh-token" | "bad-body" | "retry"; value: string } | undefined { return instagramProvider.handleErrors(body); } From 3a5e0b14729e79eb299c718ad3d9a66364ca22bb Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 14 Aug 2025 12:07:01 +0700 Subject: [PATCH 114/282] feat: retry --- .../nestjs-libraries/src/integrations/social.abstract.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 778864fb..104cbe77 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -109,16 +109,17 @@ export abstract class SocialAbstract { json.includes('Rate limit') ) { await timer(5000); - return this.fetch(url, options, identifier, totalRetries + 1); + return this.fetch(url, options, identifier, totalRetries + 1, ignoreConcurrency); } const handleError = this.handleErrors(json || '{}'); if (handleError?.type === 'retry') { await timer(5000); - return this.fetch(url, options, identifier, totalRetries + 1); + return this.fetch(url, options, identifier, totalRetries + 1, ignoreConcurrency); } + if ( request.status === 401 && (handleError?.type === 'refresh-token' || !handleError) From 5115d812e593debde2003cbfef8f4287fb9a174c Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 14 Aug 2025 12:07:07 +0700 Subject: [PATCH 115/282] feat: retry --- libraries/nestjs-libraries/src/integrations/social.abstract.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 104cbe77..94bb51b0 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -119,7 +119,6 @@ export abstract class SocialAbstract { return this.fetch(url, options, identifier, totalRetries + 1, ignoreConcurrency); } - if ( request.status === 401 && (handleError?.type === 'refresh-token' || !handleError) From b04510222b7b0ac991b86236f28ebe12fc5e764a Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 26 Aug 2025 22:18:45 +0700 Subject: [PATCH 116/282] feat: fix date for preview --- .../src/app/(app)/(preview)/p/[id]/page.tsx | 17 ++++++++++++----- .../components/preview/render.preview.date.tsx | 6 ++++++ 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 apps/frontend/src/components/preview/render.preview.date.tsx diff --git a/apps/frontend/src/app/(app)/(preview)/p/[id]/page.tsx b/apps/frontend/src/app/(app)/(preview)/p/[id]/page.tsx index e008ba84..85f7aa9a 100644 --- a/apps/frontend/src/app/(app)/(preview)/p/[id]/page.tsx +++ b/apps/frontend/src/app/(app)/(preview)/p/[id]/page.tsx @@ -10,6 +10,16 @@ import utc from 'dayjs/plugin/utc'; import { VideoOrImage } from '@gitroom/react/helpers/video.or.image'; import { CopyClient } from '@gitroom/frontend/components/preview/copy.client'; import { getT } from '@gitroom/react/translation/get.translation.service.backend'; +import dynamicLoad from 'next/dynamic'; + +const RenderPreviewDate = dynamicLoad( + () => + import('@gitroom/frontend/components/preview/render.preview.date').then( + (mod) => mod.RenderPreviewDate + ), + { ssr: false } +); + dayjs.extend(utc); export const metadata: Metadata = { title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Preview`, @@ -91,11 +101,8 @@ export default async function Auth({
      )}
      - {t('publication_date', 'Publication Date:')} - {dayjs - .utc(post[0].publishDate) - .local() - .format('MMMM D, YYYY h:mm A')} + {t('publication_date', 'Publication Date:')}{' '} +
      diff --git a/apps/frontend/src/components/preview/render.preview.date.tsx b/apps/frontend/src/components/preview/render.preview.date.tsx new file mode 100644 index 00000000..8097e5d4 --- /dev/null +++ b/apps/frontend/src/components/preview/render.preview.date.tsx @@ -0,0 +1,6 @@ +import { FC } from 'react'; +import dayjs from 'dayjs'; + +export const RenderPreviewDate: FC<{ date: string }> = ({ date }) => { + return <>{dayjs.utc(date).local().format('MMMM D, YYYY h:mm A')}; +}; From 60da0f6fb1787129af93376de7796273068eb026 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 26 Aug 2025 22:29:22 +0700 Subject: [PATCH 117/282] feat: fix date for preview --- apps/frontend/src/components/preview/render.preview.date.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/frontend/src/components/preview/render.preview.date.tsx b/apps/frontend/src/components/preview/render.preview.date.tsx index 8097e5d4..0ac9d2f8 100644 --- a/apps/frontend/src/components/preview/render.preview.date.tsx +++ b/apps/frontend/src/components/preview/render.preview.date.tsx @@ -1,5 +1,7 @@ import { FC } from 'react'; import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +dayjs.extend(utc); export const RenderPreviewDate: FC<{ date: string }> = ({ date }) => { return <>{dayjs.utc(date).local().format('MMMM D, YYYY h:mm A')}; From 3bea9ee7e9886f2155a8449c7e45219a44cae9bb Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 26 Aug 2025 23:12:56 +0700 Subject: [PATCH 118/282] feat: disconnect prisma --- .../src/components/preview/render.preview.date.tsx | 1 + .../src/database/prisma/prisma.service.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/components/preview/render.preview.date.tsx b/apps/frontend/src/components/preview/render.preview.date.tsx index 0ac9d2f8..cd0a5264 100644 --- a/apps/frontend/src/components/preview/render.preview.date.tsx +++ b/apps/frontend/src/components/preview/render.preview.date.tsx @@ -4,5 +4,6 @@ import utc from 'dayjs/plugin/utc'; dayjs.extend(utc); export const RenderPreviewDate: FC<{ date: string }> = ({ date }) => { + console.log(date); return <>{dayjs.utc(date).local().format('MMMM D, YYYY h:mm A')}; }; diff --git a/libraries/nestjs-libraries/src/database/prisma/prisma.service.ts b/libraries/nestjs-libraries/src/database/prisma/prisma.service.ts index eeb4ebc3..089afc5d 100644 --- a/libraries/nestjs-libraries/src/database/prisma/prisma.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/prisma.service.ts @@ -1,8 +1,8 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() -export class PrismaService extends PrismaClient implements OnModuleInit { +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { constructor() { super({ log: [ @@ -16,6 +16,10 @@ export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { await this.$connect(); } + + async onModuleDestroy() { + await this.$disconnect(); + } } @Injectable() @@ -26,7 +30,6 @@ export class PrismaRepository { } } - @Injectable() export class PrismaTransaction { public model: Pick; From 6a70f2f92d121a849d25b2198ebe972016362329 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 26 Aug 2025 23:22:21 +0700 Subject: [PATCH 119/282] feat: client --- apps/frontend/src/components/preview/render.preview.date.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/frontend/src/components/preview/render.preview.date.tsx b/apps/frontend/src/components/preview/render.preview.date.tsx index cd0a5264..543f0645 100644 --- a/apps/frontend/src/components/preview/render.preview.date.tsx +++ b/apps/frontend/src/components/preview/render.preview.date.tsx @@ -1,3 +1,5 @@ +'use client'; + import { FC } from 'react'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; From 3a1a7500aecc37d5e6d1e8d124847aa918024c0f Mon Sep 17 00:00:00 2001 From: Nevo David <100117126+nevo-david@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:18:40 +0700 Subject: [PATCH 120/282] Update README with new GitHub link and image --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5dd97d86..91350317 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

      - - + + automate

      From cede5216b259f8dcb9a0938cb18befef69fb2f29 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 1 Sep 2025 01:29:22 +0700 Subject: [PATCH 121/282] feat: growchief --- .../src/components/layout/top.menu.tsx | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/layout/top.menu.tsx b/apps/frontend/src/components/layout/top.menu.tsx index 1f8eb6a9..66c78d98 100644 --- a/apps/frontend/src/components/layout/top.menu.tsx +++ b/apps/frontend/src/components/layout/top.menu.tsx @@ -1,7 +1,6 @@ 'use client'; import { FC, ReactNode } from 'react'; -import { usePathname } from 'next/navigation'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; import { useVariables } from '@gitroom/react/helpers/variable.context'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; @@ -129,6 +128,39 @@ export const useMenuItem = () => { ] satisfies MenuItemInterface[] as MenuItemInterface[]; const secondMenu = [ + { + name: 'GrowChief', + icon: ( + + + + + + ), + path: 'https://growchief.com', + role: ['ADMIN', 'SUPERADMIN', 'USER'], + requireBilling: true, + }, { name: t('affiliate', 'Affiliate'), icon: ( @@ -237,7 +269,7 @@ export const useMenuItem = () => { ), path: '/settings', - role: ['ADMIN', "USER", 'SUPERADMIN'], + role: ['ADMIN', 'USER', 'SUPERADMIN'], }, ] satisfies MenuItemInterface[] as MenuItemInterface[]; From 5c6617da2aa6bd44d7a2f73ff0486bd40f3b2daf Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 1 Sep 2025 10:43:17 +0700 Subject: [PATCH 122/282] feat: fix gt and lt --- libraries/helpers/src/utils/strip.html.validation.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts index 7ad3591c..16f97042 100644 --- a/libraries/helpers/src/utils/strip.html.validation.ts +++ b/libraries/helpers/src/utils/strip.html.validation.ts @@ -207,7 +207,9 @@ export const stripHtmlValidation = ( .replace(/ /gi, ' ') .replace(/^]*>/i, '') .replace(/]*>/gi, '\n') - .replace(/<\/p>/gi, ''); + .replace(/<\/p>/gi, '') + .replace(/>/gi, '>') + .replace(/</gi, '<') if (none) { return striptags(html); From baa2f7a74dcc4687fdf8f5f98fc933cead2b1ed2 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 1 Sep 2025 10:47:13 +0700 Subject: [PATCH 123/282] feat: api --- apps/backend/src/public-api/public.api.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/backend/src/public-api/public.api.module.ts b/apps/backend/src/public-api/public.api.module.ts index 73f85ebd..6610735a 100644 --- a/apps/backend/src/public-api/public.api.module.ts +++ b/apps/backend/src/public-api/public.api.module.ts @@ -34,3 +34,4 @@ export class PublicApiModule implements NestModule { consumer.apply(PublicAuthMiddleware).forRoutes(...authenticatedController); } } + From bcc26b6676d618995cc5258d2feb86b5e2b5b103 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 1 Sep 2025 10:48:46 +0700 Subject: [PATCH 124/282] feat: main --- apps/workers/src/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/workers/src/main.ts b/apps/workers/src/main.ts index dab28879..e2240fe1 100644 --- a/apps/workers/src/main.ts +++ b/apps/workers/src/main.ts @@ -22,3 +22,4 @@ async function bootstrap() { } bootstrap(); + From 477a835d24f010fcf42973e24973ed2504300c2d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 1 Sep 2025 10:48:51 +0700 Subject: [PATCH 125/282] feat: main --- apps/workers/src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/workers/src/main.ts b/apps/workers/src/main.ts index e2240fe1..dab28879 100644 --- a/apps/workers/src/main.ts +++ b/apps/workers/src/main.ts @@ -22,4 +22,3 @@ async function bootstrap() { } bootstrap(); - From 46f9d2645a977048283df824d500c84c6496ff3d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 1 Sep 2025 10:49:03 +0700 Subject: [PATCH 126/282] feat: main --- apps/cron/src/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/cron/src/main.ts b/apps/cron/src/main.ts index c22bce19..99577af4 100644 --- a/apps/cron/src/main.ts +++ b/apps/cron/src/main.ts @@ -10,3 +10,4 @@ async function bootstrap() { } bootstrap(); + From aedfe24a0b73b110b8b977decfd384ebbdddc539 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 1 Sep 2025 10:49:07 +0700 Subject: [PATCH 127/282] feat: main --- apps/cron/src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/cron/src/main.ts b/apps/cron/src/main.ts index 99577af4..c22bce19 100644 --- a/apps/cron/src/main.ts +++ b/apps/cron/src/main.ts @@ -10,4 +10,3 @@ async function bootstrap() { } bootstrap(); - From ec2fd4d1c2dc9cbc4142e5b1f4cad4a14eb704c4 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 1 Sep 2025 10:50:15 +0700 Subject: [PATCH 128/282] feat: deploy worker --- apps/workers/src/app/plugs.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/workers/src/app/plugs.controller.ts b/apps/workers/src/app/plugs.controller.ts index f730323c..a7d1134b 100644 --- a/apps/workers/src/app/plugs.controller.ts +++ b/apps/workers/src/app/plugs.controller.ts @@ -18,7 +18,7 @@ export class PlugsController { return await this._integrationService.processPlugs(data); } catch (err) { console.log( - "Unhandled error, let's avoid crashing the plugs worker", + "Unhandled error, let's avoid crashing the plug worker", err ); } From 93378ded4c6cf132a3def1a952f58d2aaa091317 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 1 Sep 2025 10:50:37 +0700 Subject: [PATCH 129/282] feat: missing jobs --- apps/cron/src/tasks/check.missing.queues.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/cron/src/tasks/check.missing.queues.ts b/apps/cron/src/tasks/check.missing.queues.ts index 6c9def7f..0664676c 100644 --- a/apps/cron/src/tasks/check.missing.queues.ts +++ b/apps/cron/src/tasks/check.missing.queues.ts @@ -28,6 +28,7 @@ export class CheckMissingQueues { ) ).filter((p) => !p.isJob); + for (const job of notExists) { this._workerServiceProducer.emit('post', { id: job.id, From c11830551c653c82b6bf341ad68a0c82bb03834b Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 3 Sep 2025 16:52:22 +0700 Subject: [PATCH 130/282] feat: fix append --- apps/frontend/src/components/new-launch/store.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/components/new-launch/store.ts b/apps/frontend/src/components/new-launch/store.ts index 754c833d..d7b03718 100644 --- a/apps/frontend/src/components/new-launch/store.ts +++ b/apps/frontend/src/components/new-launch/store.ts @@ -11,7 +11,7 @@ import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone'; interface Values { id: string; content: string; - media: { id: string; path: string, thumbnail?: string }[]; + media: { id: string; path: string; thumbnail?: string }[]; } interface Internal { @@ -504,7 +504,9 @@ export const useLaunchStore = create()((set) => ({ ? { ...item, integrationValue: item.integrationValue.map((v, i) => - i === index ? { ...v, media: [...v.media, ...media] } : v + i === index + ? { ...v, media: [...(v?.media || []), ...media] } + : v ), } : item @@ -516,7 +518,9 @@ export const useLaunchStore = create()((set) => ({ ) => set((state) => ({ global: state.global.map((item, i) => - i === index ? { ...item, media: [...item.media, ...media] } : item + i === index + ? { ...item, media: [...(item?.media || []), ...media] } + : item ), })), setPostComment: (postComment: PostComment) => From 4bfef6badeb9c9fac55a023f3919ae73329ca268 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 4 Sep 2025 23:55:59 +0700 Subject: [PATCH 131/282] fix: if flatmap is empty --- .../src/components/media/new.uploader.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/apps/frontend/src/components/media/new.uploader.tsx b/apps/frontend/src/components/media/new.uploader.tsx index 850fdc84..febe58fe 100644 --- a/apps/frontend/src/components/media/new.uploader.tsx +++ b/apps/frontend/src/components/media/new.uploader.tsx @@ -88,18 +88,10 @@ export function useUppyUploader(props: { // Expand generic types to specific ones const expandedTypes = allowedTypes.flatMap((type) => { if (type === 'image/*') { - return [ - 'image/png', - 'image/jpeg', - 'image/jpg', - 'image/gif', - ]; + return ['image/png', 'image/jpeg', 'image/jpg', 'image/gif']; } if (type === 'video/*') { - return [ - 'video/mp4', - 'video/mpeg', - ]; + return ['video/mp4', 'video/mpeg']; } return [type]; }); @@ -214,12 +206,11 @@ export function useUppyUploader(props: { return; } - console.log(result); if (transloadit.length > 0) { // @ts-ignore const allRes = result.transloadit[0].results; const toSave = uniq( - allRes[Object.keys(allRes)[0]].flatMap((item: any) => + (allRes[Object.keys(allRes)[0]] || []).flatMap((item: any) => item.url.split('/').pop() ) ); From 1ed1de5925c5b894386bdd24ae73147315a06c86 Mon Sep 17 00:00:00 2001 From: Mo Date: Sat, 6 Sep 2025 02:58:33 +0700 Subject: [PATCH 132/282] logo jsx attribute fix --- apps/frontend/src/components/new-layout/logo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/new-layout/logo.tsx b/apps/frontend/src/components/new-layout/logo.tsx index dfa1481d..3c9113ca 100644 --- a/apps/frontend/src/components/new-layout/logo.tsx +++ b/apps/frontend/src/components/new-layout/logo.tsx @@ -17,7 +17,7 @@ export const Logo = () => { Date: Sat, 6 Sep 2025 14:08:16 +0400 Subject: [PATCH 133/282] =?UTF-8?q?Fully=20translated=20into=20Georgian=20?= =?UTF-8?q?-=20=E1=83=A1=E1=83=A0=E1=83=A3=E1=83=9A=E1=83=90=E1=83=93=20?= =?UTF-8?q?=E1=83=92=E1=83=90=E1=83=93=E1=83=90=E1=83=97=E1=83=90=E1=83=A0?= =?UTF-8?q?=E1=83=92=E1=83=9B=E1=83=9C=E1=83=98=E1=83=9A=E1=83=98=20?= =?UTF-8?q?=E1=83=A5=E1=83=90=E1=83=A0=E1=83=97=E1=83=A3=E1=83=9A=E1=83=90?= =?UTF-8?q?=E1=83=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../locales/ka_ge/translation.json | 505 ++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 libraries/react-shared-libraries/src/translation/locales/ka_ge/translation.json diff --git a/libraries/react-shared-libraries/src/translation/locales/ka_ge/translation.json b/libraries/react-shared-libraries/src/translation/locales/ka_ge/translation.json new file mode 100644 index 00000000..e8614579 --- /dev/null +++ b/libraries/react-shared-libraries/src/translation/locales/ka_ge/translation.json @@ -0,0 +1,505 @@ +{ + "calendar": "კალენდარი", + "webhooks": "ვებჰუქები", + "webhooks_are_a_way_to_get_notified_when_something_happens_in_postiz_via_an_http_request": "ვებჰუქები საშუალებას გაძლევთ მიიღოთ შეტყობინება, როცა Postiz-ში რაიმე ხდება HTTP მოთხოვნის მეშვეობით.", + "name": "სახელი", + "url": "ბმული", + "edit": "რედაქტირება", + "delete": "წაშლა", + "add_a_webhook": "ვებჰუქის დამატება", + "save": "შენახვა", + "send_test": "ტესტის გაგზავნა", + "select_role": "როლის არჩევა", + "video_made_with_ai": "ვიდეო შექმნილია AI-ის მეშვეობით", + "please_add_at_least": "გთხოვთ დაამატოთ მინიმუმ 20 სიმბოლო", + "send_invitation_via_email": "მოწვევა გადაიგზავნოს ელფოსტით?", + "global_settings": "გლობალური პარამეტრები", + "copy_id": "არხის ID-ის კოპირება", + "team_members": "გუნდის წევრები", + "invite_your_assistant_or_team_member_to_manage_your_account": "მოიწვიეთ ასისტენტი ან გუნდის წევრი თქვენი ანგარიშის სამართავად", + "remove": "წაშლა", + "add_another_member": "სხვა წევრის დამატება", + "signatures": "ხელმოწერები", + "you_can_add_signatures_to_your_account_to_be_used_in_your_posts": "შეგიძლიათ დაამატოთ ხელმოწერები, რომლებიც გამოყენებული იქნება თქვენს პოსტებში.", + "content": "კონტენტი", + "auto_add": "ავტომატურად დამატება?", + "actions": "ქმედებები", + "use_signature": "ხელმოწერის გამოყენება", + "add_a_signature": "ხელმოწერის დამატება", + "no": "არა", + "yes": "კი", + "your_git_repository": "თქვენი Git რეპოზიტორია", + "connect_your_github_repository_to_receive_updates_and_analytics": "დააკავშირეთ თქვენი GitHub რეპოზიტორია განახლებებისა და ანალიტიკის მისაღებად", + "connected": "დაკავშირებულია:", + "disconnect": "გათიშვა", + "connect_your_repository": "რეპოზიტორიის დაკავშირება", + "cancel": "გაუქმება", + "connect": "დაკავშირება", + "public_api": "საჯარო API", + "check_n8n": "იხილეთ ჩვენი N8N მორგებული node Postiz-ისთვის.", + "use_postiz_api_to_integrate_with_your_tools": "გამოიყენეთ Postiz API თქვენი ინსტრუმენტების ინტეგრაციისთვის.", + "read_how_to_use_it_over_the_documentation": "იხილეთ დეტალები დოკუმენტაციაში.", + "reveal": "ჩვენება", + "copy_key": "გასაღების კოპირება", + "mcp": "MCP", + "connect_your_mcp_client_to_postiz_to_schedule_your_posts_faster": "დააკავშირეთ MCP-კლიენტი Postiz-სთან პოსტების უფრო სწრაფად დასაგეგმად!", + "share_with_a_client": "კლიენტთან გაზიარება", + "post": "პოსტი", + "comments": "კომენტარები", + "user": "მომხმარებელი", + "login_register_to_add_comments": "კომენტარის დასამატებლად გაიარეთ ავტორიზაცია ან რეგისტრაცია", + "status": "სტატუსი:", + "there_are_not_plugs_matching_your_channels": "თქვენს არხებთან შესაბამისი მოდულები ვერ მოიძებნა", + "you_have_to_add_x_or_linkedin_or_threads": "უნდა დაამატოთ: X, LinkedIn ან Threads", + "go_to_the_calendar_to_add_channels": "გადადით კალენდარში არხების დასამატებლად", + "channels": "არხები", + "activate": "აქტივაცია", + "this_channel_needs_to_be_refreshed": "ამ არხს განახლება სჭირდება,", + "click_here_to_refresh": "დააჭირეთ აქ განახლებისთვის", + "can_t_show_analytics_yet": "ჯერ ანალიტიკა მიუწვდომელია", + "you_have_to_add_social_media_channels": "დაამატეთ სოციალური მედიის არხები", + "supported": "მხარდაჭერილია:", + "step": "ნაბიჯი", + "skip_onboarding": "გაშვების გამოტოვება", + "onboarding": "გაშვება", + "next": "შემდეგი", + "you_are_done_from_here_you_can": "მზადაა! აქედან შეგიძლიათ:", + "view_analytics": "ანალიტიკის ნახვა", + "schedule_a_new_post": "ახალი პოსტის დაგეგმვა", + "to_sell_posts_you_would_have_to": "პოსტების გასაყიდად საჭიროა:", + "1_connect_at_least_one_channel": "1. მინიმუმ ერთი არხის დაკავშირება", + "2_connect_you_bank_account": "2. საბანკო ანგარიშის დაკავშირება", + "go_back_to_connect_channels": "დაბრუნდით არხების დასაკავშირებლად", + "move_to_the_seller_page_to_connect_you_bank": "გადადით გამყიდველის გვერდზე საბანკო ანგარიშის დასაკავშირებლად", + "connect_channels": "არხების დაკავშირება", + "connect_your_social_media_and_publishing_websites_channels_to_schedule_posts_later": "დააკავშირეთ სოციალური მედიისა და გამოქვეყნების არხები, რომ შემდეგ დაგეგმოთ პოსტები", + "social": "სოციალური", + "publishing_platforms": "გამოქვეყნების პლატფორმები", + "no_channels": "არხები ჯერ არ არის", + "connect_your_accounts": "დააბმითეთ თქვენი ანგარიშები — დაგეგმვა, გამოქვეყნება და ანალიზი ერთ სივრცეში.", + "notifications": "შეტყობინებები", + "no_notifications": "შეტყობინებები არ არის", + "send_message": "შეტყობინების გაგზავნა", + "mar_28": "მარტი 28", + "there_are_no_messages_yet": "ჯერ არ არის შეტყობინებები.", + "checkout_the_marketplace": "ეწვიეთ მარკეტპლეისს", + "go_to_marketplace": "გადასვლა მარკეტპლეისში", + "all_messages": "ყველა შეტყობინება", + "previous": "წინა", + "select_or_upload_pictures_maximum_5_at_a_time": "აირჩიეთ ან ატვირთეთ სურათები (მაქს. 5 ერთდროულად)", + "you_can_also_drag_drop_pictures": "ასევე შეგიძლიათ გადაათრიოთ და ჩააგდოთ სურათები", + "you_don_t_have_any_assets_yet": "ჯერ არ გაქვთ ასეტები", + "click_the_button_below_to_upload_one": "დააჭირეთ ქვედა ღილაკს ატვირთვისთვის", + "add_selected_media": "არჩეული მედიის დამატება", + "insert_media": "მედიის ჩასმა", + "design_media": "მედიის დიზაინი", + "select": "არჩევა", + "editor": "რედაქტორი", + "clear": "გასუფთავება", + "order_completed": "შეკვეთა დასრულდა", + "the_order_has_been_completed": "შეკვეთა დასრულებულია", + "post_has_been_published": "პოსტი გამოქვეყნდა", + "url_1": "ბმული:", + "new_offer": "ახალი შეთავაზება", + "platform": "პლატფორმა", + "posts": "პოსტები", + "pay_accept_offer": "გადახდა და შეთავაზების მიღება", + "accepted": "მიღებულია", + "post_draft": "პოსტის მონახაზი", + "revision_needed": "საჭიროა გადახედვა", + "approve": "დამტკიცება", + "preview": "გადახედვა", + "revision_requested": "მოთხოვნილია გადახედვა", + "accepted_1": "მიღებულია", + "cancelled_by_the_seller": "გაუქმდა გამყიდველის მიერ", + "please_select_your_country_where_your_business_is": "აირჩიეთ თქვენი ბიზნესის ქვეყანა", + "select_country": "--აირჩიეთ ქვეყანა--", + "connect_bank_account": "საბანკო ანგარიშის დაკავშირება", + "seller_mode": "გამყიდველის რეჟიმი", + "active": "აქტიური", + "details": "დეტალები", + "audience_size": "აუდიტორიის ზომა", + "add_another_platform": "სხვა პლატფორმის დამატება", + "send_an_offer_for": "შეთავაზების გაგზავნა $", + "complete_order_and_pay_early": "შეკვეთის დასრულება და გადახდა", + "order_in_progress": "შეკვეთა პროცესშია", + "create_a_new_offer": "ახალი შეთავაზების შექმნა", + "orders": "შეკვეთები", + "price": "ფასი", + "state": "სტატუსი", + "showing": "ნაჩვენებია", + "to": "მდე", + "from": "დან", + "results": "შედეგები", + "content_writer": "კონტენტის ავტორი", + "influencer": "ინფლუენსერი", + "request_service": "მომსახურების მოთხოვნა", + "the_marketplace_is_not_opened_yet": "მარკეტპლეისი ჯერ არ არის გახსნილი", + "check_again_soon": "შეამოწმეთ მალე!", + "filter": "ფილტრი", + "result": "შედეგი", + "seller": "გამყიდველი", + "buyer": "მყიდველი", + "discord_support": "Discord მხარდაჭერა", + "teams": "გუნდები", + "webhooks_1": "ვებჰუქები", + "auto_post": "ავტო-პოსტი", + "logout_from": "გასვლა ანგარიშიდან", + "join_10000_entrepreneurs_who_use_postiz": "შეუერთდით 10,000+ მეწარმეს, ვინც იყენებს Postiz-ს", + "to_manage_all_your_social_media_channels": "ყველა სოციალური არხის სამართავად", + "100_no_risk_trial": "100% რისკის გარეშე საცდელი პერიოდი", + "pay_nothing_for_the_first_7_days": "პირველი 7 დღე უფასოა", + "cancel_anytime_hassle_free": "გაუქმება jederzeit, უპრობლემოდ", + "add_free_subscription": "-- უფასო გამოწერის დამატება --", + "currently_impersonating": "ამჟამად იმპერსონაციაა", + "user_1": "მომხმარებელი:", + "drag_n_drop_some_files_here": "გადაათრიეთ ფაილები აქ", + "add_time_slot": "დროის სლოტის დამატება", + "add_slot": "სლოტის დამატება", + "cancel_publication": "გამოქვეყნების გაუქმება", + "statistics": "სტატისტიკა", + "loading": "იტვირთება", + "short_link": "მოკლე ბმული", + "original_link": "ორიგინალი ბმული", + "clicks": "კლიკები", + "selected_customer": "არჩეული კლიენტი", + "customer": "კლიენტი:", + "repeat_post_every": "პოსტის გამეორება ყოველ...", + "use_this_media": "ამ მედიის გამოყენება", + "create_new_post": "პოსტის შექმნა", + "update_post": "არსებული პოსტის განახლება", + "merge_comments_into_one_post": "კომენტარების გაერთიანება ერთ პოსტად", + "accounts_that_will_engage": "ანგარიშები, რომლებიც ჩაერთვებიან:", + "day": "დღე", + "week": "კვირა", + "month": "თვე", + "remove_from_customer": "კლიენტიდან მოხსნა", + "show_more": "+ მეტი", + "show_less": "- ნაკლები", + "upload": "ატვირთვა", + "ai": "AI", + "add_channel": "არხის დამატება", + "add_platform": "პლატფორმის დამატება", + "articles": "სტატიები", + "add_comment": "კომენტარის დამატება", + "add_post": "პოსტის დამატება თრედში", + "add_comment_or_post": "კომენტარის / პოსტის დამატება", + "you_are_in_global_editing_mode": "გლობალური რედაქტირების რეჟიმში ხართ", + "the_post_should_be_at_least_6_characters_long": "პოსტი უნდა შეიცავდეს მინიმუმ 6 სიმბოლოს", + "are_you_sure_you_want_to_delete_post": "დარწმუნებული ხართ, რომ წაშალოთ ეს პოსტი?", + "post_deleted_successfully": "პოსტი წარმატებით წაიშალა", + "delete_post": "პოსტს წაშლა", + "save_as_draft": "მონახაზად შენახვა", + "post_now": "გამოქვეყნება ახლა", + "please_add": "დაამატეთ, გთხოვთ", + "to_your_telegram_group_channel_and_click_here": "თქვენს Telegram ჯგუფში/არხში და შემდეგ დააჭირეთ აქ:", + "connect_telegram": "Telegram-ის დაკავშირება", + "please_add_the_following_command_in_your_chat": "დაამატეთ ჩატში შემდეგი ბრძანება:", + "copy": "კოპირება", + "settings": "პარამეტრები", + "integrations": "ინტეგრაციები", + "add_integration": "ინტეგრაციის დამატება", + "you_are_now_editing_only": "ახლა რედაქტირებ მხოლოდ", + "tag_a_company": "მიანიჭე ტეგი კომპანიას", + "video_length_is_invalid_must_be_up_to": "ვიდეოს სიგრძე არასწორია, უნდა იყოს მაქსიმუმ", + "seconds": "წამი", + "this_feature_available_only_for_photos": "ეს ფუნქცია ხელმისაწვდომია მხოლოდ ფოტოებისთვის — დაემატება ნაგულისხმევი მუსიკა, რომელსაც შემდეგ შეცვლით.", + "allow_user_to": "ნება მიეცით მომხმარებელს:", + "your_video_will_be_labeled_promotional": "თქვენი ვიდეო მოინათლება როგორც \"რეკლამიური კონტენტი\".", + "this_cannot_be_changed_once_posted": "ეს ვერ შეიცვლება პოსტის გამოქვეყნების შემდეგ.", + "turn_on_to_disclose_video_promotes": "ჩართეთ, რათა მიუთითოთ, რომ ვიდეო ხელს უწყობს საქონელს ან მომსახურებას ღირებულების სანაცვლოდ. ვიდეო შესაძლოა ასახავდეს თქვენ, მესამე პირს ან ორივეს.", + "you_are_promoting_yourself": "აქვეყნებთ თქვენი ბრენდის რეკლამას.", + "this_video_will_be_classified_brand_organic": "ვიდეო კლასიფიცირდება როგორც Brand Organic.", + "you_are_promoting_another_brand": "აქვეყნებთ სხვა ბრენდის/მესამე მხარის რეკლამას.", + "this_video_will_be_classified_branded_content": "ვიდეო კლასიფიცირდება როგორც Branded Content.", + "by_posting_you_agree_to_tiktoks": "პოსტის გამოქვეყნებით ეთანხმებით TikTok-ის", + "music_usage_confirmation": "მუსიკის გამოყენების დადასტურებას", + "branded_content_policy": "ბრენდირებული კონტენტის პოლიტიკას", + "select_1": "--აირჩიეთ--", + "select_flair": "--აირჩიეთ ფლეარი--", + "link": "ბმული", + "add_subreddit": "Subreddit-ის დამატება", + "please_add_at_least_one_subreddit": "დაამატეთ მინიმუმ ერთი Subreddit", + "add_community": "Community-ის დამატება", + "select_post_type": "აირჩიეთ პოსტის ტიპი...", + "we_couldn_t_find_any_business_connected_to_your_linkedin_page": "LinkedIn გვერდს მიერთებული ბიზნესი ვერ ვიპოვეთ.", + "please_close_this_dialog_create_a_new_page_and_add_a_new_channel_again": "დახურეთ ეს ფანჯარა, შექმენით ახალი გვერდი და თავიდან დაამატეთ არხი.", + "select_linkedin_page": "აირჩიეთ LinkedIn გვერდი:", + "we_couldn_t_find_any_business_connected_to_the_selected_pages": "არჩეულ გვერდებს მიერთებული ბიზნესი ვერ ვიპოვეთ.", + "we_recommend_you_to_connect_all_the_pages_and_all_the_businesses": "გირჩევთ დააკავშიროთ ყველა გვერდი და ყველა ბიზნესი.", + "please_close_this_dialog_delete_your_integration_and_add_a_new_channel_again": "დახურეთ ეს ფანჯარა, წაშალეთ ინტეგრაცია და თავიდან დაამატეთ არხი.", + "select_instagram_account": "აირჩიეთ Instagram ანგარიში:", + "select_page": "გვერდის არჩევა:", + "generate_image_with_ai": "სურათის გენერირება AI-ით", + "reconnect_channel": "არხის ხელახლა დაკავშირება", + "update_credentials": "მომართვების განახლება", + "additional_settings": "დამატებითი პარამეტრები", + "change_bot": "ბოტის შეცვლა", + "move_add_to_customer": "გადატანა / დამატება კლიენტზე", + "edit_time_slots": "დროის სლოტების რედაქტირება", + "enable_channel": "არხის ჩართვა", + "disable_channel": "არხის გამორთვა", + "add": "დამატება", + "short_post": "მოკლე პოსტი", + "long_post": "გრძელი პოსტი", + "a_thread_with_short_posts": "თრედი მოკლე პოსტებით", + "a_thread_with_long_posts": "თრედი გრძელი პოსტებით", + "personal_voice_i_am_happy_to_announce": "პირადი ხმით (\"მიხარია განცხადება...\")", + "company_voice_we_are_happy_to_announce": "კომპანიის ხმით (\"მოხარულები ვართ, რომ ვაცხადებთ...\")", + "generate": "გენერირება", + "generate_posts": "პოსტების გენერირება", + "purchase_a_life_time_pro_account_with_sol_199": "შეიძინეთ სამუდამო PRO ანგარიში SOL-ით ($199). გთხოვთ გაითვალისწინოთ, რომ ანაზღაურება არ ხდება.", + "purchase_now": "ყიდვა ახლა", + "pay_today": "გადახდა დღეს", + "we_are_sorry_to_see_you_go": "ანანთაით ხართ, რომ მიდიხართ :(", + "would_you_mind_shortly_tell_us_what_we_could_have_done_better": "გვისურვებდით მოკლედ გვეცნობრებინათ, რა შეგვეძლო გაგვეკეთებინა უკეთ?", + "cancel_subscription": "გამოწერის გაუქმება", + "plans": "გეგმები", + "monthly": "თვიური", + "yearly": "წლიური", + "reactivate_subscription": "გამოწერის ხელახლა აქტივაცია", + "update_payment_method_invoices_history": "გადახდის მეთოდის განახლება / ინვოისების ისტორია", + "cancel_subscription_1": "გამოწერის გაუქმება", + "your_subscription_will_be_canceled_at": "თქვენი გამოწერა გაუქმდება თარიღზე:", + "you_will_never_be_charged_again": "სხვა აღარ ჩამოგეჭრებათ თანხა", + "current_package": "მიმდინარე პაკეტი:", + "next_package": "შემდეგი პაკეტი:", + "claim": "მოთხოვნა", + "frequently_asked_questions": "ხშირად დასმული კითხვები", + "autopost": "ავტოპოსტი", + "autopost_can_automatically_posts_your_rss_new_items_to_social_media": "ავტოპოსტი ავტომატურად გამოაქვეყნებს თქვენს RSS-ის ახალ ჩანაწერებს სოციალურ მედია არხებზე", + "title": "სათაური", + "add_an_autopost": "ავტოპოსტის დამატება", + "post_content": "პოსტის შინაარსი", + "sign_up": "რეგისტრაცია", + "or": "ან", + "by_registering_you_agree_to_our": "რეგისტრაციით ეთანხმებით ჩვენს", + "and": "და", + "terms_of_service": "გამოყენების პირობებს", + "privacy_policy": "კონფიდენციალურობის პოლიტიკას", + "create_account": "ანგარიშის შექმნა", + "already_have_an_account": "უკვე გაქვთ ანგარიში?", + "sign_in": "შესვლა", + "sign_in_1": "შესვლა", + "don_t_have_an_account": "არ გაქვთ ანგარიში?", + "forgot_password": "დაგავიწყდათ პაროლი", + "forgot_password_1": "პაროლის აღდგენა", + "send_password_reset_email": "პაროლის აღდგენის წერილის გაგზავნა", + "go_back_to_login": "დაბრუნება შესვლაზე", + "we_have_send_you_an_email_with_a_link_to_reset_your_password": "გაგიგზავნეთ ელფოსტა პაროლის აღდგენის ბმულით.", + "change_password": "პაროლის შეცვლა", + "we_successfully_reset_your_password_you_can_now_login_with_your": "პაროლი წარმატებით აღდგა. შეგიძლიათ შეხვიდეთ თქვენი", + "click_here_to_go_back_to_login": "დააჭირეთ აქ შესვლაზე დასაბრუნებლად", + "activate_your_account": "ანგარიშის გააქტიურება", + "thank_you_for_registering": "გმადლობთ რეგისტრაციისთვის!", + "please_check_your_email_to_activate_your_account": "გთხოვთ, გადაამოწმოთ ელფოსტა ანგარიშის გასააქტიურებლად.", + "sign_in_with": "შესვლა შემდეგით", + "continue_with_google": "გაგრძელება Google-ით", + "sign_in_with_github": "შესვლა GitHub-ით", + "continue_with_farcaster": "გაგრძელება Farcaster-ით", + "continue_with_your_wallet": "გაგრძელება საფულით", + "stars_per_day": "ვარსკვლავები დღეში", + "media": "მედია", + "check_launch": "გაშვების გადამოწმება", + "load_your_github_repository_from_settings_to_see_analytics": "ჩატვირთეთ GitHub რეპოზიტორია პარამეტრებიდან ანალიტიკის სანახავად", + "stars": "ვარსკვლავები", + "processing_stars": "ვარსკვლავების დამუშავება...", + "forks": "ფორკები", + "registration_is_disabled": "რეგისტრაცია გათიშულია", + "login_instead": "შემოსვლა", + "gitroom": "Gitroom", + "select_a_conversation_and_chat_away": "აირჩიეთ საუბარი და დაიწყეთ ჩათი", + "adding_channel_redirecting_you": "არხის დამატება... გადამისამართება", + "could_not_add_provider": "პროვაიდერის დამატება ვერ მოხერხდა.", + "you_are_being_redirected_back": "ბრუნდებით უკან", + "we_are_experiencing_some_difficulty_try_to_refresh_the_page": "ამჟამად ჭირს მუშაობა, სცადეთ გვერდის განახლება", + "post_not_found": "პოსტი ვერ მოიძებნა", + "publication_date": "გამოქვეყნების თარიღი:", + "analytics": "ანალიტიკა", + "launches": "გაშვებები", + "plugs": "მოდულები", + "billing": "ბილინგი", + "affiliate": "აფილியேიტი", + "monday": "ორშაბათი", + "tuesday": "სამშაბათი", + "wednesday": "ოთხშაბათი", + "thursday": "ხუთშაბათი", + "friday": "პარასკევი", + "saturday": "შაბათი", + "sunday": "კვირა", + "can_t_change_date_remove_post_from_publication": "თარიღის შეცვლა შეუძლებელია — მოხსენით პოსტი გამოქვეყნებიდან", + "predicted_github_trending_change": "პროგნოზირებული ცვლილება GitHub Trending-ში", + "duplicate_post": "პოსტის დუბლირება", + "preview_post": "პოსტზე წინასწარი გადახედვა", + "post_statistics": "პოსტის სტატისტიკა", + "draft": "მონახაზი", + "week_number": "კვირა {{number}}", + "top_title_edit_webhook": "ვებჰუქის რედაქტირება", + "top_title_add_webhook": "ვებჰუქის დამატება", + "top_title_oh_no": "ო, არა", + "top_title_auto_plug": "ავტომოდული: {{title}}", + "top_title_edit_autopost": "ავტოპოსტის რედაქტირება", + "top_title_add_autopost": "ავტოპოსტის დამატება", + "top_title_send_a_new_offer": "ახალი შეთავაზების გაგზავნა", + "top_title_media_library": "მედიალაიბრარი", + "top_title_add_signature": "ხელმოწერის დამატება", + "top_title_send_a_message_to": "შეტყობინების გაგზავნა: {{name}}", + "top_title_configure_provider": "პროვაიდერის კონფიგურაცია", + "top_title_add_member": "წევრის დამატება", + "top_title_change_bot_picture": "ბოტის სურათის შეცვლა", + "top_title_create_a_new_tag": "ახალი ტეგის შექმნა", + "top_title_select_company": "კომპანიის არჩევა", + "top_title_additional_settings": "დამატებითი პარამეტრები", + "top_title_time_table_slots": "დროის ცხრილი / სლოტები", + "top_title_design_media": "მედიის დიზაინი", + "top_title_edit_post": "პოსტის რედაქტირება", + "top_title_create_post": "ახალი პოსტის შექმნა", + "top_title_move__add_to_customer": "გადატანა / დამატება კლიენტზე", + "top_title_add_api_key_for": "API გასაღები — {{name}}", + "top_title_instance_url": "ინსტანციის URL", + "top_title_custom_url": "მორგებული URL", + "top_title_add_channel": "არხის დამატება", + "top_title_add_telegram": "Telegram-ის დამატება", + "top_title_add_wrapcast": "Wrapcast-ის დამატება", + "top_title_comments_for": "კომენტარები — {{date}}", + "top_title_edit_signature": "ხელმოწერის რედაქტირება", + "label_name": "სახელი", + "label_url": "ბმული", + "label_title": "სათაური", + "label_subtitle": "ქვესათაური", + "label_email": "ელ.ფოსტა", + "label_full_name": "სრული სახელი", + "label_password": "პაროლი", + "label_confirm_password": "პაროლის დადასტურება", + "label_api_key": "API გასაღები", + "label_instance_url": "ინსტანციის URL", + "label_custom_url": "მორგებული URL", + "label_feedback": "უკუკავშირი", + "label_bio": "ბიო", + "label_role": "როლი", + "label_country": "ქვეყანა", + "label_audience_size": "აუდიტორიის ზომა ყველა პლატფორმაზე", + "label_pick_time": "დროის არჩევა", + "label_nickname": "მეტსახელი", + "label_write_anything": "დაწერეთ რაც გსურთ", + "label_output_format": "გამოტანის ფორმატი", + "label_add_pictures": "დავამატოთ სურათები?", + "label_hour": "საათი", + "label_minutes": "წუთი", + "label_select_publication": "აირჩიეთ პუბლიკაცია", + "label_canonical_link": "კანონიკური ბმული", + "label_cover_picture": "ქოვერის სურათი", + "label_tags": "ტეგები", + "label_topics": "თემები", + "label_tags_maximum_4": "ტეგები (მაქს. 4)", + "label_attachments": "დანართები", + "label_type": "ტიპი", + "label_thumbnail": "თამბნეილი", + "label_who_can_see_this_video": "ვის შეუძლია ამ ვიდეოს ნახვა?", + "label_content_posting_method": "კონტენტის გამოქვეყნების მეთოდი", + "label_auto_add_music": "მუსიკის ავტომატური დამატება", + "label_duet": "დუეტი", + "label_stitch": "Stitch", + "label_comments": "კომენტარები", + "label_disclose_video_content": "ვიდეოს შინაარსის გამჟღავნება", + "label_your_brand": "თქვენი ბრენდი", + "label_branded_content": "ბრენდირებული კონტენტი", + "label_subreddit": "Subreddit", + "label_flair": "Flair", + "label_media": "მედია", + "label_search_subreddit": "Subreddit-ის ძიება", + "label_delay": "დაყოვნება", + "label_post_type": "პოსტის ტიპი", + "label_collaborators": "თანაავტორები (მაქს. 3) — ანგარიშები არ უნდა იყოს პრაივეტი", + "label_community": "Community", + "label_search_community": "Community-ის ძიება", + "label_channel": "არხი", + "label_search_channel": "არხის ძიება", + "label_select_channel": "აირჩიეთ არხი", + "label_new_password": "ახალი პაროლი", + "label_repeat_password": "პაროლის გამეორება", + "label_platform": "პლატფორმა", + "label_price_per_post": "ფასი ერთ პოსტზე", + "label_integrations": "ინტეგრაციები", + "label_code": "კოდი", + "label_should_sync_last_post": "გავათანაბროთ თუ არა მიმდინარე ბოლო პოსტი?", + "label_when_post": "როდის დავპოსტოთ?", + "label_autogenerate_content": "კონტენტის ავტომატური გენერირება", + "label_generate_picture": "სურათის გენერირება?", + "label_company": "კომპანია", + "label_tag_color": "ტეგის ფერი", + "label_select_board": "ბორდის არჩევა", + "label_select_organization": "ორგანიზაციის არჩევა", + "label_auto_add_signature": "ავტომატურად დავამატოთ ხელმოწერა?", + "enable_color_picker": "ფერების ამრჩევის ჩართვა", + "cancel_the_color_picker": "ფერის ამრჩევის გაუქმება", + "no_content_yet": "კონტენტი ჯერჯერობით არ არის", + "write_your_reply": "დაწერეთ თქვენი პოსტი...", + "add_a_tag": "ტეგის დამატება", + "add_to_calendar": "კალენდარში დამატება", + "select_channels_from_circles": "აირჩიეთ არხები ზემოთ არსებულ წრეებიდან", + "not_matching_order": "შეკვეთას არ ემთხვევა", + "submit_for_order": "გაგზავნა შეკვეთაზე", + "schedule": "დაგეგმვა", + "update": "განახლება", + "attachments": "დანართები", + "tags": "ტეგები", + "public_to_everyone": "ხილულია ყველასთვის", + "mutual_follow_friends": "ურთიერთჩამომყოლები", + "follower_of_creator": "შემქმნელის გამომწერები", + "self_only": "მხოლოდ მე", + "post_content_directly_to_tiktok": "კონტენტის პირდაპირ გამოქვეყნება TikTok-ში", + "upload_content_to_tiktok_without_posting": "კონტენტის ატვირთვა TikTok-ში გამოქვეყნების გარეშე", + "choose_upload_without_posting_description": "აირჩიეთ ატვირთვა გამოქვეყნების გარეშე, თუ გსურთ მასალის გადამოწმება/რედაქტირება TikTok-ის აპში გამოქვეყნებამდე.", + "faq_am_i_going_to_be_charged_by_postiz": "Postiz დამაკისრებს თანხას?", + "faq_to_confirm_credit_card_information_postiz_will_hold": "საკრედიტო ბარათის დადასტურებისთვის Postiz დროებით დაბლოკავს $2-ს და მაშინვე გაათავისუფლებს", + "faq_can_i_trust_postiz": "შემიძლია ვენდო Postiz-ს?", + "faq_postiz_gitroom_is_proudly_open_source": "Postiz არის ღია კოდის! გამჭვირვალე კულტურა — შეგიძლიათ ნახოთ მთელი კოდი ან გამოიყენოთ პირადი პროექტებისთვის. ღია რეპოზიტორიის სანახავად დააჭირეთ აქ.", + "faq_what_are_channels": "რა არის არხები?", + "faq_postiz_gitroom_allows_you_to_schedule_posts": "Postiz გაძლევთ პოსტების დაგეგმვის საშუალებას სხვადასხვა არხებზე — მაგალითად X, Facebook, Instagram, TikTok, YouTube, Reddit, LinkedIn, Dribbble, Threads და Pinterest.", + "faq_what_are_team_members": "რა არის გუნდის წევრები?", + "faq_if_you_have_a_team_with_multiple_members": "თუ თქვენ ჰყავთ გუნდი რამდენიმე წევრით, შეგიძლიათ მოიწვიოთ ისინი workspace-ში, ითანამშრომლოთ პოსტებზე და დაამატონ თავიანთი არხები", + "faq_what_is_ai_auto_complete": "რა არის AI auto-complete?", + "faq_we_automate_chatgpt_to_help_you_write": "ვავტომატებთ ChatGPT-ს, რომ დაგეხმაროთ სოციალური პოსტებისა და სტატიათა წერაში.", + "enter_email": "შეიტანეთ ელფოსტა", + "are_you_sure": "დარწმუნებული ხართ?", + "yes_delete_it": "კი, წაშალე!", + "no_cancel": "არა, გაუქმება!", + "are_you_sure_you_want_to_delete": "ნამდვილად გსურთ წაშალოთ {{name}}?", + "are_you_sure_you_want_to_delete_the_image": "ნამდვილად გსურთ სურათის წაშლა?", + "are_you_sure_you_want_to_logout": "ნამდვილად გსურთ გასვლა?", + "yes_logout": "კი, გამოსვლა", + "are_you_sure_you_want_to_delete_this_slot": "წავშალოთ ეს სლოტი?", + "are_you_sure_you_want_to_delete_this_subreddit": "წავშალოთ ეს Subreddit?", + "are_you_sure_you_want_to_close_the_window": "დავხუროთ ფანჯარა?", + "yes_close": "კი, დახურე", + "link_copied_to_clipboard": "ბმული დაკოპირდა", + "are_you_sure_you_want_to_close_this_modal_all_data_will_be_lost": "დავხუროთ ეს ფანჯარა? (ყველა მონაცემი დაიკარგება)", + "yes_close_it": "კი, დახურე!", + "uploading_pictures": "სურათების ატვირთვა...", + "agent_starting": "აგენტის გაშვება", + "researching_your_content": "თქვენი კონტენტის კვლევა...", + "understanding_the_category": "კატეგორიის გააზრება...", + "finding_the_topic": "თემის ძიება...", + "finding_popular_posts_to_match_with": "პოპულარული პოსტების მოპოვება შესატყვისად...", + "generating_hook": "ჰუკის გენერირება...", + "generating_content": "კონტენტის გენერირება...", + "generating_pictures": "სურათების გენერირება...", + "finding_time_to_post": "საუკეთესო დროის პოვნა...", + "write_anything": "დაწერეთ რაც გსურთ", + "you_can_write_anything_you_want_and_also_add_links_we_will_do_the_research_for_you": "დაწერეთ რაც გინდათ და დაამატეთ ბმულები — კვლევას ჩვენ მოვახდენთ", + "output_format": "გამოტანის ფორმატი", + "add_pictures": "სურათების დამატება?", + "7_days": "7 დღე", + "30_days": "30 დღე", + "90_days": "90 დღე", + "start_7_days_free_trial": "დაიწყე 7-დღიანი უფასო პერიოდი", + "change_language": "ენის შეცვლა", + "that_a_wrap": "დასრულებულია!\n\nთუ მოგეწონა ეს თრედი:\n\n1. გამომყევი @{{username}}\n2. გააზიარეთ ქვემოთ არსებული პოსტი", + "post_as_images_carousel": "გამოქვეყნება სურათების კარუსელად", + "save_set": "სეტის შენახვა", + "separate_post": "თრედის დაყოფა რამდენიმე პოსტად", + "label_who_can_reply_to_this_post": "ვის შეუძლია პასუხის გაცემა ამ პოსტზე?", + "delete_integration": "ინტეგრაციის წაშლა", + "start_writing_your_post": "დაიწყეთ თქვენი პოსტის წერა წინასწარი ხედვისთვის" +} From e2b407cac865a3e1486d4c397bb2e42ad81b5ce8 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Mon, 8 Sep 2025 22:50:48 +0200 Subject: [PATCH 134/282] DartNode --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 91350317..86290d91 100644 --- a/README.md +++ b/README.md @@ -135,3 +135,5 @@ This repository's source code is available under the [AGPL-3.0 license](LICENSE)

      g2

      + +[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") \ No newline at end of file From 18086edd9f8da2de5ffd69296102a1f7d0f73f45 Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Tue, 9 Sep 2025 07:51:37 +0200 Subject: [PATCH 135/282] remove Dartnode --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 86290d91..91350317 100644 --- a/README.md +++ b/README.md @@ -135,5 +135,3 @@ This repository's source code is available under the [AGPL-3.0 license](LICENSE)

      g2

      - -[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") \ No newline at end of file From 43c3af4e35dcf7f1d61d73e15f26002332d83449 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 14 Sep 2025 14:42:12 +0700 Subject: [PATCH 136/282] feat: proxy video response --- .../src/api/routes/public.controller.ts | 58 ++++++++++++++++++- apps/backend/src/mcp/main.mcp.ts | 2 +- .../helpers/media.settings.component.tsx | 6 +- .../src/upload/cloudflare.storage.ts | 49 +++++++++------- .../src/upload/local.storage.ts | 51 ++++++++-------- 5 files changed, 117 insertions(+), 49 deletions(-) diff --git a/apps/backend/src/api/routes/public.controller.ts b/apps/backend/src/api/routes/public.controller.ts index 66238060..3137fc2e 100644 --- a/apps/backend/src/api/routes/public.controller.ts +++ b/apps/backend/src/api/routes/public.controller.ts @@ -1,4 +1,14 @@ -import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Query, + Req, + Res, + StreamableFile, +} from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service'; import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; @@ -11,6 +21,10 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service'; import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments'; +import { Readable, pipeline } from 'stream'; +import { promisify } from 'util'; + +const pump = promisify(pipeline); @ApiTags('Public') @Controller('/public') @@ -136,4 +150,46 @@ export class PublicController { console.log('cryptoPost', body, path); return this._nowpayments.processPayment(path, body); } + + @Get('/stream') + async streamFile( + @Query('url') url: string, + @Res() res: Response, + @Req() req: Request + ) { + if (!url.endsWith('mp4')) { + return res.status(400).send('Invalid video URL'); + } + + const ac = new AbortController(); + const onClose = () => ac.abort(); + req.on('aborted', onClose); + res.on('close', onClose); + + const r = await fetch(url, { signal: ac.signal }); + + if (!r.ok && r.status !== 206) { + res.status(r.status); + throw new Error(`Upstream error: ${r.statusText}`); + } + + const type = r.headers.get('content-type') ?? 'application/octet-stream'; + res.setHeader('Content-Type', type); + + const contentRange = r.headers.get('content-range'); + if (contentRange) res.setHeader('Content-Range', contentRange); + + const len = r.headers.get('content-length'); + if (len) res.setHeader('Content-Length', len); + + const acceptRanges = r.headers.get('accept-ranges') ?? 'bytes'; + res.setHeader('Accept-Ranges', acceptRanges); + + if (r.status === 206) res.status(206); // Partial Content for range responses + + try { + await pump(Readable.fromWeb(r.body as any), res); + } catch (err) { + } + } } diff --git a/apps/backend/src/mcp/main.mcp.ts b/apps/backend/src/mcp/main.mcp.ts index 483ae851..83a05ade 100644 --- a/apps/backend/src/mcp/main.mcp.ts +++ b/apps/backend/src/mcp/main.mcp.ts @@ -50,7 +50,7 @@ export class MainMcp { @McpTool({ toolName: 'POSTIZ_SCHEDULE_POST', zod: { - type: eenum(['draft', 'scheduled']), + type: eenum(['draft', 'schedule']), configId: string(), generatePictures: boolean(), date: string().describe('UTC TIME'), diff --git a/apps/frontend/src/components/launches/helpers/media.settings.component.tsx b/apps/frontend/src/components/launches/helpers/media.settings.component.tsx index 3cf774d1..2a0531e8 100644 --- a/apps/frontend/src/components/launches/helpers/media.settings.component.tsx +++ b/apps/frontend/src/components/launches/helpers/media.settings.component.tsx @@ -5,6 +5,7 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; +import { useVariables } from '@gitroom/react/helpers/variable.context'; const postUrlEmitter = new EventEmitter(); export const MediaSettingsLayout = () => { @@ -97,7 +98,8 @@ export const CreateThumbnail: FC<{ altText?: string; onAltTextChange?: (altText: string) => void; }> = (props) => { - const { onSelect, media, altText, onAltTextChange } = props; + const { onSelect, media } = props; + const { backendUrl } = useVariables(); const videoRef = useRef(null); const canvasRef = useRef(null); const [currentTime, setCurrentTime] = useState(0); @@ -211,7 +213,7 @@ export const CreateThumbnail: FC<{