From 1e2d45f8867065d50fffa57aa2b55cf6a403a12a Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 31 Jul 2025 20:30:26 +0700 Subject: [PATCH 01/23] 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 fdddf2ca2634cdf6dabe4fa0f51c6ce79098f8de Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Thu, 31 Jul 2025 22:53:02 +0200 Subject: [PATCH 02/23] Update copilot-instructions.md --- .github/copilot-instructions.md | 63 +++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 220e7bd7..fbd90264 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -49,5 +49,68 @@ --- +# Logs + +- Where logs are used, ensure Sentry is imported using `import * as Sentry from "@sentry/nextjs"` +- Enable logging in Sentry using `Sentry.init({ enableLogs: true })` +- Reference the logger using `const { logger } = Sentry` +- Sentry offers a `consoleLoggingIntegration` that can be used to log specific console error types automatically without instrumenting the individual logger calls + +## Configuration + +The Sentry initialization needs to be updated to enable the logs feature. + +### Baseline + +```javascript +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: "https://examplePublicKey@o0.ingest.sentry.io/0", + + enableLogs: true, +}); +``` + +### Logger Integration + +```javascript +Sentry.init({ + dsn: "https://examplePublicKey@o0.ingest.sentry.io/0", + integrations: [ + // send console.log, console.error, and console.warn calls as logs to Sentry + Sentry.consoleLoggingIntegration({ levels: ["log", "error", "warn"] }), + ], +}); +``` + +## Logger Examples + +`logger.fmt` is a template literal function that should be used to bring variables into the structured logs. + +```javascript +import * as Sentry from "@sentry/nextjs"; + +const { logger } = Sentry; + +logger.trace("Starting database connection", { database: "users" }); +logger.debug(logger.fmt`Cache miss for user: ${userId}`); +logger.info("Updated profile", { profileId: 345 }); +logger.warn("Rate limit reached for endpoint", { + endpoint: "/api/results/", + isEnterprise: false, +}); +logger.error("Failed to process payment", { + orderId: "order_123", + amount: 99.99, +}); +logger.fatal("Database connection pool exhausted", { + database: "users", + activeConnections: 100, +}); +``` + +--- + For questions or unclear conventions, check the main README or ask for clarification in your PR description. From e665eaaf2c40d511e7fb509b22048a7eb364590b Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Thu, 31 Jul 2025 22:54:54 +0200 Subject: [PATCH 03/23] Update copilot-instructions.md --- .github/copilot-instructions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fbd90264..999efe6d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -66,7 +66,7 @@ The Sentry initialization needs to be updated to enable the logs feature. import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: "https://examplePublicKey@o0.ingest.sentry.io/0", + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, enableLogs: true, }); @@ -76,7 +76,7 @@ Sentry.init({ ```javascript Sentry.init({ - dsn: "https://examplePublicKey@o0.ingest.sentry.io/0", + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, integrations: [ // send console.log, console.error, and console.warn calls as logs to Sentry Sentry.consoleLoggingIntegration({ levels: ["log", "error", "warn"] }), From 2756f28d72f6259dc02685ac10e35522e33b7e14 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 11:14:11 +0700 Subject: [PATCH 04/23] 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 b2d68887ac7c9f82dd5a40481d64af538896b928 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 11:14:27 +0700 Subject: [PATCH 05/23] feat: remove try-catch from pinterest --- .../integrations/social/pinterest.provider.ts | 95 +++++++++---------- 1 file changed, 45 insertions(+), 50 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index 0ce6e1b4..8bfc048a 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -212,57 +212,52 @@ export class PinterestProvider path: m.path, })); - try { - const { id: pId } = await ( - await this.fetch('https://api.pinterest.com/v5/pins', { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - ...(postDetails?.[0]?.settings.link - ? { link: postDetails?.[0]?.settings.link } - : {}), - ...(postDetails?.[0]?.settings.title - ? { title: postDetails?.[0]?.settings.title } - : {}), - description: postDetails?.[0]?.message, - ...(postDetails?.[0]?.settings.dominant_color - ? { dominant_color: postDetails?.[0]?.settings.dominant_color } - : {}), - board_id: postDetails?.[0]?.settings.board, - media_source: mediaId - ? { - source_type: 'video_id', - media_id: mediaId, - cover_image_url: picture?.path, - } - : mapImages?.length === 1 - ? { - source_type: 'image_url', - url: mapImages?.[0]?.path, - } - : { - source_type: 'multiple_image_urls', - items: mapImages, - }, - }), - }) - ).json(); - - return [ - { - id: postDetails?.[0]?.id, - postId: pId, - releaseURL: `https://www.pinterest.com/pin/${pId}`, - status: 'success', + const { id: pId } = await ( + await this.fetch('https://api.pinterest.com/v5/pins', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', }, - ]; - } catch (err) { - console.log(err); - return []; - } + body: JSON.stringify({ + ...(postDetails?.[0]?.settings.link + ? { link: postDetails?.[0]?.settings.link } + : {}), + ...(postDetails?.[0]?.settings.title + ? { title: postDetails?.[0]?.settings.title } + : {}), + description: postDetails?.[0]?.message, + ...(postDetails?.[0]?.settings.dominant_color + ? { dominant_color: postDetails?.[0]?.settings.dominant_color } + : {}), + board_id: postDetails?.[0]?.settings.board, + media_source: mediaId + ? { + source_type: 'video_id', + media_id: mediaId, + cover_image_url: picture?.path, + } + : mapImages?.length === 1 + ? { + source_type: 'image_url', + url: mapImages?.[0]?.path, + } + : { + source_type: 'multiple_image_urls', + items: mapImages, + }, + }), + }) + ).json(); + + return [ + { + id: postDetails?.[0]?.id, + postId: pId, + releaseURL: `https://www.pinterest.com/pin/${pId}`, + status: 'success', + }, + ]; } async analytics( From 449e2acab1d41606896f3645c74fbcaeb31bbe19 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 12:42:21 +0700 Subject: [PATCH 06/23] 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 c76283bca6417fc96e1457b15e076f62f5ac6a0c Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 12:43:00 +0700 Subject: [PATCH 07/23] feat: handle pinterest errors --- .../integrations/social/pinterest.provider.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index 8bfc048a..ef9814ba 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -30,6 +30,24 @@ export class PinterestProvider editor = 'normal' as const; + public override handleErrors(body: string): + | { + type: 'refresh-token' | 'bad-body'; + value: string; + } + | undefined { + + if (body.indexOf('cover_image_url or cover_image_content_type') > -1) { + return { + type: 'bad-body' as const, + value: + 'When uploading a video, you must add also an image to be used as a cover image.', + }; + } + + return undefined; + } + async refreshToken(refreshToken: string): Promise { const { access_token, expires_in } = await ( await this.fetch('https://api.pinterest.com/v5/oauth/token', { From 58abf6eb00f861314476e654ab1f8bc5d4a3d748 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 13:45:52 +0700 Subject: [PATCH 08/23] 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 09/23] 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 10/23] 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 11/23] 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 12/23] 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 13/23] 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 14/23] 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 15/23] 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 16/23] 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 17/23] 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 18/23] 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 19/23] 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 20/23] 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 21/23] 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 22/23] 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 23/23] 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: