From cdbf173ca92af40bc0f42ca97b8a259cfe498d0f Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 8 Jun 2025 15:02:32 +0700 Subject: [PATCH 1/4] feat: carousel --- .../providers/linkedin/linkedin.provider.tsx | 38 +++- .../posts/providers-settings/linkedin.dto.ts | 7 + .../integrations/social/linkedin.provider.ts | 3 +- .../translation/locales/en/translation.json | 3 +- package.json | 1 + pnpm-lock.yaml | 178 +++++++++++++++++- 6 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 libraries/nestjs-libraries/src/dtos/posts/providers-settings/linkedin.dto.ts diff --git a/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx b/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx index 785a0d14..39a42e59 100644 --- a/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx +++ b/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx @@ -1,10 +1,40 @@ import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; -export default withProvider( - null, +import { Checkbox } from '@gitroom/react/form/checkbox'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { LinkedinDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/linkedin.dto'; + +const LinkedInSettings = () => { + const t = useT(); + const { watch, register, formState, control } = useSettings(); + + return ( +
+ +
+ ); +}; +export default withProvider( + LinkedInSettings, undefined, - undefined, - async (posts) => { + LinkedinDto, + async (posts, vals) => { const [firstPost, ...restPosts] = posts; + + if ( + vals.post_as_images_carousel && + (firstPost.length < 2 || + firstPost.some((p) => p.path.indexOf('mp4') > -1)) + ) { + return 'LinkedIn carousel can only be created with 2 or more images and no videos.'; + } + if ( firstPost.length > 1 && firstPost.some((p) => p.path.indexOf('mp4') > -1) diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/linkedin.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/linkedin.dto.ts new file mode 100644 index 00000000..b3840d14 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/linkedin.dto.ts @@ -0,0 +1,7 @@ +import { IsBoolean, IsOptional } from 'class-validator'; + +export class LinkedinDto { + @IsBoolean() + @IsOptional() + post_as_images_carousel: boolean; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 13c6b065..8433b336 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -11,6 +11,7 @@ import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; import { Integration } from '@prisma/client'; import { PostPlug } from '@gitroom/helpers/decorators/post.plug'; +import { LinkedinDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/linkedin.dto'; export class LinkedinProvider extends SocialAbstract implements SocialProvider { identifier = 'linkedin'; @@ -314,7 +315,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { return connectAll.join(''); } - async post( + async post( id: string, accessToken: string, postDetails: PostDetails[], 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 e6616838..9b8adfd7 100644 --- a/libraries/react-shared-libraries/src/translation/locales/en/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/en/translation.json @@ -482,5 +482,6 @@ "90_days": "90 Days", "start_7_days_free_trial": "Start 7 days free trial", "change_language": "Change Language", - "that_a_wrap": "That's a wrap!\n\nIf you enjoyed this thread:\n\n1. Follow me @{{username}} for more of these\n2. RT the tweet below to share this thread with your audience\n" + "that_a_wrap": "That's a wrap!\n\nIf you enjoyed this thread:\n\n1. Follow me @{{username}} for more of these\n2. RT the tweet below to share this thread with your audience\n", + "post_as_images_carousel": "Post as images carousel" } diff --git a/package.json b/package.json index caf9611c..4ed9c5db 100644 --- a/package.json +++ b/package.json @@ -149,6 +149,7 @@ "i18next": "^25.2.1", "i18next-browser-languagedetector": "^8.1.0", "i18next-resources-to-backend": "^1.2.1", + "image-to-pdf": "^3.0.2", "ioredis": "^5.3.2", "json-to-graphql-query": "^2.2.5", "jsonwebtoken": "^9.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fdd83a3..28f9e426 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -324,6 +324,9 @@ importers: i18next-resources-to-backend: specifier: ^1.2.1 version: 1.2.1 + image-to-pdf: + specifier: ^3.0.2 + version: 3.0.2 ioredis: specifier: ^5.3.2 version: 5.6.1 @@ -5552,6 +5555,9 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.3.17': + resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==} + '@swc/helpers@0.5.13': resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} @@ -7182,6 +7188,10 @@ packages: base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -7290,6 +7300,9 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browserify-aes@1.2.0: resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} @@ -8202,6 +8215,10 @@ packages: resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} engines: {node: '>= 0.4'} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -8318,6 +8335,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + diacritics@1.3.0: resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} @@ -8555,6 +8575,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-iterator-helpers@1.2.1: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} @@ -9137,6 +9160,9 @@ packages: debug: optional: true + fontkit@1.9.0: + resolution: {integrity: sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==} + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -9845,6 +9871,9 @@ packages: engines: {node: '>=16.x'} hasBin: true + image-to-pdf@3.0.2: + resolution: {integrity: sha512-6/IQCt4f384zjQ1w8P7FHIN/tF0mau8RbAIydT/+wyfZ1RAb8E2fiKe9t/k0V880h0d3zRpw9Q1bM5AIgVL/4g==} + immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -10453,6 +10482,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + jpeg-exif@1.1.4: + resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} + js-base64@3.7.7: resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} @@ -10932,6 +10964,9 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -11719,10 +11754,12 @@ packages: multer@1.4.4-lts.1: resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==} engines: {node: '>= 6.0.0'} + deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version. multer@1.4.5-lts.2: resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==} engines: {node: '>= 6.0.0'} + deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version. multicast-dns@7.2.5: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} @@ -12248,6 +12285,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} @@ -12370,6 +12410,9 @@ packages: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} engines: {node: '>=0.12'} + pdfkit@0.15.2: + resolution: {integrity: sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==} + peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} @@ -12468,6 +12511,9 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + png-js@1.0.0: + resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -13508,6 +13554,9 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + restructure@2.0.1: + resolution: {integrity: sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==} + retry-axios@2.6.0: resolution: {integrity: sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==} engines: {node: '>=10.7.0'} @@ -14038,6 +14087,10 @@ packages: resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} engines: {node: '>=0.10.0'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + store2@2.14.4: resolution: {integrity: sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==} @@ -14407,6 +14460,9 @@ packages: tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tiny-invariant@1.0.6: resolution: {integrity: sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==} @@ -14813,10 +14869,16 @@ packages: resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} engines: {node: '>=4'} + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + unicode-property-aliases-ecmascript@2.1.0: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unidragger@3.0.1: resolution: {integrity: sha512-RngbGSwBFmqGBWjkaH+yB677uzR95blSQyxq6hYbrQCejH3Mx1nm8DVOuh3M9k2fQyTstWUG5qlgCnNqV/9jVw==} @@ -19540,6 +19602,21 @@ snapshots: - typescript - verdaccio + '@nrwl/js@19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(typescript@5.5.4)': + dependencies: + '@nx/js': 19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(typescript@5.5.4) + transitivePeerDependencies: + - '@babel/traverse' + - '@swc-node/register' + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - debug + - nx + - supports-color + - typescript + - verdaccio + '@nrwl/nest@19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(chokidar@3.5.3)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(ts-node@10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(typescript@5.5.4))(typescript@5.5.4)': dependencies: '@nx/nest': 19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(chokidar@3.5.3)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(ts-node@10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(typescript@5.5.4))(typescript@5.5.4) @@ -19886,7 +19963,7 @@ snapshots: '@babel/preset-env': 7.27.1(@babel/core@7.27.1) '@babel/preset-typescript': 7.27.1(@babel/core@7.27.1) '@babel/runtime': 7.27.1 - '@nrwl/js': 19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(typescript@5.4.5) + '@nrwl/js': 19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(typescript@5.5.4) '@nx/devkit': 19.7.2(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))) '@nx/workspace': 19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)) babel-plugin-const-enum: 1.2.0(@babel/core@7.27.1) @@ -22453,6 +22530,10 @@ snapshots: '@swc/counter@0.1.3': {} + '@swc/helpers@0.3.17': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.13': dependencies: tslib: 2.8.1 @@ -24777,6 +24858,8 @@ snapshots: base-x@5.0.1: {} + base64-js@0.0.8: {} + base64-js@1.5.1: {} base64url@3.0.1: {} @@ -24909,6 +24992,10 @@ snapshots: brorand@1.1.0: {} + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + browserify-aes@1.2.0: dependencies: buffer-xor: 1.0.3 @@ -25940,6 +26027,27 @@ snapshots: object-keys: 1.1.1 regexp.prototype.flags: 1.5.4 + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + deep-is@0.1.4: {} deepmerge@2.2.1: {} @@ -26031,6 +26139,8 @@ snapshots: dependencies: dequal: 2.0.3 + dfa@1.2.0: {} + diacritics@1.3.0: {} didyoumean@1.2.2: {} @@ -26320,6 +26430,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-iterator-helpers@1.2.1: dependencies: call-bind: 1.0.8 @@ -27174,6 +27296,18 @@ snapshots: optionalDependencies: debug: 4.4.0(supports-color@5.5.0) + fontkit@1.9.0: + dependencies: + '@swc/helpers': 0.3.17 + brotli: 1.3.3 + clone: 2.1.2 + deep-equal: 2.2.3 + dfa: 1.2.0 + restructure: 2.0.1 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -28190,6 +28324,10 @@ snapshots: dependencies: queue: 6.0.2 + image-to-pdf@3.0.2: + dependencies: + pdfkit: 0.15.2 + immer@9.0.21: {} immutable@4.3.7: {} @@ -29026,6 +29164,8 @@ snapshots: joycon@3.1.1: {} + jpeg-exif@1.1.4: {} + js-base64@3.7.7: {} js-beautify@1.15.4: @@ -29516,6 +29656,11 @@ snapshots: lilconfig@3.1.3: {} + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + lines-and-columns@1.2.4: {} lines-and-columns@2.0.3: {} @@ -31399,6 +31544,8 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@0.2.9: {} + param-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -31529,6 +31676,14 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 + pdfkit@0.15.2: + dependencies: + crypto-js: 4.2.0 + fontkit: 1.9.0 + jpeg-exif: 1.1.4 + linebreak: 1.1.0 + png-js: 1.0.0 + peberminta@0.9.0: {} peek-readable@4.1.0: {} @@ -31636,6 +31791,8 @@ snapshots: pluralize@8.0.0: {} + png-js@1.0.0: {} + pngjs@5.0.0: {} polotno@2.22.2(@types/react@18.3.1)(@types/sortablejs@1.15.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.79.2(@babel/core@7.27.1)(@types/react@18.3.1)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): @@ -32890,6 +33047,8 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restructure@2.0.1: {} + retry-axios@2.6.0(axios@1.9.0): dependencies: axios: 1.9.0(debug@4.4.0) @@ -33539,6 +33698,11 @@ snapshots: stealthy-require@1.1.1: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + store2@2.14.4: {} storybook-source-link@4.0.1(@storybook/addons@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -33966,6 +34130,8 @@ snapshots: tiny-case@1.0.3: {} + tiny-inflate@1.0.3: {} + tiny-invariant@1.0.6: {} tiny-invariant@1.2.0: {} @@ -34362,8 +34528,18 @@ snapshots: unicode-match-property-value-ecmascript@2.2.0: {} + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + unicode-property-aliases-ecmascript@2.1.0: {} + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + unidragger@3.0.1: dependencies: ev-emitter: 2.1.2 From 40f71c0bf38a9c5387038c737865be300c70ed52 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 8 Jun 2025 15:52:00 +0700 Subject: [PATCH 2/4] feat: linkedin post as carousel --- apps/backend/.swcrc | 38 -- apps/cron/.swcrc | 38 -- .../integrations/social/linkedin.provider.ts | 459 +++++++++++++----- 3 files changed, 335 insertions(+), 200 deletions(-) delete mode 100644 apps/backend/.swcrc delete mode 100644 apps/cron/.swcrc diff --git a/apps/backend/.swcrc b/apps/backend/.swcrc deleted file mode 100644 index 7d41ef14..00000000 --- a/apps/backend/.swcrc +++ /dev/null @@ -1,38 +0,0 @@ -{ - "jsc": { - "parser": { - "syntax": "typescript", - "tsx": false, - "decorators": true, - "dynamicImport": true - }, - "target": "es2020", - "baseUrl": "/Users/nevodavid/Projects/gitroom", - "paths": { - "@gitroom/backend/*": ["apps/backend/src/*"], - "@gitroom/cron/*": ["apps/cron/src/*"], - "@gitroom/frontend/*": ["apps/frontend/src/*"], - "@gitroom/helpers/*": ["libraries/helpers/src/*"], - "@gitroom/nestjs-libraries/*": ["libraries/nestjs-libraries/src/*"], - "@gitroom/react/*": ["libraries/react-shared-libraries/src/*"], - "@gitroom/plugins/*": ["libraries/plugins/src/*"], - "@gitroom/workers/*": ["apps/workers/src/*"], - "@gitroom/extension/*": ["apps/extension/src/*"] - }, - "keepClassNames": true, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true - }, - "loose": true - }, - "module": { - "type": "commonjs", - "strict": false, - "strictMode": true, - "lazy": false, - "noInterop": false - }, - "sourceMaps": true, - "minify": false -} \ No newline at end of file diff --git a/apps/cron/.swcrc b/apps/cron/.swcrc deleted file mode 100644 index 7d41ef14..00000000 --- a/apps/cron/.swcrc +++ /dev/null @@ -1,38 +0,0 @@ -{ - "jsc": { - "parser": { - "syntax": "typescript", - "tsx": false, - "decorators": true, - "dynamicImport": true - }, - "target": "es2020", - "baseUrl": "/Users/nevodavid/Projects/gitroom", - "paths": { - "@gitroom/backend/*": ["apps/backend/src/*"], - "@gitroom/cron/*": ["apps/cron/src/*"], - "@gitroom/frontend/*": ["apps/frontend/src/*"], - "@gitroom/helpers/*": ["libraries/helpers/src/*"], - "@gitroom/nestjs-libraries/*": ["libraries/nestjs-libraries/src/*"], - "@gitroom/react/*": ["libraries/react-shared-libraries/src/*"], - "@gitroom/plugins/*": ["libraries/plugins/src/*"], - "@gitroom/workers/*": ["apps/workers/src/*"], - "@gitroom/extension/*": ["apps/extension/src/*"] - }, - "keepClassNames": true, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true - }, - "loose": true - }, - "module": { - "type": "commonjs", - "strict": false, - "strictMode": true, - "lazy": false, - "noInterop": false - }, - "sourceMaps": true, - "minify": false -} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 8433b336..3e93f882 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -12,6 +12,8 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab import { Integration } from '@prisma/client'; import { PostPlug } from '@gitroom/helpers/decorators/post.plug'; import { LinkedinDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/linkedin.dto'; +import imageToPDF from 'image-to-pdf'; +import { Readable } from 'stream'; export class LinkedinProvider extends SocialAbstract implements SocialProvider { identifier = 'linkedin'; @@ -200,13 +202,24 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { picture: any, type = 'personal' as 'company' | 'personal' ) { + // Determine the appropriate endpoint based on file type + const isVideo = fileName.indexOf('mp4') > -1; + const isPdf = fileName.toLowerCase().indexOf('pdf') > -1; + + let endpoint: string; + if (isVideo) { + endpoint = 'videos'; + } else if (isPdf) { + endpoint = 'documents'; + } else { + endpoint = 'images'; + } + const { - value: { uploadUrl, image, video, uploadInstructions, ...all }, + value: { uploadUrl, image, video, document, uploadInstructions, ...all }, } = await ( await this.fetch( - `https://api.linkedin.com/v2/${ - fileName.indexOf('mp4') > -1 ? 'videos' : 'images' - }?action=initializeUpload`, + `https://api.linkedin.com/rest/${endpoint}?action=initializeUpload`, { method: 'POST', headers: { @@ -221,7 +234,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { type === 'personal' ? `urn:li:person:${personId}` : `urn:li:organization:${personId}`, - ...(fileName.indexOf('mp4') > -1 + ...(isVideo ? { fileSizeBytes: picture.length, uploadCaptions: false, @@ -235,7 +248,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { ).json(); const sendUrlRequest = uploadInstructions?.[0]?.uploadUrl || uploadUrl; - const finalOutput = video || image; + const finalOutput = video || image || document; const etags = []; for (let i = 0; i < picture.length; i += 1024 * 1024 * 2) { @@ -245,8 +258,10 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { 'X-Restli-Protocol-Version': '2.0.0', 'LinkedIn-Version': '202501', Authorization: `Bearer ${accessToken}`, - ...(fileName.indexOf('mp4') > -1 + ...(isVideo ? { 'Content-Type': 'application/octet-stream' } + : isPdf + ? { 'Content-Type': 'application/pdf' } : {}), }, body: picture.slice(i, i + 1024 * 1024 * 2), @@ -255,9 +270,9 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { etags.push(upload.headers.get('etag')); } - if (fileName.indexOf('mp4') > -1) { + if (isVideo) { const a = await this.fetch( - 'https://api.linkedin.com/v2/videos?action=finalizeUpload', + 'https://api.linkedin.com/rest/videos?action=finalizeUpload', { method: 'POST', body: JSON.stringify({ @@ -315,147 +330,343 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { return connectAll.join(''); } - async post( - id: string, + private async convertImagesToPdfCarousel( + postDetails: PostDetails[], + firstPost: PostDetails + ): Promise[]> { + // Collect all images from all posts + const allImages = postDetails.flatMap( + (post) => + post.media?.filter( + (media) => + media.url.toLowerCase().includes('.jpg') || + media.url.toLowerCase().includes('.jpeg') || + media.url.toLowerCase().includes('.png') + ) || [] + ); + + if (allImages.length === 0) { + return postDetails; + } + + // Convert images to buffers and get dimensions + const imageData = await Promise.all( + allImages.map(async (media) => { + const buffer = await readOrFetch(media.url); + const image = sharp(buffer, { + animated: lookup(media.url) === 'image/gif', + }); + const metadata = await image.metadata(); + + return { + buffer, + width: metadata.width || 0, + height: metadata.height || 0, + }; + }) + ); + + // Use the dimensions of the first image for the PDF page size + // You could also use the largest dimensions if you prefer + const firstImageDimensions = imageData[0]; + const pageSize = [firstImageDimensions.width, firstImageDimensions.height]; + + // Convert images to PDF with exact image dimensions + const pdfStream = imageToPDF( + imageData.map((data) => data.buffer), + pageSize + ); + + // Convert stream to buffer + const pdfBuffer = await this.streamToBuffer(pdfStream); + + // Create a temporary file-like object for the PDF + const pdfMedia = { + url: 'carousel.pdf', + buffer: pdfBuffer, + }; + + // Return modified post details with PDF instead of images + const modifiedFirstPost = { + ...firstPost, + media: [pdfMedia] as any[], + }; + + // Remove media from other posts since we're combining everything into one PDF + const modifiedRestPosts = postDetails.slice(1).map((post) => ({ + ...post, + media: [] as any[], + })); + + return [modifiedFirstPost, ...modifiedRestPosts]; + } + + private async streamToBuffer(stream: Readable): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('end', () => resolve(Buffer.concat(chunks))); + stream.on('error', reject); + }); + } + + private async processMediaForPosts( + postDetails: PostDetails[], accessToken: string, - postDetails: PostDetails[], - integration: Integration, - type = 'personal' as 'company' | 'personal' - ): Promise { - const [firstPost, ...restPosts] = postDetails; + personId: string, + type: 'company' | 'personal' + ): Promise> { + const mediaUploads = await Promise.all( + postDetails.flatMap( + (post) => + post.media?.map(async (media) => { + let mediaBuffer: Buffer; + + // Check if media has a buffer (from PDF conversion) + if ('buffer' in media && Buffer.isBuffer((media as any).buffer)) { + mediaBuffer = (media as any).buffer; + } else { + mediaBuffer = await this.prepareMediaBuffer(media.url); + } + + const uploadedMediaId = await this.uploadPicture( + media.url, + accessToken, + personId, + mediaBuffer, + type + ); - const uploadAll = ( - await Promise.all( - postDetails.flatMap((p) => - p?.media?.flatMap(async (m) => { return { - id: await this.uploadPicture( - m.url, - accessToken, - id, - m.url.indexOf('mp4') > -1 - ? Buffer.from(await readOrFetch(m.url)) - : await sharp(await readOrFetch(m.url), { - animated: lookup(m.url) === 'image/gif', - }) - .toFormat('jpeg') - .resize({ - width: 1000, - }) - .toBuffer(), - type - ), - postId: p.id, + id: uploadedMediaId, + postId: post.id, }; - }) - ) + }) || [] ) - ).reduce((acc, val) => { - if (!val?.id) { - return acc; - } - acc[val.postId] = acc[val.postId] || []; - acc[val.postId].push(val.id); + ); + return mediaUploads.reduce((acc, upload) => { + if (!upload?.id) return acc; + + acc[upload.postId] = acc[upload.postId] || []; + acc[upload.postId].push(upload.id); return acc; }, {} as Record); + } - const media_ids = (uploadAll[firstPost.id] || []).filter((f) => f); + private async prepareMediaBuffer(mediaUrl: string): Promise { + const isVideo = mediaUrl.indexOf('mp4') > -1; - const data = await this.fetch('https://api.linkedin.com/v2/posts', { + if (isVideo) { + return Buffer.from(await readOrFetch(mediaUrl)); + } + + return await sharp(await readOrFetch(mediaUrl), { + animated: lookup(mediaUrl) === 'image/gif', + }) + .toFormat('jpeg') + .resize({ width: 1000 }) + .toBuffer(); + } + + private buildPostContent(isPdf: boolean, mediaIds: string[]) { + if (mediaIds.length === 0) { + return {}; + } + + if (mediaIds.length === 1) { + return { + content: { + media: { + ...(isPdf ? { title: 'slides.pdf' } : {}), + id: mediaIds[0], + }, + }, + }; + } + + return { + content: { + multiImage: { + images: mediaIds.map((id) => ({ id })), + }, + }, + }; + } + + private createLinkedInPostPayload( + id: string, + type: 'company' | 'personal', + message: string, + mediaIds: string[], + isPdf: boolean + ) { + const author = + type === 'personal' ? `urn:li:person:${id}` : `urn:li:organization:${id}`; + + return { + author, + commentary: this.fixText(message), + visibility: 'PUBLIC', + distribution: { + feedDistribution: 'MAIN_FEED', + targetEntities: [] as string[], + thirdPartyDistributionChannels: [] as string[], + }, + ...this.buildPostContent(isPdf, mediaIds), + lifecycleState: 'PUBLISHED', + isReshareDisabledByAuthor: false, + }; + } + + private async createMainPost( + id: string, + accessToken: string, + firstPost: PostDetails, + mediaIds: string[], + type: 'company' | 'personal', + idPdf: boolean + ): Promise { + const postPayload = this.createLinkedInPostPayload( + id, + type, + firstPost.message, + mediaIds, + idPdf, + ); + + const response = await this.fetch('https://api.linkedin.com/rest/posts', { method: 'POST', headers: { + 'LinkedIn-Version': '202501', 'X-Restli-Protocol-Version': '2.0.0', 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, - body: JSON.stringify({ - author: - type === 'personal' - ? `urn:li:person:${id}` - : `urn:li:organization:${id}`, - commentary: this.fixText(firstPost.message), - visibility: 'PUBLIC', - distribution: { - feedDistribution: 'MAIN_FEED', - targetEntities: [], - thirdPartyDistributionChannels: [], - }, - ...(media_ids.length > 0 - ? { - content: { - ...(media_ids.length === 0 - ? {} - : media_ids.length === 1 - ? { - media: { - id: media_ids[0], - }, - } - : { - multiImage: { - images: media_ids.map((id) => ({ - id, - })), - }, - }), - }, - } - : {}), - lifecycleState: 'PUBLISHED', - isReshareDisabledByAuthor: false, - }), + body: JSON.stringify(postPayload), }); - if (data.status !== 201 && data.status !== 200) { + console.log('LinkedIn post response:', response); + if (response.status !== 201 && response.status !== 200) { throw new Error('Error posting to LinkedIn'); } - const topPostId = data.headers.get('x-restli-id')!; + return response.headers.get('x-restli-id')!; + } - const ids = [ + private async createCommentPost( + id: string, + accessToken: string, + post: PostDetails, + parentPostId: string, + type: 'company' | 'personal' + ): Promise { + const actor = + type === 'personal' ? `urn:li:person:${id}` : `urn:li:organization:${id}`; + + const response = await this.fetch( + `https://api.linkedin.com/v2/socialActions/${decodeURIComponent( + parentPostId + )}/comments`, { - status: 'posted', - postId: topPostId, - id: firstPost.id, - releaseURL: `https://www.linkedin.com/feed/update/${topPostId}`, - }, - ]; - for (const post of restPosts) { - const { object } = await ( - await this.fetch( - `https://api.linkedin.com/v2/socialActions/${decodeURIComponent( - topPostId - )}/comments`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - actor: - type === 'personal' - ? `urn:li:person:${id}` - : `urn:li:organization:${id}`, - object: topPostId, - message: { - text: this.fixText(post.message), - }, - }), - } - ) - ).json(); + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + actor, + object: parentPostId, + message: { + text: this.fixText(post.message), + }, + }), + } + ); - ids.push({ - status: 'posted', - postId: object, - id: post.id, - releaseURL: `https://www.linkedin.com/embed/feed/update/${object}`, - }); + const { object } = await response.json(); + return object; + } + + private createPostResponse( + postId: string, + originalPostId: string, + isMainPost: boolean = false + ): PostResponse { + const baseUrl = isMainPost + ? 'https://www.linkedin.com/feed/update/' + : 'https://www.linkedin.com/embed/feed/update/'; + + return { + status: 'posted', + postId, + id: originalPostId, + releaseURL: `${baseUrl}${postId}`, + }; + } + + async post( + id: string, + accessToken: string, + postDetails: PostDetails[], + integration: Integration, + type = 'personal' as 'company' | 'personal' + ): Promise { + let processedPostDetails = postDetails; + const [firstPost] = postDetails; + + // Check if we should convert images to PDF carousel + if (firstPost.settings?.post_as_images_carousel) { + processedPostDetails = await this.convertImagesToPdfCarousel( + postDetails, + firstPost + ); } - return ids; + const [processedFirstPost, ...restPosts] = processedPostDetails; + + // Process and upload media for all posts + const uploadedMedia = await this.processMediaForPosts( + processedPostDetails, + accessToken, + id, + type + ); + + // Get media IDs for the main post + const mainPostMediaIds = ( + uploadedMedia[processedFirstPost.id] || [] + ).filter(Boolean); + + // Create the main LinkedIn post + const mainPostId = await this.createMainPost( + id, + accessToken, + processedFirstPost, + mainPostMediaIds, + type, + !!firstPost.settings?.post_as_images_carousel + ); + + // Build response array starting with main post + const responses: PostResponse[] = [ + this.createPostResponse(mainPostId, processedFirstPost.id, true), + ]; + + // Create comment posts for remaining posts + for (const post of restPosts) { + const commentPostId = await this.createCommentPost( + id, + accessToken, + post, + mainPostId, + type + ); + + responses.push(this.createPostResponse(commentPostId, post.id, false)); + } + + return responses; } @PostPlug({ From da60d8f6654e1cd52859863d98bbb096cb1d0bf8 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 8 Jun 2025 16:02:21 +0700 Subject: [PATCH 3/4] feat: fix translations --- i18n.lock | 1 + .../src/translation/locales/ar/translation.json | 3 ++- .../src/translation/locales/bn/translation.json | 3 ++- .../src/translation/locales/de/translation.json | 3 ++- .../src/translation/locales/es/translation.json | 3 ++- .../src/translation/locales/fr/translation.json | 3 ++- .../src/translation/locales/he/translation.json | 3 ++- .../src/translation/locales/it/translation.json | 3 ++- .../src/translation/locales/ja/translation.json | 3 ++- .../src/translation/locales/ko/translation.json | 3 ++- .../src/translation/locales/pt/translation.json | 3 ++- .../src/translation/locales/ru/translation.json | 3 ++- .../src/translation/locales/tr/translation.json | 3 ++- .../src/translation/locales/vi/translation.json | 3 ++- .../src/translation/locales/zh/translation.json | 3 ++- 15 files changed, 29 insertions(+), 14 deletions(-) diff --git a/i18n.lock b/i18n.lock index 770ba02d..7d13037a 100644 --- a/i18n.lock +++ b/i18n.lock @@ -487,3 +487,4 @@ checksums: start_7_days_free_trial: e9c42510c2cc750fabe704ebc0a9e768 change_language: c798f65b78e23b2cf8fc29a1a24a182f that_a_wrap: 0ecf5b5a1fbac9c2653f2642baf5d4a5 + post_as_images_carousel: 2f82f0f6adbf03abfeec3389800d7232 diff --git a/libraries/react-shared-libraries/src/translation/locales/ar/translation.json b/libraries/react-shared-libraries/src/translation/locales/ar/translation.json index 943bb9b3..b80ad6d3 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ar/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ar/translation.json @@ -482,5 +482,6 @@ "90_days": "90 يومًا", "start_7_days_free_trial": "ابدأ تجربة مجانية لمدة 7 أيام", "change_language": "تغيير اللغة", - "that_a_wrap": "انتهينا!\n\nإذا أعجبك هذا التسلسل:\n\n1. تابعني على @{{username}} للمزيد من هذه المواضيع\n2. أعد تغريد التغريدة أدناه لمشاركة هذا التسلسل مع جمهورك\n" + "that_a_wrap": "انتهينا!\n\nإذا أعجبك هذا التسلسل:\n\n1. تابعني على @{{username}} للمزيد من هذه المواضيع\n2. أعد تغريد التغريدة أدناه لمشاركة هذا التسلسل مع جمهورك\n", + "post_as_images_carousel": "انشر كعرض شرائح للصور" } diff --git a/libraries/react-shared-libraries/src/translation/locales/bn/translation.json b/libraries/react-shared-libraries/src/translation/locales/bn/translation.json index 9d5c38f2..423c74db 100644 --- a/libraries/react-shared-libraries/src/translation/locales/bn/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/bn/translation.json @@ -482,5 +482,6 @@ "90_days": "৯০ দিন", "start_7_days_free_trial": "৭ দিনের বিনামূল্যে ট্রায়াল শুরু করুন", "change_language": "ভাষা পরিবর্তন করুন", - "that_a_wrap": "এটাই শেষ!\n\nযদি আপনি এই থ্রেডটি উপভোগ করে থাকেন:\n\n১. আরও এমন পোস্টের জন্য আমাকে @{{username}} ফলো করুন\n২. আপনার অডিয়েন্সের সাথে এই থ্রেডটি শেয়ার করতে নিচের টুইটটি রিটুইট করুন\n" + "that_a_wrap": "এটাই শেষ!\n\nযদি আপনি এই থ্রেডটি উপভোগ করে থাকেন:\n\n১. আরও এমন পোস্টের জন্য আমাকে @{{username}} ফলো করুন\n২. আপনার অডিয়েন্সের সাথে এই থ্রেডটি শেয়ার করতে নিচের টুইটটি রিটুইট করুন\n", + "post_as_images_carousel": "ছবির ক্যারোসেল হিসেবে পোস্ট করুন" } diff --git a/libraries/react-shared-libraries/src/translation/locales/de/translation.json b/libraries/react-shared-libraries/src/translation/locales/de/translation.json index 7a07013d..63f68358 100644 --- a/libraries/react-shared-libraries/src/translation/locales/de/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/de/translation.json @@ -482,5 +482,6 @@ "90_days": "90 Tage", "start_7_days_free_trial": "7-tägige kostenlose Testversion starten", "change_language": "Sprache ändern", - "that_a_wrap": "Das war's!\n\nWenn dir dieser Thread gefallen hat:\n\n1. Folge mir @{{username}} für mehr davon\n2. Retweete den untenstehenden Tweet, um diesen Thread mit deinem Publikum zu teilen\n" + "that_a_wrap": "Das war's!\n\nWenn dir dieser Thread gefallen hat:\n\n1. Folge mir @{{username}} für mehr davon\n2. Retweete den untenstehenden Tweet, um diesen Thread mit deinem Publikum zu teilen\n", + "post_as_images_carousel": "Als Bilderkarussell posten" } diff --git a/libraries/react-shared-libraries/src/translation/locales/es/translation.json b/libraries/react-shared-libraries/src/translation/locales/es/translation.json index a181c037..c293f1f5 100644 --- a/libraries/react-shared-libraries/src/translation/locales/es/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/es/translation.json @@ -482,5 +482,6 @@ "90_days": "90 días", "start_7_days_free_trial": "Comienza la prueba gratuita de 7 días", "change_language": "Cambiar idioma", - "that_a_wrap": "¡Eso es todo!\n\nSi te gustó este hilo:\n\n1. Sígueme en @{{username}} para más contenido como este\n2. Haz RT al tuit de abajo para compartir este hilo con tu audiencia\n" + "that_a_wrap": "¡Eso es todo!\n\nSi te gustó este hilo:\n\n1. Sígueme en @{{username}} para más contenido como este\n2. Haz RT al tuit de abajo para compartir este hilo con tu audiencia\n", + "post_as_images_carousel": "Publicar como carrusel de imágenes" } diff --git a/libraries/react-shared-libraries/src/translation/locales/fr/translation.json b/libraries/react-shared-libraries/src/translation/locales/fr/translation.json index 8f70790c..54b1d1c4 100644 --- a/libraries/react-shared-libraries/src/translation/locales/fr/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/fr/translation.json @@ -482,5 +482,6 @@ "90_days": "90 jours", "start_7_days_free_trial": "Commencez l’essai gratuit de 7 jours", "change_language": "Changer de langue", - "that_a_wrap": "C'est terminé !\n\nSi vous avez aimé ce fil :\n\n1. Suivez-moi @{{username}} pour en voir d'autres\n2. Retweetez le tweet ci-dessous pour partager ce fil avec votre audience\n" + "that_a_wrap": "C'est terminé !\n\nSi vous avez aimé ce fil :\n\n1. Suivez-moi @{{username}} pour en voir d'autres\n2. Retweetez le tweet ci-dessous pour partager ce fil avec votre audience\n", + "post_as_images_carousel": "Publier en carrousel d’images" } diff --git a/libraries/react-shared-libraries/src/translation/locales/he/translation.json b/libraries/react-shared-libraries/src/translation/locales/he/translation.json index 50e3ab2c..a4b41ccd 100644 --- a/libraries/react-shared-libraries/src/translation/locales/he/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/he/translation.json @@ -482,5 +482,6 @@ "90_days": "90 ימים", "start_7_days_free_trial": "התחל תקופת ניסיון חינם ל-7 ימים", "change_language": "שנה שפה", - "that_a_wrap": "זה הסוף!\n\nאם נהנית מהשרשור הזה:\n\n1. עקוב אחרי @{{username}} לעוד תכנים כאלה\n2. רטווט את הציוץ למטה כדי לשתף את השרשור עם הקהל שלך\n" + "that_a_wrap": "זה הסוף!\n\nאם נהנית מהשרשור הזה:\n\n1. עקוב אחרי @{{username}} לעוד תכנים כאלה\n2. רטווט את הציוץ למטה כדי לשתף את השרשור עם הקהל שלך\n", + "post_as_images_carousel": "פרסם כתמונות בגלריה" } diff --git a/libraries/react-shared-libraries/src/translation/locales/it/translation.json b/libraries/react-shared-libraries/src/translation/locales/it/translation.json index 946a1463..65e18d2e 100644 --- a/libraries/react-shared-libraries/src/translation/locales/it/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/it/translation.json @@ -482,5 +482,6 @@ "90_days": "90 giorni", "start_7_days_free_trial": "Inizia la prova gratuita di 7 giorni", "change_language": "Cambia lingua", - "that_a_wrap": "È tutto!\n\nSe ti è piaciuto questo thread:\n\n1. Seguimi su @{{username}} per altri contenuti come questo\n2. Ritwitta il tweet qui sotto per condividere questo thread con il tuo pubblico\n" + "that_a_wrap": "È tutto!\n\nSe ti è piaciuto questo thread:\n\n1. Seguimi su @{{username}} per altri contenuti come questo\n2. Ritwitta il tweet qui sotto per condividere questo thread con il tuo pubblico\n", + "post_as_images_carousel": "Pubblica come carosello di immagini" } diff --git a/libraries/react-shared-libraries/src/translation/locales/ja/translation.json b/libraries/react-shared-libraries/src/translation/locales/ja/translation.json index 65931c65..93ea8f94 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ja/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ja/translation.json @@ -482,5 +482,6 @@ "90_days": "90日間", "start_7_days_free_trial": "7日間の無料トライアルを開始", "change_language": "言語を変更", - "that_a_wrap": "以上で終了です!\n\nこのスレッドを楽しんでいただけたなら:\n\n1. @{{username}} をフォローして、さらに多くの投稿をご覧ください\n2. 下のツイートをリツイートして、このスレッドをあなたのフォロワーと共有してください\n" + "that_a_wrap": "以上で終了です!\n\nこのスレッドを楽しんでいただけたなら:\n\n1. @{{username}} をフォローして、さらに多くの投稿をご覧ください\n2. 下のツイートをリツイートして、このスレッドをあなたのフォロワーと共有してください\n", + "post_as_images_carousel": "画像カルーセルとして投稿" } diff --git a/libraries/react-shared-libraries/src/translation/locales/ko/translation.json b/libraries/react-shared-libraries/src/translation/locales/ko/translation.json index 0dcd2698..99fb60b7 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ko/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ko/translation.json @@ -482,5 +482,6 @@ "90_days": "90일", "start_7_days_free_trial": "7일 무료 체험 시작하기", "change_language": "언어 변경", - "that_a_wrap": "여기까지입니다!\n\n이 스레드가 유익하셨다면:\n\n1. 더 많은 정보를 원하시면 @{{username}}를 팔로우하세요\n2. 아래 트윗을 리트윗해서 이 스레드를 여러분의 팔로워들과 공유하세요\n" + "that_a_wrap": "여기까지입니다!\n\n이 스레드가 유익하셨다면:\n\n1. 더 많은 정보를 원하시면 @{{username}}를 팔로우하세요\n2. 아래 트윗을 리트윗해서 이 스레드를 여러분의 팔로워들과 공유하세요\n", + "post_as_images_carousel": "이미지 캐러셀로 게시" } diff --git a/libraries/react-shared-libraries/src/translation/locales/pt/translation.json b/libraries/react-shared-libraries/src/translation/locales/pt/translation.json index 2706a561..add488d8 100644 --- a/libraries/react-shared-libraries/src/translation/locales/pt/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/pt/translation.json @@ -482,5 +482,6 @@ "90_days": "90 Dias", "start_7_days_free_trial": "Comece o teste gratuito de 7 dias", "change_language": "Mudar idioma", - "that_a_wrap": "É isso aí!\n\nSe você gostou deste fio:\n\n1. Siga-me @{{username}} para ver mais conteúdos como este\n2. Dê RT no tweet abaixo para compartilhar este fio com seu público\n" + "that_a_wrap": "É isso aí!\n\nSe você gostou deste fio:\n\n1. Siga-me @{{username}} para ver mais conteúdos como este\n2. Dê RT no tweet abaixo para compartilhar este fio com seu público\n", + "post_as_images_carousel": "Publicar como carrossel de imagens" } diff --git a/libraries/react-shared-libraries/src/translation/locales/ru/translation.json b/libraries/react-shared-libraries/src/translation/locales/ru/translation.json index 3c1ceebb..1547ddd7 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ru/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ru/translation.json @@ -482,5 +482,6 @@ "90_days": "90 дней", "start_7_days_free_trial": "Начать 7-дневную бесплатную пробную версию", "change_language": "Сменить язык", - "that_a_wrap": "На этом всё!\n\nЕсли вам понравилась эта серия:\n\n1. Подпишитесь на меня @{{username}}, чтобы не пропустить новые посты\n2. Ретвитните твит ниже, чтобы поделиться этой серией со своей аудиторией\n" + "that_a_wrap": "На этом всё!\n\nЕсли вам понравилась эта серия:\n\n1. Подпишитесь на меня @{{username}}, чтобы не пропустить новые посты\n2. Ретвитните твит ниже, чтобы поделиться этой серией со своей аудиторией\n", + "post_as_images_carousel": "Опубликовать как карусель изображений" } diff --git a/libraries/react-shared-libraries/src/translation/locales/tr/translation.json b/libraries/react-shared-libraries/src/translation/locales/tr/translation.json index 681a913b..054f97e5 100644 --- a/libraries/react-shared-libraries/src/translation/locales/tr/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/tr/translation.json @@ -482,5 +482,6 @@ "90_days": "90 Gün", "start_7_days_free_trial": "7 gün ücretsiz denemeyi başlat", "change_language": "Dili Değiştir", - "that_a_wrap": "Bu iş burada bitti!\n\nEğer bu diziyi beğendiyseniz:\n\n1. Daha fazlası için beni @{{username}} hesabından takip edin\n2. Aşağıdaki tweet'i RT'leyerek bu diziyi kendi kitlenizle paylaşın\n" + "that_a_wrap": "Bu iş burada bitti!\n\nEğer bu diziyi beğendiyseniz:\n\n1. Daha fazlası için beni @{{username}} hesabından takip edin\n2. Aşağıdaki tweet'i RT'leyerek bu diziyi kendi kitlenizle paylaşın\n", + "post_as_images_carousel": "Görselleri kaydırmalı gönder olarak paylaş" } diff --git a/libraries/react-shared-libraries/src/translation/locales/vi/translation.json b/libraries/react-shared-libraries/src/translation/locales/vi/translation.json index 3339a76e..8e601f7f 100644 --- a/libraries/react-shared-libraries/src/translation/locales/vi/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/vi/translation.json @@ -482,5 +482,6 @@ "90_days": "90 ngày", "start_7_days_free_trial": "Bắt đầu dùng thử miễn phí 7 ngày", "change_language": "Thay đổi ngôn ngữ", - "that_a_wrap": "Kết thúc rồi!\n\nNếu bạn thích chuỗi bài này:\n\n1. Hãy theo dõi tôi @{{username}} để xem thêm nhiều nội dung như vậy\n2. Retweet bài bên dưới để chia sẻ chuỗi này với mọi người\n" + "that_a_wrap": "Kết thúc rồi!\n\nNếu bạn thích chuỗi bài này:\n\n1. Hãy theo dõi tôi @{{username}} để xem thêm nhiều nội dung như vậy\n2. Retweet bài bên dưới để chia sẻ chuỗi này với mọi người\n", + "post_as_images_carousel": "Đăng dưới dạng băng chuyền hình ảnh" } diff --git a/libraries/react-shared-libraries/src/translation/locales/zh/translation.json b/libraries/react-shared-libraries/src/translation/locales/zh/translation.json index b4fa9ae7..852765ab 100644 --- a/libraries/react-shared-libraries/src/translation/locales/zh/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/zh/translation.json @@ -482,5 +482,6 @@ "90_days": "90天", "start_7_days_free_trial": "开始7天免费试用", "change_language": "切换语言", - "that_a_wrap": "本帖到此结束!\n\n如果你喜欢这个话题:\n\n1. 关注我 @{{username}},获取更多类似内容\n2. 转发下方推文,与更多人分享本帖\n" + "that_a_wrap": "本帖到此结束!\n\n如果你喜欢这个话题:\n\n1. 关注我 @{{username}},获取更多类似内容\n2. 转发下方推文,与更多人分享本帖\n", + "post_as_images_carousel": "以图片轮播的形式发布" } From 8cfd94038e8c35c263fc1b9388c13be0f11dbf0e Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 8 Jun 2025 16:05:09 +0700 Subject: [PATCH 4/4] feat: post to LinkedIn --- .../src/integrations/social/linkedin.provider.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 3e93f882..b49cc81c 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -423,7 +423,12 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { let mediaBuffer: Buffer; // Check if media has a buffer (from PDF conversion) - if ('buffer' in media && Buffer.isBuffer((media as any).buffer)) { + if ( + media && + typeof media === 'object' && + 'buffer' in media && + Buffer.isBuffer(media.buffer) + ) { mediaBuffer = (media as any).buffer; } else { mediaBuffer = await this.prepareMediaBuffer(media.url); @@ -525,14 +530,14 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { firstPost: PostDetails, mediaIds: string[], type: 'company' | 'personal', - idPdf: boolean + isPdf: boolean ): Promise { const postPayload = this.createLinkedInPostPayload( id, type, firstPost.message, mediaIds, - idPdf, + isPdf ); const response = await this.fetch('https://api.linkedin.com/rest/posts', { @@ -546,7 +551,6 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { body: JSON.stringify(postPayload), }); - console.log('LinkedIn post response:', response); if (response.status !== 201 && response.status !== 200) { throw new Error('Error posting to LinkedIn'); }