From e3c3854840461994187d59633b3f82f4e5790239 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 8 Feb 2026 20:53:28 +0700 Subject: [PATCH] feat: platforms with a chrome extension --- .env.example | 3 + .../routes/no.auth.integrations.controller.ts | 85 +++++ apps/extension/manifest.dev.json | 37 +- apps/extension/manifest.json | 44 ++- apps/extension/nodemon.chrome.json | 16 - apps/extension/nodemon.firefox.json | 16 - apps/extension/package.json | 12 +- apps/extension/public/contentStyle.css | 0 apps/extension/public/dev-icon-128.png | Bin 5730 -> 0 bytes apps/extension/public/dev-icon-32.png | Bin 1049 -> 0 bytes apps/extension/src/assets/img/logo.svg | 7 - apps/extension/src/assets/styles/tailwind.css | 13 - apps/extension/src/background.ts | 209 +++++++++++ apps/extension/src/global.d.ts | 11 - apps/extension/src/locales/en/messages.json | 10 - apps/extension/src/pages/background/index.ts | 37 -- .../content/elements/action.component.tsx | 115 ------ apps/extension/src/pages/content/index.tsx | 11 - .../src/pages/content/main.content.tsx | 191 ---------- apps/extension/src/pages/content/style.css | 27 -- apps/extension/src/pages/options/Options.css | 8 - apps/extension/src/pages/options/Options.tsx | 6 - apps/extension/src/pages/options/index.css | 0 apps/extension/src/pages/options/index.html | 12 - apps/extension/src/pages/options/index.tsx | 13 - apps/extension/src/pages/panel/Panel.css | 7 - apps/extension/src/pages/panel/Panel.tsx | 10 - apps/extension/src/pages/panel/index.css | 0 apps/extension/src/pages/panel/index.html | 12 - apps/extension/src/pages/panel/index.tsx | 14 - apps/extension/src/pages/popup/Popup.tsx | 77 ---- apps/extension/src/pages/popup/index.css | 16 - apps/extension/src/pages/popup/index.html | 12 - apps/extension/src/pages/popup/index.tsx | 14 - .../providers/cookie-provider.interface.ts | 19 + .../src/providers/list/linkedin.provider.ts | 12 - .../src/providers/list/skool.provider.ts | 12 + .../src/providers/list/x.provider.ts | 24 -- .../src/providers/provider.interface.ts | 8 - apps/extension/src/providers/provider.list.ts | 8 - .../src/providers/provider.registry.ts | 18 + apps/extension/src/types/messages.ts | 85 +++++ apps/extension/src/utils/load.cookie.ts | 13 - apps/extension/src/utils/load.storage.ts | 6 - apps/extension/src/utils/request.util.ts | 33 -- apps/extension/src/utils/save.storage.ts | 7 - apps/extension/src/vite-env.d.ts | 1 - apps/extension/tsconfig.json | 3 +- apps/extension/vite.config.base.ts | 19 +- apps/extension/vite.config.chrome.ts | 4 +- apps/extension/vite.config.firefox.ts | 31 -- apps/extension/vite.config.ts | 23 ++ .../frontend/public/icons/platforms/skool.png | Bin 0 -> 1805 bytes apps/frontend/src/app/(app)/layout.tsx | 1 + apps/frontend/src/app/(extension)/layout.tsx | 1 + apps/frontend/src/chrome.d.ts | 15 + .../launches/add.provider.component.tsx | 219 +++++++++++- .../components/launches/calendar.context.tsx | 2 +- .../launches/continue.integration.tsx | 27 ++ .../src/components/launches/menu/menu.tsx | 20 +- .../src/components/new-launch/editor.tsx | 24 +- .../providers/show.all.providers.tsx | 5 + .../providers/skool/skool.group.select.tsx | 57 +++ .../providers/skool/skool.label.select.tsx | 65 ++++ .../providers/skool/skool.provider.tsx | 37 ++ .../src/components/new-launch/store.ts | 6 +- .../src/utils/strip.html.validation.ts | 10 + .../all.providers.settings.ts | 5 +- .../posts/providers-settings/skool.dto.ts | 28 ++ .../src/integrations/integration.manager.ts | 4 + .../src/integrations/social/skool.provider.ts | 333 ++++++++++++++++++ .../social/social.integrations.interface.ts | 4 +- .../src/helpers/variable.context.tsx | 2 + 73 files changed, 1358 insertions(+), 878 deletions(-) mode change 100755 => 100644 apps/extension/manifest.dev.json delete mode 100644 apps/extension/nodemon.chrome.json delete mode 100644 apps/extension/nodemon.firefox.json delete mode 100644 apps/extension/public/contentStyle.css delete mode 100644 apps/extension/public/dev-icon-128.png delete mode 100755 apps/extension/public/dev-icon-32.png delete mode 100644 apps/extension/src/assets/img/logo.svg delete mode 100644 apps/extension/src/assets/styles/tailwind.css create mode 100644 apps/extension/src/background.ts delete mode 100644 apps/extension/src/global.d.ts delete mode 100644 apps/extension/src/locales/en/messages.json delete mode 100644 apps/extension/src/pages/background/index.ts delete mode 100644 apps/extension/src/pages/content/elements/action.component.tsx delete mode 100644 apps/extension/src/pages/content/index.tsx delete mode 100644 apps/extension/src/pages/content/main.content.tsx delete mode 100644 apps/extension/src/pages/content/style.css delete mode 100644 apps/extension/src/pages/options/Options.css delete mode 100644 apps/extension/src/pages/options/Options.tsx delete mode 100644 apps/extension/src/pages/options/index.css delete mode 100644 apps/extension/src/pages/options/index.html delete mode 100644 apps/extension/src/pages/options/index.tsx delete mode 100644 apps/extension/src/pages/panel/Panel.css delete mode 100644 apps/extension/src/pages/panel/Panel.tsx delete mode 100644 apps/extension/src/pages/panel/index.css delete mode 100644 apps/extension/src/pages/panel/index.html delete mode 100644 apps/extension/src/pages/panel/index.tsx delete mode 100644 apps/extension/src/pages/popup/Popup.tsx delete mode 100644 apps/extension/src/pages/popup/index.css delete mode 100644 apps/extension/src/pages/popup/index.html delete mode 100644 apps/extension/src/pages/popup/index.tsx create mode 100644 apps/extension/src/providers/cookie-provider.interface.ts delete mode 100644 apps/extension/src/providers/list/linkedin.provider.ts create mode 100644 apps/extension/src/providers/list/skool.provider.ts delete mode 100644 apps/extension/src/providers/list/x.provider.ts delete mode 100644 apps/extension/src/providers/provider.interface.ts delete mode 100644 apps/extension/src/providers/provider.list.ts create mode 100644 apps/extension/src/providers/provider.registry.ts create mode 100644 apps/extension/src/types/messages.ts delete mode 100644 apps/extension/src/utils/load.cookie.ts delete mode 100644 apps/extension/src/utils/load.storage.ts delete mode 100644 apps/extension/src/utils/request.util.ts delete mode 100644 apps/extension/src/utils/save.storage.ts delete mode 100644 apps/extension/src/vite-env.d.ts delete mode 100644 apps/extension/vite.config.firefox.ts create mode 100644 apps/extension/vite.config.ts create mode 100644 apps/frontend/public/icons/platforms/skool.png create mode 100644 apps/frontend/src/chrome.d.ts create mode 100644 apps/frontend/src/components/new-launch/providers/skool/skool.group.select.tsx create mode 100644 apps/frontend/src/components/new-launch/providers/skool/skool.label.select.tsx create mode 100644 apps/frontend/src/components/new-launch/providers/skool/skool.provider.tsx create mode 100644 libraries/nestjs-libraries/src/dtos/posts/providers-settings/skool.dto.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/skool.provider.ts diff --git a/.env.example b/.env.example index 61d2a020..e6fc8396 100644 --- a/.env.example +++ b/.env.example @@ -76,6 +76,9 @@ MASTODON_URL="https://mastodon.social" MASTODON_CLIENT_ID="" MASTODON_CLIENT_SECRET="" +# Chrome Extension Settings (for cookie-based platform integrations like Skool) +EXTENSION_ID="" + # Misc Settings OPENAI_API_KEY="" NEXT_PUBLIC_DISCORD_SUPPORT="" diff --git a/apps/backend/src/api/routes/no.auth.integrations.controller.ts b/apps/backend/src/api/routes/no.auth.integrations.controller.ts index 832505ba..cce5450b 100644 --- a/apps/backend/src/api/routes/no.auth.integrations.controller.ts +++ b/apps/backend/src/api/routes/no.auth.integrations.controller.ts @@ -232,6 +232,10 @@ export class NoAuthIntegrationsController { ? AuthService.fixedEncryption( Buffer.from(body.code, 'base64').toString() ) + : integrationProvider.isChromeExtension + ? AuthService.signJWT( + JSON.parse(Buffer.from(body.code, 'base64').toString()) + ) : undefined ); @@ -284,11 +288,21 @@ export class NoAuthIntegrationsController { await ioRedis.del(`redirect:${body.state}`); } + const extensionToken = integrationProvider.isChromeExtension + ? AuthService.signJWT({ + integrationId: createUpdate.id, + organizationId: org.id, + internalId: String(id), + provider: integration, + }) + : undefined; + return { ...createUpdate, onboarding: onboarding === 'true', pages, ...(returnURL ? { returnURL } : {}), + ...(extensionToken ? { extensionToken } : {}), }; } @@ -307,4 +321,75 @@ export class NoAuthIntegrationsController { return this._integrationService.saveProviderPage(org.id, id, body); } + + @Post('/extension-refresh') + async extensionRefreshCookies( + @Body() body: { jwt: string; cookies: string } + ) { + let payload: any; + try { + payload = AuthService.verifyJWT(body.jwt); + } catch { + throw new HttpException('Invalid token', 401); + } + + const { integrationId, organizationId, internalId, provider } = payload; + if (!integrationId || !organizationId || !internalId || !provider) { + throw new HttpException('Invalid token payload', 400); + } + + const integration = await this._integrationService.getIntegrationById( + organizationId, + integrationId + ); + if (!integration || integration.internalId !== internalId) { + throw new HttpException('Integration not found', 404); + } + + const integrationProvider = + this._integrationManager.getSocialIntegration(provider); + if (!integrationProvider?.isChromeExtension) { + throw new HttpException('Not a Chrome extension integration', 400); + } + + const authResult = await integrationProvider.authenticate({ + code: body.cookies, + codeVerifier: '', + }); + + if (typeof authResult === 'string') { + throw new HttpException(authResult, 400); + } + + if (String(authResult.id) !== String(integration.internalId)) { + await this._integrationService.refreshNeeded( + organizationId, + integrationId + ); + return { success: false, reason: 'account_mismatch' }; + } + + await this._integrationService.createOrUpdateIntegration( + undefined, + false, + organizationId, + integration.name, + undefined, + 'social', + integration.internalId, + integration.providerIdentifier, + authResult.accessToken, + '', + authResult.expiresIn, + undefined, + false, + undefined, + undefined, + AuthService.signJWT( + JSON.parse(Buffer.from(body.cookies, 'base64').toString()) + ) + ); + + return { success: true }; + } } diff --git a/apps/extension/manifest.dev.json b/apps/extension/manifest.dev.json old mode 100755 new mode 100644 index 1d1d2f16..465c8f53 --- a/apps/extension/manifest.dev.json +++ b/apps/extension/manifest.dev.json @@ -1,15 +1,30 @@ { - "action": { - "default_icon": "public/dev-icon-32.png", - "default_popup": "src/pages/popup/index.html" - }, + "manifest_version": 3, + "name": "Postiz", + "version": "2.0.0", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtH6qclAsfFf6qbUKfPmhBbfycGrt13+0h6ti/olniCGnjQjhkVVTnURfLFz+v+842Ee+pAS5HBEXo57dQ9xUtwFGXnavVR+myjN+Un9NIfFyYmYEBvLrinclsMJBwWMM8JkhxKuaOagxp1hqGgNAO4C0bzE3YN/SPoTjNpGU8TGm/ENZ/TDUneZyyVM5HEEmOTZEmjmy9FJaxbzGmZ2rixNO45pkjXMFp8+/XrFSNiCqNZt6LQNIqL5SfVIRUKGBjE3OG/gtahVToBdlXi5yzP1uYE0Qs4grJ/T1rUUzTXFAQa7heWA9mskf0xAMEtTSED4N9bZ4sF8cf5J+SGGlwIDAQAB", + "description": "Postiz browser extension for social media scheduling", "icons": { - "128": "public/dev-icon-128.png" + "32": "icon-32.png", + "128": "icon-128.png" }, - "web_accessible_resources": [ - { - "resources": ["contentStyle.css", "dev-icon-128.png", "dev-icon-32.png"], - "matches": [] - } - ] + "permissions": [ + "cookies", + "alarms", + "storage" + ], + "host_permissions": [ + "*://*.skool.com/*" + ], + "background": { + "service_worker": "background.js", + "type": "module" + }, + "externally_connectable": { + "matches": [ + "http://localhost/*", + "https://localhost/*", + "https://*.postiz.com/*" + ] + } } diff --git a/apps/extension/manifest.json b/apps/extension/manifest.json index 519fb342..f8c1c348 100755 --- a/apps/extension/manifest.json +++ b/apps/extension/manifest.json @@ -1,31 +1,29 @@ { "manifest_version": 3, "name": "Postiz", - "description": "Your ultimate social media scheduling tool", - "options_ui": { - "page": "src/pages/options/index.html" - }, - "action": { - "default_popup": "src/pages/popup/index.html", - "default_icon": { - "32": "icon-32.png" - } - }, + "version": "2.0.0", + "description": "Postiz browser extension for social media scheduling", "icons": { + "32": "icon-32.png", "128": "icon-128.png" }, - "permissions": ["activeTab", "cookies", "tabs"], - "content_scripts": [ - { - "matches": ["http://*/*", "https://*/*", ""], - "js": ["src/pages/content/index.tsx"], - "css": ["contentStyle.css"] - } + "permissions": [ + "cookies", + "alarms", + "storage" ], - "web_accessible_resources": [ - { - "resources": ["contentStyle.css", "icon-128.png", "icon-32.png"], - "matches": [] - } - ] + "host_permissions": [ + "*://*.skool.com/*" + ], + "background": { + "service_worker": "background.js", + "type": "module" + }, + "externally_connectable": { + "matches": [ + "http://localhost/*", + "https://localhost/*", + "https://*.postiz.com/*" + ] + } } diff --git a/apps/extension/nodemon.chrome.json b/apps/extension/nodemon.chrome.json deleted file mode 100644 index 95f85a87..00000000 --- a/apps/extension/nodemon.chrome.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "env": { - "__DEV__": "true" - }, - "watch": [ - "src", - "utils", - "vite.config.base.ts", - "vite.config.chrome.ts", - "manifest.json", - "manifest.dev.json" - ], - "ext": "tsx,css,html,ts,json", - "ignore": ["src/**/*.spec.ts"], - "exec": "vite build --config vite.config.chrome.ts --mode development" -} diff --git a/apps/extension/nodemon.firefox.json b/apps/extension/nodemon.firefox.json deleted file mode 100644 index 996650fe..00000000 --- a/apps/extension/nodemon.firefox.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "env": { - "__DEV__": "true" - }, - "watch": [ - "src", - "utils", - "vite.config.base.ts", - "vite.config.firefox.ts", - "manifest.json", - "manifest.dev.json" - ], - "ext": "tsx,css,html,ts,json", - "ignore": ["src/**/*.spec.ts"], - "exec": "vite build --config vite.config.firefox.ts --mode development" -} diff --git a/apps/extension/package.json b/apps/extension/package.json index a3feacd9..1e175dae 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -1,14 +1,10 @@ { "name": "postiz-extension", - "version": "1.0.3", - "description": "A simple chrome & firefox extension template with Vite, React, TypeScript and Tailwind CSS.", + "version": "2.0.0", + "description": "Postiz browser extension for cookie-based platform authentication", "scripts": { - "build": "rm -rf dist && vite build --config vite.config.chrome.ts && zip -r extension.zip dist", - "build:chrome": "vite build --config vite.config.chrome.ts", - "build:firefox": "vite build --config vite.config.firefox.ts", - "dev": "rm -rf dist && dotenv -e ../../.env -- vite build --config vite.config.chrome.ts --mode development --watch", - "dev:chrome": "nodemon --config nodemon.chrome.json", - "dev:firefox": "nodemon --config nodemon.firefox.json" + "build": "rm -rf dist && vite build && cp manifest.json dist/manifest.json && cd dist && zip -r ../extension.zip .", + "dev": "rm -rf dist && HOT_RELOAD_EXTENSION_VITE_PORT=8081 NODE_ENV=development dotenv -e ../../.env -- vite build --config vite.config.chrome.ts --mode development --watch" }, "type": "module" } diff --git a/apps/extension/public/contentStyle.css b/apps/extension/public/contentStyle.css deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/extension/public/dev-icon-128.png b/apps/extension/public/dev-icon-128.png deleted file mode 100644 index 69c0e48328e98e78461be74abb941943cfa27ea7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5730 zcmb_gyd|r zz+K(c3jiRZ_|I?vpK@pc0BTANWd(!xIs3VeNLyo{fzV4=GqdSH=p~O#UY}52SUR(872yH{kpSC70-XM*6u=J> z93Wb+|5HGZ`V!oV7Yt&(%ZxeXEoLYDi28@YMiK5a8l3F( zcwFs~#Z)%-!_;99+2OhVgdjx}4F@tC5-Fq-^_fEI&jLTUCVnMT76~#fVw^qhAuX&l zc{YeT(bC?jArfu0)gMF;?RagU`U@8b$c1JsziZF+JQr>flhu~MO7Q)>a?_#)Y{Z}sYsmh}~CY59ocJSq7(+<@iLDe2g6;WfF-mrNC*Q5D9)D>d1IrCGze$rDuW zagQ4rrJRe|;u+DtxpY6CTMed;?2szDjEFpy^hQnuU8HL1W@6Xng5ZJNYwPkCsydXv zR7IMfEarJ!hc*AZ`Q1XbUP|2eE~>c;H|TbhDyQNOsHjfozq`9D?meaO*4|G*NJz=C zwZ6NhU8)ZGJb%J1TL{0eST|U->3&=7>GHXtW$TS9{f4yDmjb7o^#pg2p52+_(zk9g z^x9x~pA|eDQ+3N$)v>#85p--Zjdu|x*0D#ja+k{d&v9rNgS5I?J_~Z_9G%FI19A*T zF!TK7N^0?o2xpKz`=_e?fKh`%l(e(?|TKYzGcO>?*sY0jv)4B&NU&h^f8v%LaD%98<+Lw1w( zQzAaxgD4?!-%P*|^SEI6T7^6fHC=GrS0-kw*>_EjHzD1=uFN@wxbhGfiZDP@g`74F z23Mru$aw1qZHqIi?+n7c9B(+3cL+DCQyKg6%=LGpwLb_6>Wzh@$MD9I$&*>fN$dCX zba->!T-L1Lr}bS-aNYRnKR#eV`UsY2f7#z^J&@mOSaz*2q?zz3H>gM(q0-5djxAuY z@Yvb;eSPG4qEico^TALjwq2Uf^o5WB9GiA&x2F)K&p|~m zJY}xMCO5uJ>2t0Ks|&$2&ByXQmO~ssTuDr>we4?^o;qQ_3J=!rMDFx(+u-!EH;$vs zL|U!$T!qO3)o0F3?%Skt%2FLUV6UUyh-UJX&@8===g;!I)3wW{L$px-mGMFQr7R^c zKv-~ejB$t8xII=bH0IlM9T2-DT zkV5Cg;T^%@U26VO%S3Qqb;v&VqHlqlIUsJ83sONgjF%z0h$aayAnznKMG;>8`YG2- zG@J$vl!~Iq%>3ZqOrseIPI9IpT=ZozBFkjS)nOxTyJ)KFtjynUw{%PA(i%hR6bleX zxcCy%sr+4!UfX&!5lGOq>LGLuSe~}Ar&(7M0U9b%yj{x|;Y2i(7(8H3bM*zY^fZOvBKdIJD^;Q}K-rr(=4c@F>URwC~WxHe0WdT&) zFg32&5@)UDz~xT$U?^{ zeIk?0bf%04ll=r+<)lg@HjYs`7tS-G1o91nCah)IAuys+om|V)^V^%t<8bs3q*0CMAB*{oDg8?i z3~$r9eY|ZdMma@3V2ca~b)w<;*wi+|fg=7#I)9nb7h#66j)DBVJiJZ*5ZUounWlp^ znais$MM^K;;2do+uT5q0mKzW*#}Z6tyerIA>R|(rm1GpXeyaFRV6VV-qxIz+&U9BF z39Ed|w>#>(?~|#XvGp+W+s$8Ak6-JG(QT7+$qAUgFDrUMndn0at9dq`jZkPeV&-IA zx$Rzv12Aaw)qVHFl~kyabdtIFZ5zC7>-UId$G5R(=qoNl6D~jR@s-W)jE?o^(%IS` zLZ^N1vtXyrX9gUUbpCt#bj>%~Hu5=*4QD+6)PDlRB< z55oo$w)C})o1Yk0C4Lw}*69HVrosVnRADv)oN4S)P%f<*lT^sq{H{ijZ0mId-36&` zvT~#9p7)Eio-4_}yHO5|htB;B?AuF?+8xN}N5;cdC61c`A}c^Xo8m(cBHyr}m-t^mEI5Nc zMI&rad6SZn@ajFuRK`K`AD|6(xJMv~ z|BZz_Cqwq#cLl+)nh?pwN&OY?8EtOCK(W=cd<(yQA5`Vjk`_uFVeRKByuYwg##I_+ zW8EGugi+5FILozOvr_0noVQ1V7k90hNSOLp4777%&3zn&KiSoN?C9U;uu6k+vzwIi z_wY8|BJE|jBrGmQUrBhq&C7PXvLa3{Sf`9F6xZBq9sk?S1)>hUKp*anAb!nd?LyhNg3wVWXaUasJVRK4}DUiA*~)2 zdmL6WHMVHD*dAzXRpjdo98Usha(V0H2gw)N9fBalN@>tp%rTAufcbv$dCLh&^`Z0M zxeITpNDCC9h7G;?S22DhQ=-p@ua^G`JGv0B@|QI+;PQeNC?basfCGt)JBTO2k?nzx zYfNOuY|e$>ToO~cAL7UWmqxtk9W~)VcPEDkxT2{v7wZ3dx2<6Ddj- zxDZ?Tw;7(rDHh@td}Sg3O{$p&=lQi?dkr~CuF3d)-cN8+guyTqU9sP!0j#fS@pMWZ=Q3Xfbn+*Q@uBCc{i*muj^pvLZC#V8k?9MV zCyS#)T+30(5ICRA$*uz7u1b#UD#H4!HUK832f}?8k$3)Q1bdR%(f!5pmM|pZ*yc|jmvs6TeSb&7poyoapukgWw@Sb z%eCH|hN31ThmGkcU7iX-%MhD*2_Z zXwV*!6%NGy&okRU^w(Y>H3Z;zZ$sjt4XS7a&^;hmDL2mri&c{wN_uz0#QKQF&MK12 zi23c6vaKB$;PKI9>Qm1w@%AT1B|`qI9X@80x>WQ~q;AmWOxxCzDm;?Mi;PQYaJ&^Rk-w(6uqh^a@i-mQqMM>MAOV8pD=I2M!Nt(r)W2azK z4x8fFRvYEgZo55{l@!ez!Lfr~Y$||qMQFF>yg>bYAbVf;yN=_^YL7YdU;U-`R}|)< zpVOc+_N`SkQeQ1fEUNU%4NNR}>Y8iRi=hQ(<=;$RZW+O(s}Y#iStpSt>+I%aJ2nS+ zY;iVfn#)s_$%=A$cU;#&l!*%nS0r$kWK_ZsC?N{Aw*5{%PK11t za=YhpyIK|V+U$L*uWxF$FM4j4`;d17X0%T*UGrIp!KjTyM|`G49aa5eju45~Shp~- z=f|h5RYi{UiZLerFt6*xk<~|%@35SS9xT;4x6=Mzd~@qKZR=gVk`dP1U{WF_6$~dG zo2=&eq!9)P)gY)Z)O#f$%UfLz69mfBEuH0O<9>XaY5O|*e*SjE-|H8iZ; zQEM-7F}Wk{fsUp40|z8kyQGzk)}lDH6z4iKUnU&Qe$$sz#Z*}(CbP?zecJvIJc`Qe z{x}b_V=DD}FZCIzT%iqVf3qR|>X$g&mT7-=HPI)ntu{Z+5(o-AJ1UN+k!?dH6#4$B z?pG%Vu+!7m?V;WM&Q&Ql8s!Yx9-|YmHM`?8nnKsYQc&HVJ%rb(c%x4iSMZaP47CbK z-l1WFuij6iz?AM7a{0-$(?1g(A3n}LMKAg3^IyKJ@N#hFWTko#w>q2(D6>9|r`1L? z?%C@Je)qc+Aly|Dyw>ryCaZYg;iigy(0Urr3Dq94X~MB!!)58-;^NDbIeoW6y0M3n zyo&>&qrGrYXIu~4Trgmj)AxKi=nuiD^JW8Sqz`cKA0p;w<)!{l7#>80Fa(YCXu*ox zX<;rJd+-xe4oXT9dpjA~ml8n|uJvpf! z+9p5e5rLo5HmfY~4Fo_&&UULcFLof>{E^EvYZD(Y#v-BT8ODmeYHfDsus>Q>s^#BJ zgpw}0TR3=F=ejeQDjmY!3g`d!Ng(evq*>bm-sT71fr_}nI|;X-it(ZZH?OK-d{@&x zN`1Qo*!CFF5Ci@oU7&_zar4d>igC6%owITaw#RPU1d~yUHZjDG!FaH&c5)|{O?)Wg zn@$vYm)!} yMGXB*W)lW>Px&(@8`@R9HvtmVHcBbr{D#_jb7#xV&DMGRVf;1(G6lOhVXLjc{bO)zV4I`-|8J zsb*WQ(uv7pzEGmJW$BaPJyib6pEExi? z#)aNbL>xB~;BV?u`auUKzsT80KB4y8{=K1S9{?mLHQKN5>k7oiT|qGKohW8cco2?_DEU1(K=TAI(YF{gl6wpH=k z_E*WuT#MCW#s2#h#pa{NPiU&Xhfw`8bpW=}&*&L?Uv^%+_+BhO|ELIE?EH@GHBVDm zB&B^1E0R`m>y|*b^#{e~%oi=xSK3+P`-nOK=Y)lC{;ZOn^?Dz!_5XratCgQ^wkxD0 zrxO=%CM_+MV;?q9T2@5O+q;!u9UYy#p3}xs|15O?QVX8xe+EIEm<)Q=s#Lkt>BJnJ zh}j&=qmN|}9uY1B7z_p_SZ{A1C66}}6R=JlfaKMF<7EcNyJdmS@7sxvj*{1cAP^B2 zMSOf5U$>o~GJs>y!P*toJP=f@9>6;M0lgz9Wr5~%r%6s;sfZ<*lhFC-=(haxzyCXu zbXlcDZswt&t?B{(xOst&!5y-|iBB8IT9c`WRlHEe^5sdClod@IAmhO@VyILPAieEc z`qv{OQh&IP4LR$l6`N`!YgIY-xhvEINS>|Niy86Q<$jm?ezqZ9NN)EStzUjcZDkv= z#^>h;aOto8{OW2ZENqGVLXs-o;c((`IFJfWUdywC!P{%M+s(3|QlbN%MZ;w4PBrHA zJz#gY(B<4W+rj01A8i08eIiT!GYR+0nAK-a0As>+&JW}v;=MyqgHabI2Q(QH - - - - - - diff --git a/apps/extension/src/assets/styles/tailwind.css b/apps/extension/src/assets/styles/tailwind.css deleted file mode 100644 index 4ff77402..00000000 --- a/apps/extension/src/assets/styles/tailwind.css +++ /dev/null @@ -1,13 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@theme { - --animate-spin-slow: spin 20s linear infinite; - - @keyframes spin { - to { - transform: rotate(360deg); - } - } -} diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts new file mode 100644 index 00000000..e9dda89f --- /dev/null +++ b/apps/extension/src/background.ts @@ -0,0 +1,209 @@ +import { ExtensionRequest, GetCookiesResponse, ProviderInfo, StoredRefreshEntry } from './types/messages'; +import { getAllProviders, getProvider } from './providers/provider.registry'; +import { CookieProvider } from './providers/cookie-provider.interface'; + +const EXTENSION_VERSION = '2.0.0'; +const REFRESH_ALARM_NAME = 'cookie-refresh'; +const STORAGE_KEY = 'refreshEntries'; + +const ALLOWED_ORIGIN_PATTERNS = [ + /^https?:\/\/localhost(:\d+)?$/, + /^https?:\/\/([a-z0-9-]+\.)*postiz\.com$/, +]; + +function isOriginAllowed(origin: string | undefined): boolean { + if (!origin) return false; + return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin)); +} + +async function extractCookies(provider: CookieProvider): Promise { + const allCookies = await chrome.cookies.getAll({ url: provider.url }); + + const extracted: Record = {}; + const missingRequired: string[] = []; + + for (const def of provider.cookies) { + const found = allCookies.find((c) => c.name === def.name); + if (found) { + extracted[def.name] = found.value; + } else if (def.required) { + missingRequired.push(def.name); + } + } + + if (missingRequired.length > 0) { + return { + success: false, + provider: provider.identifier, + error: `Missing required cookies: ${missingRequired.join(', ')}. User may need to log in to ${provider.name}.`, + missingCookies: missingRequired, + }; + } + + return { + success: true, + provider: provider.identifier, + cookies: extracted, + }; +} + +// --- Refresh Token Storage Helpers --- + +async function getStoredEntries(): Promise> { + const result = await chrome.storage.local.get(STORAGE_KEY); + return result[STORAGE_KEY] || {}; +} + +async function setStoredEntries(entries: Record): Promise { + await chrome.storage.local.set({ [STORAGE_KEY]: entries }); +} + +async function ensureAlarm(): Promise { + const existing = await chrome.alarms.get(REFRESH_ALARM_NAME); + if (!existing) { + chrome.alarms.create(REFRESH_ALARM_NAME, { periodInMinutes: 1440 }); + } +} + +async function clearAlarmIfEmpty(): Promise { + const entries = await getStoredEntries(); + if (Object.keys(entries).length === 0) { + await chrome.alarms.clear(REFRESH_ALARM_NAME); + } +} + +// --- Background Cookie Refresh --- + +async function refreshAllCookies(): Promise { + const entries = await getStoredEntries(); + for (const [integrationId, entry] of Object.entries(entries)) { + try { + const provider = getProvider(entry.provider); + if (!provider) continue; + + const cookieResult = await extractCookies(provider); + if (!cookieResult.success) continue; + + const base64Cookies = btoa(JSON.stringify(cookieResult.cookies)); + + await fetch(`${entry.backendUrl}/integrations/extension-refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jwt: entry.jwt, cookies: base64Cookies }), + }); + } catch { + // Silently skip — will retry next cycle + } + } +} + +// --- Alarm Listener --- + +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === REFRESH_ALARM_NAME) { + refreshAllCookies(); + } +}); + +// --- Ensure alarm on startup --- + +(async () => { + const entries = await getStoredEntries(); + if (Object.keys(entries).length > 0) { + await ensureAlarm(); + } +})(); + +// --- Message Listener --- + +chrome.runtime.onMessageExternal.addListener( + ( + message: ExtensionRequest, + sender: chrome.runtime.MessageSender, + sendResponse: (response: unknown) => void + ) => { + const origin = sender.origin ?? sender.url; + if (!isOriginAllowed(origin)) { + sendResponse({ error: 'Unauthorized origin' }); + return true; + } + + switch (message.type) { + case 'PING': { + sendResponse({ status: 'ok', version: EXTENSION_VERSION }); + break; + } + + case 'GET_PROVIDERS': { + const providers = getAllProviders(); + const providerInfos: ProviderInfo[] = providers.map((p) => ({ + identifier: p.identifier, + name: p.name, + url: p.url, + cookieNames: p.cookies.map((c) => c.name), + })); + sendResponse({ providers: providerInfos }); + break; + } + + case 'GET_COOKIES': { + const provider = getProvider(message.provider); + if (!provider) { + sendResponse({ + success: false, + provider: message.provider, + error: `Unknown provider: ${message.provider}`, + }); + break; + } + + extractCookies(provider) + .then((result) => sendResponse(result)) + .catch((err) => + sendResponse({ + success: false, + provider: message.provider, + error: `Failed to extract cookies: ${err.message}`, + }) + ); + + return true; + } + + case 'STORE_REFRESH_TOKEN': { + (async () => { + const entries = await getStoredEntries(); + entries[message.integrationId] = { + jwt: message.jwt, + backendUrl: message.backendUrl, + provider: message.provider, + }; + await setStoredEntries(entries); + await ensureAlarm(); + sendResponse({ success: true }); + })().catch(() => sendResponse({ success: false })); + + return true; + } + + case 'REMOVE_REFRESH_TOKEN': { + (async () => { + const entries = await getStoredEntries(); + delete entries[message.integrationId]; + await setStoredEntries(entries); + await clearAlarmIfEmpty(); + sendResponse({ success: true }); + })().catch(() => sendResponse({ success: false })); + + return true; + } + + default: { + sendResponse({ error: `Unknown message type: ${(message as any).type}` }); + break; + } + } + + return true; + } +); diff --git a/apps/extension/src/global.d.ts b/apps/extension/src/global.d.ts deleted file mode 100644 index 7ad3edd1..00000000 --- a/apps/extension/src/global.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare module '*.svg' { - import React = require('react'); - export const ReactComponent: React.SFC>; - const src: string; - export default src; -} - -declare module '*.json' { - const content: string; - export default content; -} diff --git a/apps/extension/src/locales/en/messages.json b/apps/extension/src/locales/en/messages.json deleted file mode 100644 index f0941104..00000000 --- a/apps/extension/src/locales/en/messages.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extName": { - "message": "name in src/locales/en/messages.json", - "description": "Extension name" - }, - "extDescription": { - "message": "description in src/locales/en/messages.json", - "description": "Extension description" - } -} diff --git a/apps/extension/src/pages/background/index.ts b/apps/extension/src/pages/background/index.ts deleted file mode 100644 index ce643b60..00000000 --- a/apps/extension/src/pages/background/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { fetchRequestUtil } from '@gitroom/extension/utils/request.util'; - -const isDevelopment = process.env.NODE_ENV === 'development'; - -chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { - if (request.action === 'makeHttpRequest') { - fetchRequestUtil(request).then((response) => { - sendResponse(response); - }); - } - - if (request.action === 'loadStorage') { - chrome.storage.local.get([request.key], function (storage) { - sendResponse(storage[request.key]); - }); - } - - if (request.action === 'saveStorage') { - chrome.storage.local.set({ [request.key]: request.value }, function () { - sendResponse({ success: true }); - }); - } - - if (request.action === 'loadCookie') { - chrome.cookies.get( - { - url: import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL, - name: request.cookieName, - }, - function (cookies) { - sendResponse(cookies?.value); - } - ); - } - - return true; -}); diff --git a/apps/extension/src/pages/content/elements/action.component.tsx b/apps/extension/src/pages/content/elements/action.component.tsx deleted file mode 100644 index 109520b2..00000000 --- a/apps/extension/src/pages/content/elements/action.component.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { FC, memo, useCallback, useEffect, useState } from 'react'; -import { ProviderInterface } from '@gitroom/extension/providers/provider.interface'; -import { fetchCookie } from '@gitroom/extension/utils/load.cookie'; - -const Comp: FC<{ removeModal: () => void; platform: string; style: string }> = ( - props -) => { - const load = async () => { - const cookie = await fetchCookie(`auth`); - if (document.querySelector('iframe#modal-postiz')) { - return; - } - - const div = document.createElement('div'); - div.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; - div.style.position = 'fixed'; - div.style.top = '0'; - div.style.left = '0'; - div.style.zIndex = '9999'; - div.style.width = '100%'; - div.style.height = '100%'; - div.style.border = 'none'; - div.style.overflow = 'hidden'; - document.body.appendChild(div); - - const iframe = document.createElement('iframe'); - iframe.style.backgroundColor = 'transparent'; - // @ts-ignore - iframe.allowTransparency = 'true'; - iframe.src = - (import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL) + - `/modal/${props.style}/${props.platform}?loggedAuth=${cookie}`; - iframe.id = 'modal-postiz'; - iframe.style.width = '100%'; - iframe.style.height = '100%'; - iframe.style.position = 'fixed'; - iframe.style.top = '0'; - iframe.style.left = '0'; - iframe.style.zIndex = '9999'; - iframe.style.border = 'none'; - div.appendChild(iframe); - - window.addEventListener('message', (event) => { - if (event.data.action === 'closeIframe') { - const iframe = document.querySelector('iframe#modal-postiz'); - if (iframe) { - props.removeModal(); - div.remove(); - } - } - }); - }; - useEffect(() => { - load(); - }, []); - return <>; -}; -export const ActionComponent: FC<{ - target: Node; - keyIndex: number; - actionType: string; - provider: ProviderInterface; - wrap: boolean; - selector: string; -}> = memo((props) => { - const { wrap, provider, selector, target, actionType } = props; - const [modal, showModal] = useState(false); - const handle = useCallback(async (e: any) => { - showModal(true); - e.preventDefault(); - e.stopPropagation(); - }, []); - - useEffect(() => { - const blockingDiv = document.createElement('div'); - if (document.querySelector(`.${selector}`)) { - console.log('already exists'); - return; - } - - setTimeout(() => { - // @ts-ignore - const targetInformation = target.getBoundingClientRect(); - blockingDiv.style.position = 'absolute'; - blockingDiv.id = 'blockingDiv'; - blockingDiv.style.cursor = 'pointer'; - blockingDiv.style.top = `${targetInformation.top}px`; - blockingDiv.style.left = `${targetInformation.left}px`; - blockingDiv.style.width = `${targetInformation.width}px`; - blockingDiv.style.height = `${targetInformation.height}px`; - blockingDiv.style.zIndex = '9999'; - blockingDiv.className = selector; - - document.body.appendChild(blockingDiv); - blockingDiv.addEventListener('click', handle); - }, 1000); - return () => { - blockingDiv.removeEventListener('click', handle); - blockingDiv.remove(); - }; - }, []); - - return ( -
-
- {modal && ( - showModal(false)} - /> - )} -
- ); -}); diff --git a/apps/extension/src/pages/content/index.tsx b/apps/extension/src/pages/content/index.tsx deleted file mode 100644 index d0456b43..00000000 --- a/apps/extension/src/pages/content/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoot } from 'react-dom/client'; -import './style.css'; -import { MainContent } from '@gitroom/extension/pages/content/main.content'; -const div = document.createElement('div'); -div.id = '__root'; -document.body.appendChild(div); - -const rootContainer = document.querySelector('#__root'); -if (!rootContainer) throw new Error("Can't find Content root element"); -const root = createRoot(rootContainer); -root.render(); diff --git a/apps/extension/src/pages/content/main.content.tsx b/apps/extension/src/pages/content/main.content.tsx deleted file mode 100644 index a726f5ad..00000000 --- a/apps/extension/src/pages/content/main.content.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { - FC, - Fragment, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { ProviderList } from '@gitroom/extension/providers/provider.list'; -import { createPortal } from 'react-dom'; -import { ActionComponent } from '@gitroom/extension/pages/content/elements/action.component'; - -// Define a type to track elements with their action types -interface ActionElement { - element: HTMLElement; - actionType: string; -} - -export const MainContent: FC = () => { - return ; -}; - -export const MainContentInner: FC = (props) => { - const [actionElements, setActionElements] = useState([]); - const actionSetRef = useRef(new Map()); - const provider = useMemo(() => { - return ProviderList.find((p) => { - return p.baseUrl.indexOf(new URL(window.location.href).hostname) > -1; - }); - }, []); - - useEffect(() => { - if (!provider) return; - - // Helper to scan DOM for existing matching elements - const scanDOMForExistingMatches = () => { - const action = { selector: provider.element, type: 'post' }; - const matches = document.querySelectorAll(action.selector); - matches.forEach((match) => { - const htmlMatch = match as HTMLElement; - if (!actionSetRef.current.has(htmlMatch)) { - actionSetRef.current.set(htmlMatch, action.type); - } - }); - - // Update state - const elements: ActionElement[] = []; - actionSetRef.current.forEach((actionType, element) => { - elements.push({ element, actionType }); - }); - setActionElements(elements); - }; - - // Initial scan before observing - scanDOMForExistingMatches(); - - const observer = new MutationObserver((mutationsList) => { - let addedSomething = false; - let removedSomething = false; - - for (const mutation of mutationsList) { - if (mutation.type === 'childList') { - for (const node of mutation.addedNodes) { - if (node.nodeType === Node.ELEMENT_NODE) { - const el = node as HTMLElement; - - const action = { selector: provider.element, type: 'post' }; - if ( - el.matches?.(action.selector) && - !actionSetRef.current.has(el) - ) { - actionSetRef.current.set(el, action.type); - addedSomething = true; - } - - if (el.querySelectorAll) { - const matches = el.querySelectorAll(action.selector); - matches.forEach((match) => { - const htmlMatch = match as HTMLElement; - if (!actionSetRef.current.has(htmlMatch)) { - actionSetRef.current.set(htmlMatch, action.type); - addedSomething = true; - } - }); - } - } - } - - for (const node of mutation.removedNodes) { - if (node.nodeType === Node.ELEMENT_NODE) { - const el = node as HTMLElement; - - if (actionSetRef.current.has(el)) { - actionSetRef.current.delete(el); - removedSomething = true; - } - - const action = { selector: provider.element, type: 'post' }; - if (el.querySelectorAll) { - const matches = el.querySelectorAll(action.selector); - matches.forEach((match) => { - const htmlMatch = match as HTMLElement; - if (actionSetRef.current.has(htmlMatch)) { - actionSetRef.current.delete(htmlMatch); - removedSomething = true; - } - }); - } - } - } - } - - if (mutation.type === 'attributes') { - const el = mutation.target; - if (el instanceof HTMLElement) { - const action = { selector: provider.element, type: 'post' }; - const matchesNow = el.matches(action.selector); - const wasTracked = actionSetRef.current.has(el); - - if (matchesNow && !wasTracked) { - actionSetRef.current.set(el, action.type); - addedSomething = true; - } else if (!matchesNow && wasTracked) { - actionSetRef.current.delete(el); - removedSomething = true; - } - } - } - } - - if (addedSomething || removedSomething) { - const elements: ActionElement[] = []; - actionSetRef.current.forEach((actionType, element) => { - elements.push({ element, actionType }); - }); - setActionElements(elements); - } - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeOldValue: true, - }); - - return () => observer.disconnect(); - }, []); - - return actionElements.map((actionEl, index) => ( - - {createPortal( - z.trim()) - .find((p) => actionEl.element.matches(p)) || '' - )} - />, - actionEl.element - )} - - )); -}; - -function stringToABC(text: string, length = 8) { - // Simple DJB2-like hash (non-cryptographic!) - let hash = 5381; - for (let i = 0; i < text.length; i++) { - hash = (hash * 33) ^ text.charCodeAt(i); - } - - hash = Math.abs(hash); - - // Convert to base-26 string using a–z - const alphabet = 'abcdefghijklmnopqrstuvwxyz'; - let result = ''; - while (result.length < length) { - result = alphabet[hash % 26] + result; - hash = Math.floor(hash / 26); - } - - return result; -} diff --git a/apps/extension/src/pages/content/style.css b/apps/extension/src/pages/content/style.css deleted file mode 100644 index 7324e9b7..00000000 --- a/apps/extension/src/pages/content/style.css +++ /dev/null @@ -1,27 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -.my-wrapper { - left: 0 !important; - top: 0 !important; - position: fixed !important; - width: 100% !important; - height: 100% !important; - z-index: 999999 !important; - display: flex !important; - justify-content: center !important; - background: rgba(0, 0, 0, 0.5) !important; -} - -.my-wrapper > div { - background: white !important; - width: 600px !important; - height: 300px !important; - border-radius: 10px !important; - display: flex !important; - flex-direction: column !important; - justify-items: center !important; - margin-top: 100px !important; - color: black !important; -} diff --git a/apps/extension/src/pages/options/Options.css b/apps/extension/src/pages/options/Options.css deleted file mode 100644 index 1ea51cba..00000000 --- a/apps/extension/src/pages/options/Options.css +++ /dev/null @@ -1,8 +0,0 @@ -.container { - width: 100%; - height: 50vh; - font-size: 2rem; - display: flex; - align-items: center; - justify-content: center; -} diff --git a/apps/extension/src/pages/options/Options.tsx b/apps/extension/src/pages/options/Options.tsx deleted file mode 100644 index 7f5a709b..00000000 --- a/apps/extension/src/pages/options/Options.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import '@gitroom/extension/pages/options/Options.css'; - -export default function Options() { - return
Options
; -} diff --git a/apps/extension/src/pages/options/index.css b/apps/extension/src/pages/options/index.css deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/extension/src/pages/options/index.html b/apps/extension/src/pages/options/index.html deleted file mode 100644 index db5fa745..00000000 --- a/apps/extension/src/pages/options/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Options - - - -
- - - diff --git a/apps/extension/src/pages/options/index.tsx b/apps/extension/src/pages/options/index.tsx deleted file mode 100644 index feaad09b..00000000 --- a/apps/extension/src/pages/options/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import '@gitroom/extension/pages/options/index.css'; -import Options from '@gitroom/extension/pages/options/Options'; - -function init() { - const rootContainer = document.querySelector('#__root'); - if (!rootContainer) throw new Error("Can't find Options root element"); - const root = createRoot(rootContainer); - root.render(); -} - -init(); diff --git a/apps/extension/src/pages/panel/Panel.css b/apps/extension/src/pages/panel/Panel.css deleted file mode 100644 index 8a50bc80..00000000 --- a/apps/extension/src/pages/panel/Panel.css +++ /dev/null @@ -1,7 +0,0 @@ -body { - background-color: #242424; -} - -.container { - color: #ffffff; -} diff --git a/apps/extension/src/pages/panel/Panel.tsx b/apps/extension/src/pages/panel/Panel.tsx deleted file mode 100644 index 2157d549..00000000 --- a/apps/extension/src/pages/panel/Panel.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import '@pages/panel/Panel.css'; - -export default function Panel() { - return ( -
-

Side Panel

-
- ); -} diff --git a/apps/extension/src/pages/panel/index.css b/apps/extension/src/pages/panel/index.css deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/extension/src/pages/panel/index.html b/apps/extension/src/pages/panel/index.html deleted file mode 100644 index 7aa306d0..00000000 --- a/apps/extension/src/pages/panel/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Devtools Panel - - - -
- - - diff --git a/apps/extension/src/pages/panel/index.tsx b/apps/extension/src/pages/panel/index.tsx deleted file mode 100644 index 797015a1..00000000 --- a/apps/extension/src/pages/panel/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import Panel from '@pages/panel/Panel'; -import '@pages/panel/index.css'; -import '@assets/styles/tailwind.css'; - -function init() { - const rootContainer = document.querySelector('#__root'); - if (!rootContainer) throw new Error("Can't find Panel root element"); - const root = createRoot(rootContainer); - root.render(); -} - -init(); diff --git a/apps/extension/src/pages/popup/Popup.tsx b/apps/extension/src/pages/popup/Popup.tsx deleted file mode 100644 index 604ba2bc..00000000 --- a/apps/extension/src/pages/popup/Popup.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { ProviderList } from '@gitroom/extension/providers/provider.list'; -import { fetchCookie } from '@gitroom/extension/utils/load.cookie'; - -export const PopupContainerContainer: FC = () => { - const [url, setUrl] = useState(null); - useEffect(() => { - chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { - setUrl(tabs[0]?.url); - }); - }, []); - - if (!url) { - return ( -
This website is not supported by Postiz
- ); - } - - return ; -}; - -export const PopupContainer: FC<{ url: string }> = (props) => { - const { url } = props; - const [isLoggedIn, setIsLoggedIn] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const provider = useMemo(() => { - return ProviderList.find((p) => { - return p.baseUrl.indexOf(new URL(url).hostname) > -1; - }); - }, [url]); - - const loadCookie = useCallback(async () => { - try { - if (!provider) { - setIsLoading(false); - return; - } - const auth = await fetchCookie(`auth`); - - if (auth) { - setIsLoggedIn(auth); - } - - setIsLoading(false); - } catch (e) { - setIsLoading(false); - } - }, []); - - useEffect(() => { - loadCookie(); - }, []); - - if (isLoading) { - return null; - } - - if (!provider) { - return ( -
This website is not supported by Postiz
- ); - } - - if (!isLoggedIn) { - return
You are not logged in to Postiz
; - } - - return
; -}; - -export default function Popup() { - return ( -
- -
- ); -} diff --git a/apps/extension/src/pages/popup/index.css b/apps/extension/src/pages/popup/index.css deleted file mode 100644 index d4638762..00000000 --- a/apps/extension/src/pages/popup/index.css +++ /dev/null @@ -1,16 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -body { - width: 300px; - height: 260px; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - position: relative; -} diff --git a/apps/extension/src/pages/popup/index.html b/apps/extension/src/pages/popup/index.html deleted file mode 100644 index b60f054f..00000000 --- a/apps/extension/src/pages/popup/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Popup - - - -
- - - diff --git a/apps/extension/src/pages/popup/index.tsx b/apps/extension/src/pages/popup/index.tsx deleted file mode 100644 index 606ffc55..00000000 --- a/apps/extension/src/pages/popup/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import './index.css'; -import '@gitroom/extension/assets/styles/tailwind.css'; -import Popup from '@gitroom/extension/pages/popup/Popup'; - -function init() { - const rootContainer = document.querySelector('#__root'); - if (!rootContainer) throw new Error("Can't find Popup root element"); - const root = createRoot(rootContainer); - root.render(); -} - -init(); diff --git a/apps/extension/src/providers/cookie-provider.interface.ts b/apps/extension/src/providers/cookie-provider.interface.ts new file mode 100644 index 00000000..60912fe4 --- /dev/null +++ b/apps/extension/src/providers/cookie-provider.interface.ts @@ -0,0 +1,19 @@ +export interface CookieDefinition { + /** The cookie name to extract, e.g., 'client_id' */ + name: string; + /** Whether this cookie must exist for the extraction to be considered successful */ + required: boolean; +} + +export interface CookieProvider { + /** Unique identifier used in messages, e.g., 'skool' */ + identifier: string; + /** Human-readable name, e.g., 'Skool' */ + name: string; + /** URL to query cookies for, e.g., 'https://www.skool.com' — passed to chrome.cookies.getAll({ url }) */ + url: string; + /** URL pattern for host_permissions in manifest, e.g., '*://*.skool.com/*' */ + hostPermission: string; + /** List of cookies to extract from this site */ + cookies: CookieDefinition[]; +} diff --git a/apps/extension/src/providers/list/linkedin.provider.ts b/apps/extension/src/providers/list/linkedin.provider.ts deleted file mode 100644 index 6bf0d191..00000000 --- a/apps/extension/src/providers/list/linkedin.provider.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ProviderInterface } from '@gitroom/extension/providers/provider.interface'; - -export class LinkedinProvider implements ProviderInterface { - identifier = 'linkedin'; - baseUrl = 'https://www.linkedin.com'; - element = `.share-box-feed-entry__closed-share-box`; - attachTo = `[role="main"]`; - style = 'light' as 'light'; - findIdentifier = (element: HTMLElement) => { - return element.closest('[data-urn]').getAttribute('data-urn'); - }; -} diff --git a/apps/extension/src/providers/list/skool.provider.ts b/apps/extension/src/providers/list/skool.provider.ts new file mode 100644 index 00000000..6bfd51cf --- /dev/null +++ b/apps/extension/src/providers/list/skool.provider.ts @@ -0,0 +1,12 @@ +import { CookieProvider } from '../cookie-provider.interface'; + +export const skoolProvider: CookieProvider = { + identifier: 'skool', + name: 'Skool', + url: 'https://www.skool.com', + hostPermission: '*://*.skool.com/*', + cookies: [ + { name: 'client_id', required: true }, + { name: 'auth_token', required: true }, + ], +}; diff --git a/apps/extension/src/providers/list/x.provider.ts b/apps/extension/src/providers/list/x.provider.ts deleted file mode 100644 index 359a70de..00000000 --- a/apps/extension/src/providers/list/x.provider.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ProviderInterface } from '@gitroom/extension/providers/provider.interface'; - -export class XProvider implements ProviderInterface { - identifier = 'x'; - baseUrl = 'https://x.com'; - element = `[data-testid="primaryColumn"]:has([data-testid="toolBar"]) [data-testid="tweetTextarea_0_label"], [data-testid="SideNav_NewTweet_Button"]`; - attachTo = `#react-root`; - style = 'dark' as 'dark'; - findIdentifier = (element: HTMLElement) => { - return ( - Array.from( - ( - element?.closest('article') || - element?.closest(`[aria-labelledby="modal-header"]`) - )?.querySelectorAll('a') || [] - ) - ?.find((p) => { - return p?.getAttribute('href')?.includes('/status/'); - }) - ?.getAttribute('href') - ?.split('/status/')?.[1] || window.location.href.split('/status/')?.[1] - ); - }; -} diff --git a/apps/extension/src/providers/provider.interface.ts b/apps/extension/src/providers/provider.interface.ts deleted file mode 100644 index b7221211..00000000 --- a/apps/extension/src/providers/provider.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ProviderInterface { - identifier: string; - baseUrl: string; - element: string; - findIdentifier: (element: HTMLElement) => string; - attachTo: string; - style: 'dark' | 'light'; -} diff --git a/apps/extension/src/providers/provider.list.ts b/apps/extension/src/providers/provider.list.ts deleted file mode 100644 index b0faf078..00000000 --- a/apps/extension/src/providers/provider.list.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { XProvider } from './list/x.provider'; -import { ProviderInterface } from './provider.interface'; -import { LinkedinProvider } from './list/linkedin.provider'; - -export const ProviderList = [ - new XProvider(), - new LinkedinProvider(), -] satisfies ProviderInterface[] as ProviderInterface[]; diff --git a/apps/extension/src/providers/provider.registry.ts b/apps/extension/src/providers/provider.registry.ts new file mode 100644 index 00000000..aa992c9e --- /dev/null +++ b/apps/extension/src/providers/provider.registry.ts @@ -0,0 +1,18 @@ +import { CookieProvider } from './cookie-provider.interface'; +import { skoolProvider } from './list/skool.provider'; + +export const providers: CookieProvider[] = [ + skoolProvider, +]; + +const providerMap = new Map( + providers.map((p) => [p.identifier, p]) +); + +export function getAllProviders(): CookieProvider[] { + return providers; +} + +export function getProvider(identifier: string): CookieProvider | undefined { + return providerMap.get(identifier); +} diff --git a/apps/extension/src/types/messages.ts b/apps/extension/src/types/messages.ts new file mode 100644 index 00000000..1469b59c --- /dev/null +++ b/apps/extension/src/types/messages.ts @@ -0,0 +1,85 @@ +// ---- Request Types ---- + +export interface PingRequest { + type: 'PING'; +} + +export interface GetProvidersRequest { + type: 'GET_PROVIDERS'; +} + +export interface GetCookiesRequest { + type: 'GET_COOKIES'; + provider: string; +} + +export interface StoreRefreshTokenRequest { + type: 'STORE_REFRESH_TOKEN'; + provider: string; + integrationId: string; + jwt: string; + backendUrl: string; +} + +export interface RemoveRefreshTokenRequest { + type: 'REMOVE_REFRESH_TOKEN'; + integrationId: string; +} + +export type ExtensionRequest = + | PingRequest + | GetProvidersRequest + | GetCookiesRequest + | StoreRefreshTokenRequest + | RemoveRefreshTokenRequest; + +// ---- Response Types ---- + +export interface PingResponse { + status: 'ok'; + version: string; +} + +export interface ProviderInfo { + identifier: string; + name: string; + url: string; + cookieNames: string[]; +} + +export interface GetProvidersResponse { + providers: ProviderInfo[]; +} + +export interface GetCookiesSuccessResponse { + success: true; + provider: string; + cookies: Record; +} + +export interface GetCookiesErrorResponse { + success: false; + provider: string; + error: string; + missingCookies?: string[]; +} + +export type GetCookiesResponse = + | GetCookiesSuccessResponse + | GetCookiesErrorResponse; + +export interface StoredRefreshEntry { + jwt: string; + backendUrl: string; + provider: string; +} + +export interface ErrorResponse { + error: string; +} + +export type ExtensionResponse = + | PingResponse + | GetProvidersResponse + | GetCookiesResponse + | ErrorResponse; diff --git a/apps/extension/src/utils/load.cookie.ts b/apps/extension/src/utils/load.cookie.ts deleted file mode 100644 index 77164ab8..00000000 --- a/apps/extension/src/utils/load.cookie.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const fetchCookie = (cookieName: string) => { - return chrome.runtime.sendMessage({ - action: 'loadCookie', - cookieName, - }); -}; - -export const getCookie = async ( - cookies: chrome.cookies.Cookie[], - cookie: string -) => { - // return cookies.find((c) => c.name === cookie).value; -}; diff --git a/apps/extension/src/utils/load.storage.ts b/apps/extension/src/utils/load.storage.ts deleted file mode 100644 index 0f5de843..00000000 --- a/apps/extension/src/utils/load.storage.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const fetchStorage = (key: string) => { - return chrome.runtime.sendMessage({ - action: 'loadStorage', - key, - }); -}; diff --git a/apps/extension/src/utils/request.util.ts b/apps/extension/src/utils/request.util.ts deleted file mode 100644 index 7da73fed..00000000 --- a/apps/extension/src/utils/request.util.ts +++ /dev/null @@ -1,33 +0,0 @@ -const isDev = process.env.NODE_ENV === 'development'; -export const sendRequest = ( - auth: string, - url: string, - method: 'GET' | 'POST', - body?: string -) => { - return chrome.runtime.sendMessage({ - action: 'makeHttpRequest', - url, - method, - body, - auth, - }); -}; - -export const fetchRequestUtil = async (request: any) => { - return ( - await fetch( - (import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL) + - request.url, - { - method: request.method || 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: request.auth, - // Add any auth headers here if needed - }, - ...(request.body ? { body: request.body } : {}), - } - ) - ).json(); -}; diff --git a/apps/extension/src/utils/save.storage.ts b/apps/extension/src/utils/save.storage.ts deleted file mode 100644 index f9e2fbdb..00000000 --- a/apps/extension/src/utils/save.storage.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const saveStorage = (key: string, value: any) => { - return chrome.runtime.sendMessage({ - action: 'saveStorage', - key, - value, - }); -}; diff --git a/apps/extension/src/vite-env.d.ts b/apps/extension/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2..00000000 --- a/apps/extension/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/apps/extension/tsconfig.json b/apps/extension/tsconfig.json index 45dfbf4e..eab667fd 100644 --- a/apps/extension/tsconfig.json +++ b/apps/extension/tsconfig.json @@ -21,7 +21,6 @@ "src", "utils", "vite.config.base.ts", - "vite.config.chrome.ts", - "vite.config.firefox.ts" + "vite.config.chrome.ts" ] } diff --git a/apps/extension/vite.config.base.ts b/apps/extension/vite.config.base.ts index e3e81581..6a880897 100644 --- a/apps/extension/vite.config.base.ts +++ b/apps/extension/vite.config.base.ts @@ -1,38 +1,28 @@ import react from '@vitejs/plugin-react'; import { resolve } from 'path'; import { ManifestV3Export } from '@crxjs/vite-plugin'; -import tailwindcss from '@tailwindcss/vite'; import { defineConfig, BuildOptions } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; import { stripDevIcons, crxI18n } from './custom-vite-plugins'; import manifest from './manifest.json'; import devManifest from './manifest.dev.json'; import pkg from './package.json'; -import { ProviderList } from './src/providers/provider.list'; +import { providers } from './src/providers/provider.registry'; const isDev = process.env.NODE_ENV === 'development'; // set this flag to true, if you want localization support const localize = false; const merge = isDev ? devManifest : ({} as ManifestV3Export); -const { matches, ...rest } = manifest?.content_scripts?.[0] || {}; export const baseManifest = { ...manifest, host_permissions: [ - ...ProviderList.map((p) => p.baseUrl + '/'), import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL + '/*', + (import.meta.env?.NEXT_PUBLIC_BACKEND_URL || process?.env?.NEXT_PUBLIC_BACKEND_URL || '') + '/*', + ...providers.map(p => p.hostPermission) ], permissions: [...(manifest.permissions || [])], - content_scripts: [ - { - matches: ProviderList.reduce( - (all, p) => [...all, p.baseUrl + '/*'], - [import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL + '/*'] - ), - ...rest, - }, - ], version: pkg.version, ...merge, ...(localize @@ -50,9 +40,8 @@ export const baseBuildOptions: BuildOptions = { }; export default defineConfig({ - envPrefix: ['NEXT_PUBLIC_', 'FRONTEND_URL'], + envPrefix: ['NEXT_PUBLIC_', 'FRONTEND_URL', 'NEXT_PUBLIC_BACKEND_URL'], plugins: [ - tailwindcss(), tsconfigPaths(), react(), stripDevIcons(isDev), diff --git a/apps/extension/vite.config.chrome.ts b/apps/extension/vite.config.chrome.ts index 56fc94b8..1df4a600 100644 --- a/apps/extension/vite.config.chrome.ts +++ b/apps/extension/vite.config.chrome.ts @@ -15,7 +15,7 @@ export default mergeConfig( manifest: { ...baseManifest, background: { - service_worker: 'src/pages/background/index.ts', + service_worker: 'src/background.ts', type: 'module', }, } as ManifestV3Export, @@ -28,7 +28,7 @@ export default mergeConfig( ? [ hotReloadExtension({ log: true, - backgroundPath: 'src/pages/background/index.ts', + backgroundPath: 'src/background.ts', }), ] : []), diff --git a/apps/extension/vite.config.firefox.ts b/apps/extension/vite.config.firefox.ts deleted file mode 100644 index b1a8bf9b..00000000 --- a/apps/extension/vite.config.firefox.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { resolve } from 'path'; -import { mergeConfig, defineConfig } from 'vite'; -import { crx, ManifestV3Export } from '@crxjs/vite-plugin'; -import baseConfig, { baseManifest, baseBuildOptions } from './vite.config.base'; - -const outDir = resolve(__dirname, 'dist_firefox'); - -export default mergeConfig( - baseConfig, - defineConfig({ - plugins: [ - crx({ - manifest: { - ...baseManifest, - background: { - scripts: ['src/pages/background/index.ts'], - }, - } as ManifestV3Export, - browser: 'firefox', - contentScripts: { - injectCss: true, - }, - }), - ], - build: { - ...baseBuildOptions, - outDir, - }, - publicDir: resolve(__dirname, 'public'), - }) -); diff --git a/apps/extension/vite.config.ts b/apps/extension/vite.config.ts new file mode 100644 index 00000000..e69fb7f9 --- /dev/null +++ b/apps/extension/vite.config.ts @@ -0,0 +1,23 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + outDir: resolve(__dirname, 'dist'), + emptyOutDir: true, + lib: { + entry: resolve(__dirname, 'src/background.ts'), + formats: ['es'], + fileName: () => 'background.js', + }, + rollupOptions: { + output: { + entryFileNames: 'background.js', + }, + }, + target: 'esnext', + minify: false, + sourcemap: process.env.NODE_ENV === 'development', + }, + publicDir: resolve(__dirname, 'public'), +}); diff --git a/apps/frontend/public/icons/platforms/skool.png b/apps/frontend/public/icons/platforms/skool.png new file mode 100644 index 0000000000000000000000000000000000000000..972a5339e15f9c655c4fe6f618345adf6222dda9 GIT binary patch literal 1805 zcmV+o2lDudP)pbtduX#CkGA1#N652SRDQ()+nnJ)f zD1#%e{cb z(lW?W(=y0X(=y0XlXb~0h#;;By8gC~5>R^PcNU;a3t|Y#pigPvzY(v-lnCCI4`HqD zSTs|Y+#{+S61;o@qv&3^X$a1Bz3(j$5fINpGW8x2Ll)J}6%*{sAWKbG49L8K-7vti zG?+Cv6~v(|=8*t~q*>x(Db;WuO{pai+tP>w)w1xPH*HS5Ym&r*X)1cV71ypw$QKpF z5QWmr*4)w{u8Asz=t^8a7A8o$i&*B2ug(c#JDM|tCeOa)(f^XmTLTuQsvwen9M^`m zy%D!wAF%C~fW8k$B(b0uKqFb3;MM<9e(E)JPy?bgl6Po(a0>`CdSzA+(}4Yl9lr6i zHr_pFVp-tYnnL!%Od2?T+UDp9o8LW`WBZm0-~OVHYtN5FEQ9#uFq4nI0+vyC&S(wA zof{F$qS+T}8teijTJz1Hw=ptqkk4u|jz$E+2uf9#u4G0ke3-&MAKISb>2-i?*Dah6`e0?aG)nJrR}A;EfYD z@0>GmZHN=tz1^p$OQBTj5Qb+n@#G=m<0FW3VM#ht7*`c2?%sq>W}mS6))IL7ui0q` zfG`$r>Z|g^_r@?yK*Kvj29H0V$Vb{fv*+^YDme5S$xV|sKY*(mJlSDyJm*QJ@`$XjurVT1-F%=tvSO(e<{JpOt zS-XY+bU7x!Ya`~$c8KS8Z_^mDr{=BsxL!uH@6S2*@Ao)2D%h?3Ziu_UJDEv5aC z@ousux@2uackg{HjkH84oei;F5@=VL1bKg(fx%2)`-X5?pTBzW3qS3KJC-n=~aYE_$ zhtqRp9xSt(46DNTzcRt@yUO&wKc*T9rK*sq`mK<0H2JLN^q|FeepR3v%x&20En=_h z#_Y&8jK#->s2)63-yZ)zAOM~Pdv;Fn?2m@|^@F3_wyA>WDkjT97}W#Wmgb*F9fro{ zm{x@B9qTFHx0%A%ZULvhev+JvzxM#iXt^PN77U_P>X~8=w%io(*aPGA|9FJ&?wVj# zPu!5DCVT_GVlbz9r9mmIRqZ$%J^*1&mJkqAC_jA!=ALP85D{=Zjg!?FmL{$WaVV_p zN!WQ;ncwUgqr0;)J+@rZYtW^L+~+=u3NK1`A_V_9N%Zy*(`j=dAOeU9kL=C!^`8_u z__|H3z|LyKN>?hTZunlgtfJtl*5g!bjEsf%scRsrhY>^Ye|s>ky=Ll9YSg^eDkvWM zWsygp%HukkXI}KU_Uedjn*(~f5{5>FKl~-j*%71egMDyCQdb?f#1bTUx342SbQ5+&>K$Li|Di$acau-$%^pl zn=5n_^t?Mpv)#k}#2Tv4yj~al;#!0UPLX_a9Y(=z`u>btOOtweMOVVOpJpcwxK3KG z=e_jTu{BCTH4rAs!mc}f?)`kJ@hsC#uG&YUgxvN%Xv?H!bj@>mGNAgm&J}lJVtHBJGCIoAQ`EkM$7dBi9*^u%nGNX`{9O zo=o|0y0#bMc$SnaAa>)H&~ft~W4c4UScz`hV6{{OD3#V;3zAN~GOt%!EZu8$#r#3H v46@X;46@X;46@X8#ei7A$mL$dBB=9!eS*|`4+}}u00000NkvXXu0mjf?FW81 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/app/(app)/layout.tsx b/apps/frontend/src/app/(app)/layout.tsx index a6a62c02..c7cf60a4 100644 --- a/apps/frontend/src/app/(app)/layout.tsx +++ b/apps/frontend/src/app/(app)/layout.tsx @@ -78,6 +78,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) { disableImageCompression={!!process.env.DISABLE_IMAGE_COMPRESSION} disableXAnalytics={!!process.env.DISABLE_X_ANALYTICS} sentryDsn={process.env.NEXT_PUBLIC_SENTRY_DSN!} + extensionId={process.env.EXTENSION_ID || ''} language={allHeaders.get(headerName)} transloadit={ process.env.TRANSLOADIT_AUTH && process.env.TRANSLOADIT_TEMPLATE diff --git a/apps/frontend/src/app/(extension)/layout.tsx b/apps/frontend/src/app/(extension)/layout.tsx index 4a02c5ea..3f3d83f7 100644 --- a/apps/frontend/src/app/(extension)/layout.tsx +++ b/apps/frontend/src/app/(extension)/layout.tsx @@ -49,6 +49,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) { disableImageCompression={!!process.env.DISABLE_IMAGE_COMPRESSION} disableXAnalytics={!!process.env.DISABLE_X_ANALYTICS} sentryDsn={process.env.NEXT_PUBLIC_SENTRY_DSN!} + extensionId={process.env.EXTENSION_ID || ''} transloadit={ process.env.TRANSLOADIT_AUTH && process.env.TRANSLOADIT_TEMPLATE ? [ diff --git a/apps/frontend/src/chrome.d.ts b/apps/frontend/src/chrome.d.ts new file mode 100644 index 00000000..be91b792 --- /dev/null +++ b/apps/frontend/src/chrome.d.ts @@ -0,0 +1,15 @@ +/** + * Minimal Chrome extension API types for externally_connectable messaging. + * Web pages listed in an extension's externally_connectable can use + * chrome.runtime.sendMessage to communicate with the extension. + */ +declare namespace chrome { + namespace runtime { + const lastError: { message?: string } | undefined; + function sendMessage( + extensionId: string, + message: any, + callback: (response: any) => void + ): void; + } +} diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index db11ae2f..0442002a 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -251,6 +251,109 @@ export const CustomVariables: FC<{
); }; +const ExtensionNotFound: FC = () => { + const modals = useModals(); + const t = useT(); + return ( +
+

+ {t( + 'extension_not_available', + 'The Postiz browser extension is not installed. You need to install it before connecting this channel.' + )} +

+
+ + +
+
+ ); +}; + +const ChromeExtensionWarning: FC<{ + onConfirm: () => void; + onCancel: () => void; +}> = ({ onConfirm, onCancel }) => { + const modals = useModals(); + const t = useT(); + return ( +
+

+ {t( + 'chrome_extension_warning_intro', + 'This channel connects via the browser extension. Please be aware of the following:' + )} +

+
    +
  • + {t( + 'chrome_extension_warning_tos', + 'Using a browser extension to interact with a platform may violate its terms of service and could result in your account being suspended or banned.' + )} +
  • +
  • + {t( + 'chrome_extension_warning_unstable', + 'This method is not as reliable as native integrations and may experience random disconnections.' + )} +
  • +
  • + {t( + 'chrome_extension_warning_reconnect', + 'You may need to reconnect periodically if the session expires.' + )} +
  • +
  • + We will store your cookies securely to facilitate the connection. +
  • +
  • + Postiz does not take responsibility for any issues arising or account termination due to the use of this method. +
  • +
+
+ + +
+
+ ); +}; + export const AddProviderComponent: FC<{ social: Array<{ identifier: string; @@ -258,6 +361,11 @@ export const AddProviderComponent: FC<{ toolTip?: string; isExternal: boolean; isWeb3: boolean; + isChromeExtension?: boolean; + extensionCookies?: Array<{ + name: string; + domain: string; + }>; customFields?: Array<{ key: string; label: string; @@ -274,7 +382,7 @@ export const AddProviderComponent: FC<{ onboarding?: boolean; }> = (props) => { const { update, social, article, onboarding } = props; - const { isGeneral } = useVariables(); + const { isGeneral, extensionId } = useVariables(); const toaster = useToaster(); const router = useRouter(); const fetch = useFetch(); @@ -285,6 +393,7 @@ export const AddProviderComponent: FC<{ identifier: string, isExternal: boolean, isWeb3: boolean, + isChromeExtension?: boolean, customFields?: Array<{ key: string; label: string; @@ -364,6 +473,106 @@ export const AddProviderComponent: FC<{ openWeb3(); return; } + if (isChromeExtension) { + const confirmed = await new Promise((resolve) => { + modal.openModal({ + title: t('chrome_extension_notice', 'Browser Extension Notice'), + withCloseButton: true, + onClose: () => resolve(false), + children: ( + { + resolve(true); + }} + onCancel={() => { + resolve(false); + }} + /> + ), + }); + }); + if (!confirmed) { + return; + } + if (!extensionId || !chrome?.runtime?.sendMessage) { + modal.openModal({ + title: t('extension_not_available_title', 'Extension Not Found'), + withCloseButton: true, + children: , + }); + return; + } + try { + await new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + extensionId, + { type: 'PING' }, + (response: any) => { + if (chrome.runtime.lastError || !response?.status) { + reject(new Error('Extension not reachable')); + } else { + resolve(); + } + } + ); + }); + } catch { + toaster.show( + t( + 'extension_not_installed', + 'Postiz browser extension is not installed or not reachable.' + ), + 'warning' + ); + return; + } + try { + const cookieResponse = await new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + extensionId, + { type: 'GET_COOKIES', provider: identifier }, + (response: any) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(response); + } + } + ); + }); + if (!cookieResponse.success) { + toaster.show( + cookieResponse.error || + t( + 'extension_cookies_missing', + 'Could not get cookies. Please log in to the platform first.' + ), + 'warning' + ); + return; + } + const { url } = await ( + await fetch( + `/integrations/social/${identifier}${ + onboarding ? '?onboarding=true' : '' + }` + ) + ).json(); + modal.closeAll(); + window.location.href = `/integrations/social/${identifier}?state=${url}&code=${Buffer.from( + JSON.stringify(cookieResponse.cookies) + ).toString('base64')}${onboarding ? '&onboarding=true' : ''}`; + } catch { + toaster.show( + t( + 'extension_communication_error', + 'Failed to communicate with the browser extension.' + ), + 'warning' + ); + } + return; + } if (isExternal) { modal.openModal({ title: 'URL', @@ -415,7 +624,12 @@ export const AddProviderComponent: FC<{ return true; } - return !item.isExternal && !item.isWeb3 && !item.customFields; + return ( + !item.isExternal && + !item.isWeb3 && + !item.isChromeExtension && + !item.customFields + ); }) .map((item) => (
(null); const [twoStepState, setTwoStepState] = useState(null); @@ -135,9 +137,34 @@ export const ContinueIntegration: FC<{ onboarding: resOnboarding, pages, returnURL, + extensionToken, } = await data.json(); const onboarding = resOnboarding || searchParams.onboarding === 'true'; + // Store refresh token in extension for background cookie refresh + if ( + extensionToken && + extensionId && + typeof chrome !== 'undefined' && + chrome?.runtime?.sendMessage + ) { + try { + chrome.runtime.sendMessage( + extensionId, + { + type: 'STORE_REFRESH_TOKEN', + provider, + integrationId: id, + jwt: extensionToken, + backendUrl, + }, + () => {} + ); + } catch { + // Silently ignore — extension may not be available + } + } + // If it's a two-step provider, show the selection UI inline if (inBetweenSteps && !searchParams.refresh) { setTwoStepState({ diff --git a/apps/frontend/src/components/launches/menu/menu.tsx b/apps/frontend/src/components/launches/menu/menu.tsx index 8efd153a..03e8f792 100644 --- a/apps/frontend/src/components/launches/menu/menu.tsx +++ b/apps/frontend/src/components/launches/menu/menu.tsx @@ -25,6 +25,7 @@ import { Integration } from '@prisma/client'; import { SettingsModal } from '@gitroom/frontend/components/launches/settings.modal'; import { CustomVariables } from '@gitroom/frontend/components/launches/add.provider.component'; import { useRouter } from 'next/navigation'; +import { useVariables } from '@gitroom/react/helpers/variable.context'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal'; import dayjs from 'dayjs'; @@ -59,6 +60,7 @@ export const Menu: FC<{ const fetch = useFetch(); const router = useRouter(); + const { extensionId } = useVariables(); const { integrations, reloadCalendarView } = useCalendar(); const toast = useToaster(); const modal = useModals(); @@ -146,10 +148,26 @@ export const Menu: FC<{ ); return; } + // Clean up extension refresh token if applicable + if ( + extensionId && + typeof chrome !== 'undefined' && + chrome?.runtime?.sendMessage + ) { + try { + chrome.runtime.sendMessage( + extensionId, + { type: 'REMOVE_REFRESH_TOKEN', integrationId: id }, + () => {} + ); + } catch { + // Silently ignore + } + } toast.show(t('channel_deleted', 'Channel Deleted'), 'success'); setShow(false); onChange(true); - }, [t]); + }, [t, extensionId, id]); const enableChannel = useCallback(async () => { await fetch('/integrations/enable', { diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index b7592150..511f25d8 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -526,7 +526,7 @@ export const EditorWrapper: FC<{ }; export const Editor: FC<{ - editorType?: 'normal' | 'markdown' | 'html'; + editorType?: 'none' | 'normal' | 'markdown' | 'html'; totalPosts: number; value: string; num?: number; @@ -777,14 +777,18 @@ export const Editor: FC<{ toolBar={
- - + {editorType !== 'none' && ( + <> + + + + )} {(editorType === 'markdown' || editorType === 'html') && identifier !== 'telegram' && ( <> @@ -854,7 +858,7 @@ export const Editor: FC<{ export const OnlyEditor = forwardRef< any, { - editorType: 'normal' | 'markdown' | 'html'; + editorType: 'none' | 'normal' | 'markdown' | 'html'; value: string; onChange: (value: string) => void; paste?: (event: ClipboardEvent | File[]) => void; diff --git a/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx b/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx index 09fbc0fa..b9a3ec8f 100644 --- a/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx @@ -36,6 +36,7 @@ import WordpressProvider from '@gitroom/frontend/components/new-launch/providers import ListmonkProvider from '@gitroom/frontend/components/new-launch/providers/listmonk/listmonk.provider'; import GmbProvider from '@gitroom/frontend/components/new-launch/providers/gmb/gmb.provider'; import MoltbookProvider from '@gitroom/frontend/components/new-launch/providers/moltbook/moltbook.provider'; +import SkoolProvider from '@gitroom/frontend/components/new-launch/providers/skool/skool.provider'; export const Providers = [ { @@ -157,6 +158,10 @@ export const Providers = [ { identifier: 'moltbook', component: MoltbookProvider, + }, + { + identifier: 'skool', + component: SkoolProvider, } ]; export const ShowAllProviders = forwardRef((props, ref) => { diff --git a/apps/frontend/src/components/new-launch/providers/skool/skool.group.select.tsx b/apps/frontend/src/components/new-launch/providers/skool/skool.group.select.tsx new file mode 100644 index 00000000..69dacd1e --- /dev/null +++ b/apps/frontend/src/components/new-launch/providers/skool/skool.group.select.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { FC, useEffect, useState } from 'react'; +import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function'; +import { Select } from '@gitroom/react/form/select'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; +export const SkoolGroupSelect: FC<{ + name: string; + onChange: (event: { + target: { + value: string; + name: string; + }; + }) => void; +}> = (props) => { + const { onChange, name } = props; + const t = useT(); + const customFunc = useCustomProviderFunction(); + const [groups, setGroups] = useState([]); + const { getValues } = useSettings(); + const [currentGroup, setCurrentGroup] = useState(); + const onChangeInner = (event: { + target: { + value: string; + name: string; + }; + }) => { + setCurrentGroup(event.target.value); + onChange(event); + }; + useEffect(() => { + customFunc.get('groups').then((data) => setGroups(data)); + const settings = getValues()[props.name]; + if (settings) { + setCurrentGroup(settings); + } + }, []); + if (!groups.length) { + return null; + } + return ( + + ); +}; diff --git a/apps/frontend/src/components/new-launch/providers/skool/skool.label.select.tsx b/apps/frontend/src/components/new-launch/providers/skool/skool.label.select.tsx new file mode 100644 index 00000000..037e5235 --- /dev/null +++ b/apps/frontend/src/components/new-launch/providers/skool/skool.label.select.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { FC, useEffect, useState } from 'react'; +import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function'; +import { Select } from '@gitroom/react/form/select'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; +export const SkoolLabelSelect: FC<{ + name: string; + groupId: string | undefined; + onChange: (event: { + target: { + value: string; + name: string; + }; + }) => void; +}> = (props) => { + const { onChange, name, groupId } = props; + const t = useT(); + const customFunc = useCustomProviderFunction(); + const [labels, setLabels] = useState([]); + const { getValues } = useSettings(); + const [currentLabel, setCurrentLabel] = useState(); + const onChangeInner = (event: { + target: { + value: string; + name: string; + }; + }) => { + setCurrentLabel(event.target.value); + onChange(event); + }; + useEffect(() => { + if (!groupId) { + setLabels([]); + setCurrentLabel(undefined); + return; + } + customFunc.get('label', { id: groupId }).then((data) => setLabels(data)); + }, [groupId]); + useEffect(() => { + const settings = getValues()[name]; + if (settings) { + setCurrentLabel(settings); + } + }, []); + if (!groupId || !labels.length) { + return null; + } + return ( + + ); +}; diff --git a/apps/frontend/src/components/new-launch/providers/skool/skool.provider.tsx b/apps/frontend/src/components/new-launch/providers/skool/skool.provider.tsx new file mode 100644 index 00000000..9c21abca --- /dev/null +++ b/apps/frontend/src/components/new-launch/providers/skool/skool.provider.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { + withProvider, +} from '@gitroom/frontend/components/new-launch/providers/high.order.provider'; +import { FC, useState } from 'react'; +import { SkoolDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/skool.dto'; +import { SkoolGroupSelect } from '@gitroom/frontend/components/new-launch/providers/skool/skool.group.select'; +import { SkoolLabelSelect } from '@gitroom/frontend/components/new-launch/providers/skool/skool.label.select'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { Input } from '@gitroom/react/form/input'; +const SkoolComponent: FC = () => { + const form = useSettings(); + const [selectedGroup, setSelectedGroup] = useState( + form.getValues().group + ); + const groupRegister = form.register('group'); + const onGroupChange = (event: { target: { value: string; name: string } }) => { + setSelectedGroup(event.target.value); + groupRegister.onChange(event); + }; + return ( +
+ + + +
+ ); +}; +export default withProvider({ + minimumCharacters: [], + SettingsComponent: SkoolComponent, + CustomPreviewComponent: undefined, + dto: SkoolDto, + checkValidity: undefined, + maximumCharacters: 5000, +}); diff --git a/apps/frontend/src/components/new-launch/store.ts b/apps/frontend/src/components/new-launch/store.ts index 879526ea..f848bc60 100644 --- a/apps/frontend/src/components/new-launch/store.ts +++ b/apps/frontend/src/components/new-launch/store.ts @@ -26,7 +26,7 @@ export interface SelectedIntegrations { } interface StoreState { - editor: undefined | 'normal' | 'markdown' | 'html'; + editor: undefined | 'none' | 'normal' | 'markdown' | 'html'; loaded: boolean; date: dayjs.Dayjs; postComment: PostComment; @@ -128,7 +128,7 @@ interface StoreState { setPostComment: (postComment: PostComment) => void; setActivateExitButton?: (activateExitButton: boolean) => void; setDummy: (dummy: boolean) => void; - setEditor: (editor: 'normal' | 'markdown' | 'html') => void; + setEditor: (editor: 'none' | 'normal' | 'markdown' | 'html') => void; setLoaded?: (loaded: boolean) => void; setChars: (id: string, chars: number) => void; chars: Record; @@ -613,7 +613,7 @@ export const useLaunchStore = create()((set) => ({ set((state) => ({ dummy, })), - setEditor: (editor: 'normal' | 'markdown' | 'html') => + setEditor: (editor: 'none' | 'normal' | 'markdown' | 'html') => set((state) => ({ editor, })), diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts index 682fbbc5..7691bb46 100644 --- a/libraries/helpers/src/utils/strip.html.validation.ts +++ b/libraries/helpers/src/utils/strip.html.validation.ts @@ -145,6 +145,16 @@ export const stripHtmlValidation = ( const value = serialize(parseFragment(val)); + if (type === 'none') { + return striptags(value) + .replace(/>/gi, '>') + .replace(/</gi, '<') + .replace(/&/gi, '&') + .replace(/ /gi, ' ') + .replace(/"/gi, '"') + .replace(/'/gi, "'"); + } + if (type === 'html') { return striptags(convertMention(value, convertMentionFunction), [ 'ul', diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts index 2bce9a69..6bf17192 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts @@ -21,6 +21,7 @@ import { GmbSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-s import { FarcasterDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/farcaster.dto'; import { FacebookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/facebook.dto'; import { MoltbookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/moltbook.dto'; +import { SkoolDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/skool.dto'; export type ProviderExtension = { __type: T } & M; export type AllProvidersSettings = @@ -53,7 +54,8 @@ export type AllProvidersSettings = | ProviderExtension<'telegram', None> | ProviderExtension<'nostr', None> | ProviderExtension<'moltbook', MoltbookDto> - | ProviderExtension<'vk', None>; + | ProviderExtension<'vk', None> + | ProviderExtension<'skool', SkoolDto>; type None = NonNullable; @@ -89,6 +91,7 @@ export const allProviders = (setEmpty?: any) => { { value: setEmpty, name: 'nostr' }, { value: setEmpty, name: 'vk' }, { value: MoltbookDto, name: 'moltbook' }, + { value: SkoolDto, name: 'skool' }, ].filter((f) => f.value); }; diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/skool.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/skool.dto.ts new file mode 100644 index 00000000..84eac005 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/skool.dto.ts @@ -0,0 +1,28 @@ +import { IsDefined, IsString, MinLength } from 'class-validator'; +import { JSONSchema } from 'class-validator-jsonschema'; + +export class SkoolDto { + @MinLength(1) + @IsDefined() + @IsString() + @JSONSchema({ + description: 'Group must be an id', + }) + group: string; + + @MinLength(1) + @IsDefined() + @IsString() + @JSONSchema({ + description: 'Label must be an id', + }) + label: string; + + @MinLength(1) + @IsDefined() + @IsString() + @JSONSchema({ + description: 'Title of the post', + }) + title: string; +} diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index 0bb78338..ba2ff607 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -33,6 +33,7 @@ import { KickProvider } from '@gitroom/nestjs-libraries/integrations/social/kick import { TwitchProvider } from '@gitroom/nestjs-libraries/integrations/social/twitch.provider'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; import { MoltbookProvider } from '@gitroom/nestjs-libraries/integrations/social/moltbook.provider'; +import { SkoolProvider } from '@gitroom/nestjs-libraries/integrations/social/skool.provider'; export const socialIntegrationList: Array = [ new XProvider(), @@ -65,6 +66,7 @@ export const socialIntegrationList: Array = [ new WordpressProvider(), new ListmonkProvider(), new MoltbookProvider(), + new SkoolProvider(), // new MastodonCustomProvider(), ]; @@ -80,6 +82,8 @@ export class IntegrationManager { editor: p.editor, isExternal: !!p.externalUrl, isWeb3: !!p.isWeb3, + isChromeExtension: !!p.isChromeExtension, + ...(p.extensionCookies ? { extensionCookies: p.extensionCookies } : {}), ...(p.customFields ? { customFields: await p.customFields() } : {}), })) ), diff --git a/libraries/nestjs-libraries/src/integrations/social/skool.provider.ts b/libraries/nestjs-libraries/src/integrations/social/skool.provider.ts new file mode 100644 index 00000000..78151dbf --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/skool.provider.ts @@ -0,0 +1,333 @@ +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { SocialAbstract } from '../social.abstract'; +import { + AuthTokenDetails, + MediaContent, + PostDetails, + PostResponse, + SocialProvider, +} from './social.integrations.interface'; +import dayjs from 'dayjs'; +import { Integration } from '@prisma/client'; +import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator'; +import { SkoolDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/skool.dto'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; + +export class SkoolProvider extends SocialAbstract implements SocialProvider { + identifier = 'skool'; + name = 'Skool'; + isBetweenSteps = false; + isChromeExtension = true; + scopes = [] as string[]; + editor = 'normal' as const; + dto = SkoolDto; + + extensionCookies = [ + { name: 'client_id', domain: '.skool.com' }, + { name: 'auth_token', domain: '.skool.com' }, + ]; + + private getCookies(integration: Integration): { + client_id: string; + auth_token: string; + } { + return AuthService.verifyJWT(integration.customInstanceDetails!) as { + client_id: string; + auth_token: string; + }; + } + + override handleErrors( + body: string + ): + | { type: 'refresh-token' | 'bad-body' | 'retry'; value: string } + | undefined { + if (body.includes('must be admin or level')) { + return { type: 'bad-body', value: 'You can\'t post to this channel' }; + } + if (body.includes('cannot post to this label')) { + return { type: 'bad-body', value: 'Cannot post to this label' }; + } + return undefined; + } + + maxLength() { + return 5000; + } + + async refreshToken(refreshToken: string): Promise { + return { + refreshToken: '', + expiresIn: 0, + accessToken: '', + id: '', + name: '', + picture: '', + username: '', + }; + } + + async generateAuthUrl() { + const state = makeId(6); + return { + url: state, + codeVerifier: makeId(10), + state, + }; + } + + async authenticate(params: { + code: string; + codeVerifier: string; + refresh?: string; + }) { + try { + const cookies: Record = JSON.parse( + Buffer.from(params.code, 'base64').toString() + ); + + const missing = this.extensionCookies + .map((c) => c.name) + .filter((name) => !cookies[name]); + + if (missing.length > 0) { + return `Missing required cookies: ${missing.join(', ')}`; + } + + const data = await ( + await fetch('https://api2.skool.com/self', { + method: 'GET', + headers: { + Cookie: `auth_token=${cookies.auth_token}; client_id=${cookies.client_id}`, + }, + }) + ).json(); + + return { + refreshToken: '', + expiresIn: dayjs().add(100, 'year').unix() - dayjs().unix(), + accessToken: AuthService.signJWT(cookies), + id: data.id, + name: data.first_name + ' ' + data.last_name, + picture: data.metadata.picture_profile || '', + username: data.name, + }; + } catch (e) { + return 'Invalid cookie data'; + } + } + + @Tool({ description: 'Groups', dataSchema: [] }) + async groups(accessToken: string, params: any, id: string, integration: Integration) { + try { + const { client_id, auth_token } = this.getCookies(integration); + const { groups } = await ( + await fetch( + `https://api2.skool.com/users/${id}/groups?offset=0&limit=30`, + { + headers: { + Cookie: `auth_token=${auth_token}; client_id=${client_id}`, + }, + } + ) + ).json(); + + return groups.map((p: any) => ({ + id: String(p.id), + name: p.metadata.display_name, + })); + } catch (err) { + return []; + } + } + + @Tool({ description: 'Label', dataSchema: [] }) + async label(accessToken: string, params: any, id: string, integration: Integration) { + try { + const { client_id, auth_token } = this.getCookies(integration); + const { metadata } = await ( + await this.fetch(`https://api2.skool.com/groups/${params.id}`, { + headers: { + Cookie: `auth_token=${auth_token}; client_id=${client_id}`, + }, + }) + ).json(); + + if (!metadata.labels || metadata.labels.length === 0) { + return [{ id: 'none', name: 'Default Label' }]; + } + + const labels = metadata.labels.split(','); + + if (labels.length === 0) { + return [{ id: 'none', name: 'Default Label' }]; + } + + const labelInformation = await Promise.all( + labels.map(async (labelId: string) => { + return ( + await this.fetch(`https://api2.skool.com/labels/${labelId}`, { + headers: { + Cookie: `auth_token=${auth_token}; client_id=${client_id}`, + }, + }) + ).json(); + }) + ); + + return labelInformation.map((p: any) => ({ + id: String(p.id), + name: p.metadata.display_name, + })); + } catch (err) { + return []; + } + } + + private async uploadMediaToSkool( + media: MediaContent[], + userId: string, + cookies: { client_id: string; auth_token: string } + ): Promise { + if (!media || media.length === 0) return ''; + + const fileIds: string[] = []; + + for (const item of media) { + const fileResponse = await fetch(item.path); + const fileBuffer = await fileResponse.arrayBuffer(); + const contentType = + fileResponse.headers.get('content-type') || 'application/octet-stream'; + const fileName = item.path.split('/').pop() || 'file'; + + const createFileResponse = await ( + await this.fetch('https://api2.skool.com/files', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: `auth_token=${cookies.auth_token}; client_id=${cookies.client_id}`, + }, + body: JSON.stringify({ + file_name: fileName, + content_type: contentType, + content_length: fileBuffer.byteLength, + content_disposition: '', + ref: '', + owner_id: userId, + large_thumbnail: false, + }), + }, 'create file record') + ).json(); + + await fetch(createFileResponse.write_url, { + method: 'PUT', + headers: { + 'Content-Type': createFileResponse.content_type, + 'x-amz-acl': createFileResponse.acl, + }, + body: fileBuffer, + }); + + fileIds.push(createFileResponse.file.id); + } + + return fileIds.join(','); + } + + async post( + id: string, + accessToken: string, + postDetails: PostDetails[], + integration: Integration + ): Promise { + const { client_id, auth_token } = this.getCookies(integration); + const [post] = postDetails; + + const attachments = await this.uploadMediaToSkool( + post.media || [], + id, + { client_id, auth_token } + ); + + const { id: postId, name } = await ( + await this.fetch('https://api2.skool.com/posts?follow=true', { + method: 'POST', + headers: { + Cookie: `auth_token=${auth_token}; client_id=${client_id}`, + }, + body: JSON.stringify({ + post_type: 'generic', + group_id: post.settings.group, + metadata: { + title: post.settings.title, + content: post.message, + attachments, + ...(post.settings.label && post.settings.label !== 'none' + ? { labels: post.settings.label } + : {}), + action: 0, + video_ids: '', + }, + }), + }) + ).json(); + + return [ + { + id: String(postId), + postId, + releaseURL: `https://www.skool.com/${post.settings.group}/${name}`, + status: 'success', + }, + ]; + } + + async comment( + id: string, + postId: string, + lastCommentId: string | undefined, + accessToken: string, + postDetails: PostDetails[], + integration: Integration + ): Promise { + const { client_id, auth_token } = this.getCookies(integration); + const [post] = postDetails; + + const attachments = await this.uploadMediaToSkool( + post.media || [], + id, + { client_id, auth_token } + ); + + const { id: postIdFinal, name } = await ( + await this.fetch('https://api2.skool.com/posts?follow=true', { + method: 'POST', + headers: { + Cookie: `auth_token=${auth_token}; client_id=${client_id}`, + }, + body: JSON.stringify({ + post_type: 'comment', + group_id: post.settings.group, + root_id: postId, + parent_id: lastCommentId || postId, + metadata: { + title: '', + content: post.message, + attachments, + action: 0, + video_ids: '', + }, + }), + }) + ).json(); + + return [ + { + id: String(id), + postId: postIdFinal, + releaseURL: `https://www.skool.com/${post.settings.group}/${name}`, + status: 'success', + }, + ]; + } +} 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 377621fb..bca6eb5f 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -141,7 +141,9 @@ export interface SocialProvider dto?: any; maxLength: (additionalSettings?: any) => number; isWeb3?: boolean; - editor: 'normal' | 'markdown' | 'html'; + isChromeExtension?: boolean; + extensionCookies?: { name: string; domain: string }[]; + editor: 'none' | 'normal' | 'markdown' | 'html'; customFields?: () => Promise< { key: string; diff --git a/libraries/react-shared-libraries/src/helpers/variable.context.tsx b/libraries/react-shared-libraries/src/helpers/variable.context.tsx index 1b81f7c2..5bbe64a0 100644 --- a/libraries/react-shared-libraries/src/helpers/variable.context.tsx +++ b/libraries/react-shared-libraries/src/helpers/variable.context.tsx @@ -25,6 +25,7 @@ interface VariableContextInterface { dub: boolean; transloadit: string[]; sentryDsn: string; + extensionId: string; } const VariableContext = createContext({ stripeClient: '', @@ -49,6 +50,7 @@ const VariableContext = createContext({ dub: false, transloadit: [], sentryDsn: '', + extensionId: '', } as VariableContextInterface); export const VariableContextComponent: FC< VariableContextInterface & {