From 8bd9d78bd94cdb2c14262ae512735c97b4903ed5 Mon Sep 17 00:00:00 2001 From: Fairpost Date: Wed, 17 Sep 2025 12:19:19 +0200 Subject: [PATCH 1/2] feat: Setup LinkedIn Oauth setup --- src/platforms/Bluesky/Bluesky.ts | 2 +- src/platforms/Facebook/Facebook.ts | 11 ++- src/platforms/Instagram/Instagram.ts | 11 ++- src/platforms/LinkedIn/LinkedIn.ts | 15 +++- src/platforms/LinkedIn/LinkedInAuth.ts | 116 ++++++++++++++++--------- src/platforms/Reddit/Reddit.ts | 11 ++- src/platforms/Twitter/Twitter.ts | 11 ++- src/platforms/YouTube/YouTube.ts | 11 ++- 8 files changed, 127 insertions(+), 61 deletions(-) diff --git a/src/platforms/Bluesky/Bluesky.ts b/src/platforms/Bluesky/Bluesky.ts index 1150d06..b7d28b5 100644 --- a/src/platforms/Bluesky/Bluesky.ts +++ b/src/platforms/Bluesky/Bluesky.ts @@ -49,7 +49,7 @@ export default class Bluesky extends Platform { return await this.test(); } throw this.user.log.error( - `Platform.setup: ui ${operator.ui} not supported`, + `${this.id} setup: ui ${operator.ui} not supported`, ); } diff --git a/src/platforms/Facebook/Facebook.ts b/src/platforms/Facebook/Facebook.ts index 8c94b5c..a7bf0d7 100644 --- a/src/platforms/Facebook/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -72,10 +72,15 @@ export default class Facebook extends Platform { await this.auth.setupCli(); return await this.test(); } - if (!payload) { - throw this.user.log.error("Setup via api requires a payload"); + if (operator.ui === "api") { + if (!payload) { + throw this.user.log.error("Setup via api requires a payload"); + } + return this.auth.setupApi(payload); } - return this.auth.setupApi(payload); + throw this.user.log.error( + `${this.id} setup: ui ${operator.ui} not supported`, + ); } /** @inheritdoc */ diff --git a/src/platforms/Instagram/Instagram.ts b/src/platforms/Instagram/Instagram.ts index fc9c9f9..9f3d5f2 100644 --- a/src/platforms/Instagram/Instagram.ts +++ b/src/platforms/Instagram/Instagram.ts @@ -76,10 +76,15 @@ export default class Instagram extends Platform { await this.auth.setupCli(); return await this.test(); } - if (!payload) { - throw this.user.log.error("Setup via api requires a payload"); + if (operator.ui === "api") { + if (!payload) { + throw this.user.log.error("Setup via api requires a payload"); + } + return this.auth.setupApi(payload); } - return this.auth.setupApi(payload); + throw this.user.log.error( + `${this.id} setup: ui ${operator.ui} not supported`, + ); } /** @inheritdoc */ diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index cffb638..9e28e29 100644 --- a/src/platforms/LinkedIn/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -67,10 +67,19 @@ export default class LinkedIn extends Platform { await this.auth.setupCli(); return await this.test(); } - if (!payload) { - throw this.user.log.error("Setup via api requires a payload"); + if (operator.ui === "api") { + if (!payload) { + throw this.user.log.error("Setup via api requires a payload"); + } + const result = await this.auth.setupApi(payload); + const ready = "ready" in result && result.ready; + if (!ready) return result; + return await this.test(); } - return this.auth.setupApi(payload); + + throw this.user.log.error( + `${this.id} setup: ui ${operator.ui} not supported`, + ); } /** @inheritdoc */ diff --git a/src/platforms/LinkedIn/LinkedInAuth.ts b/src/platforms/LinkedIn/LinkedInAuth.ts index 231e892..b70fc80 100644 --- a/src/platforms/LinkedIn/LinkedInAuth.ts +++ b/src/platforms/LinkedIn/LinkedInAuth.ts @@ -21,54 +21,55 @@ export default class LinkedInAuth { * Set up LinkedIn platform */ async setupCli() { - const code = await this.requestCode(); - const tokens = await this.exchangeCode(code); + const clientHost = this.user.data.get("app", "OAUTH_HOSTNAME"); + const clientPort = Number(this.user.data.get("app", "OAUTH_PORT")); + const redirectUri = OAuth2Service.getCallbackUrl(clientHost, clientPort); + const code = await this.requestCliCode(redirectUri); + const tokens = await this.exchangeCode(code, redirectUri); await this.store(tokens); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async setupApi(payload: object) { - throw this.user.log.error("LinkedInAuth:setupApi - not implemented"); - } - - /** - * Refresh LinkedIn tokens - */ - async refresh() { - const tokens = (await this.post("accessToken", { - grant_type: "refresh_token", - refresh_token: this.user.data.get("auth", "LINKEDIN_REFRESH_TOKEN"), - client_id: this.user.data.get("app", "LINKEDIN_CLIENT_ID"), - client_secret: this.user.data.get("app", "LINKEDIN_CLIENT_SECRET"), - })) as TokenResponse; - - if (!isTokenResponse(tokens)) { - throw this.user.log.error( - "LinkedInAuth.refresh: response is not a TokenResponse", - tokens, - ); + async setupApi(payload: { + state?: string; + redirect_uri?: string; + code?: string; + error?: string; + error_uri?: string; + error_description?: string; + }) { + if (payload["error"]) { + const msg = payload["error"] + " - " + payload["error_description"]; + throw this.user.log.error(msg, payload); + } + if (!payload.redirect_uri) { + throw this.user.log.error("LinkedInAuth.setup: Invalid payload", payload); } + if (!payload.code) { + return { + url: this.getRequestUrl(payload.redirect_uri, payload.state), + }; + } + const tokens = await this.exchangeCode(payload.code, payload.redirect_uri); await this.store(tokens); + return { + ready: true, + }; } /** - * Request remote code using OAuth2Service - * @returns - code + * Get oath2 url to request a code + * @param redirectUri + * @param state + * @returns - string */ - private async requestCode(): Promise { - this.user.log.trace("LinkedInAuth", "requestCode"); + private getRequestUrl(redirectUri: string, state?: string): string { const clientId = this.user.data.get("app", "LINKEDIN_CLIENT_ID"); - const clientHost = this.user.data.get("app", "OAUTH_HOSTNAME"); - const clientPort = Number(this.user.data.get("app", "OAUTH_PORT")); - const state = String(Math.random()).substring(2); - - // create auth url const url = new URL("https://www.linkedin.com"); url.pathname = "oauth/" + this.API_VERSION + "/authorization"; const query = { client_id: clientId, - redirect_uri: OAuth2Service.getCallbackUrl(clientHost, clientPort), - state: state, + redirect_uri: redirectUri, + ...(state ? { state: state } : {}), response_type: "code", duration: "permanent", scope: [ @@ -78,12 +79,23 @@ export default class LinkedInAuth { ].join(" "), }; url.search = new URLSearchParams(query).toString(); + return url.href; + } + /** + * Request remote code using OAuth2Service as a local server + * @param redirectUri + * @returns - code + */ + private async requestCliCode(redirectUri: string): Promise { + this.user.log.trace("LinkedInAuth", "requestCode"); + const state = String(Math.random()).substring(2); + const requestUrl = this.getRequestUrl(redirectUri, state); const result = await OAuth2Service.requestRemotePermissions( "LinkedIn", - url.href, - clientHost, - clientPort, + requestUrl, + this.user.data.get("app", "OAUTH_HOSTNAME"), + Number(this.user.data.get("app", "OAUTH_PORT")), ); if (result["error"]) { const msg = result["error_reason"] + " - " + result["error_description"]; @@ -103,14 +115,14 @@ export default class LinkedInAuth { /** * Exchange remote code for tokens * @param code - the code to exchange + * @param redirectUri * @returns - TokenResponse */ - private async exchangeCode(code: string): Promise { + private async exchangeCode( + code: string, + redirectUri: string, + ): Promise { this.user.log.trace("LinkedInAuth", "exchangeCode", code); - const clientHost = this.user.data.get("app", "OAUTH_HOSTNAME"); - const clientPort = Number(this.user.data.get("app", "OAUTH_PORT")); - const redirectUri = OAuth2Service.getCallbackUrl(clientHost, clientPort); - const tokens = (await this.post("accessToken", { grant_type: "authorization_code", code: code, @@ -126,6 +138,26 @@ export default class LinkedInAuth { return tokens; } + /** + * Refresh LinkedIn tokens + */ + async refresh() { + const tokens = (await this.post("accessToken", { + grant_type: "refresh_token", + refresh_token: this.user.data.get("auth", "LINKEDIN_REFRESH_TOKEN"), + client_id: this.user.data.get("app", "LINKEDIN_CLIENT_ID"), + client_secret: this.user.data.get("app", "LINKEDIN_CLIENT_SECRET"), + })) as TokenResponse; + + if (!isTokenResponse(tokens)) { + throw this.user.log.error( + "LinkedInAuth.refresh: response is not a TokenResponse", + tokens, + ); + } + await this.store(tokens); + } + /** * Save all tokens in auth store * @param tokens - the tokens to store diff --git a/src/platforms/Reddit/Reddit.ts b/src/platforms/Reddit/Reddit.ts index 57b817d..bcb1767 100644 --- a/src/platforms/Reddit/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -62,10 +62,15 @@ export default class Reddit extends Platform { await this.auth.setupCli(); return await this.test(); } - if (!payload) { - throw this.user.log.error("Setup via api requires a payload"); + if (operator.ui === "api") { + if (!payload) { + throw this.user.log.error("Setup via api requires a payload"); + } + return this.auth.setupApi(payload); } - return this.auth.setupApi(payload); + throw this.user.log.error( + `${this.id} setup: ui ${operator.ui} not supported`, + ); } /** @inheritdoc */ diff --git a/src/platforms/Twitter/Twitter.ts b/src/platforms/Twitter/Twitter.ts index ee97e4b..6d149d8 100644 --- a/src/platforms/Twitter/Twitter.ts +++ b/src/platforms/Twitter/Twitter.ts @@ -67,10 +67,15 @@ export default class Twitter extends Platform { await this.auth.setupCli(); return await this.test(); } - if (!payload) { - throw this.user.log.error("Setup via api requires a payload"); + if (operator.ui === "api") { + if (!payload) { + throw this.user.log.error("Setup via api requires a payload"); + } + return this.auth.setupApi(payload); } - return this.auth.setupApi(payload); + throw this.user.log.error( + `${this.id} setup: ui ${operator.ui} not supported`, + ); } /** @inheritdoc */ diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts index 7382743..85c55ae 100644 --- a/src/platforms/YouTube/YouTube.ts +++ b/src/platforms/YouTube/YouTube.ts @@ -67,10 +67,15 @@ export default class YouTube extends Platform { await this.auth.setupCli(); return await this.test(); } - if (!payload) { - throw this.user.log.error("Setup via api requires a payload"); + if (operator.ui === "api") { + if (!payload) { + throw this.user.log.error("Setup via api requires a payload"); + } + return this.auth.setupApi(payload); } - return this.auth.setupApi(payload); + throw this.user.log.error( + `${this.id} setup: ui ${operator.ui} not supported`, + ); } /** @inheritdoc */ From c1b1dfbd20a4a590fc030f312e9469a03bad48aa Mon Sep 17 00:00:00 2001 From: Fairpost Date: Wed, 17 Sep 2025 12:39:15 +0200 Subject: [PATCH 2/2] feat: Add test to result --- src/platforms/LinkedIn/LinkedIn.ts | 5 ++++- src/platforms/LinkedIn/LinkedInAuth.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index 9e28e29..7ca6dd3 100644 --- a/src/platforms/LinkedIn/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -74,7 +74,10 @@ export default class LinkedIn extends Platform { const result = await this.auth.setupApi(payload); const ready = "ready" in result && result.ready; if (!ready) return result; - return await this.test(); + return { + ...result, + test: await this.test(), + }; } throw this.user.log.error( diff --git a/src/platforms/LinkedIn/LinkedInAuth.ts b/src/platforms/LinkedIn/LinkedInAuth.ts index b70fc80..e54f4ec 100644 --- a/src/platforms/LinkedIn/LinkedInAuth.ts +++ b/src/platforms/LinkedIn/LinkedInAuth.ts @@ -36,7 +36,7 @@ export default class LinkedInAuth { error?: string; error_uri?: string; error_description?: string; - }) { + }): Promise<{ url?: string; ready?: boolean }> { if (payload["error"]) { const msg = payload["error"] + " - " + payload["error_description"]; throw this.user.log.error(msg, payload); @@ -122,7 +122,7 @@ export default class LinkedInAuth { code: string, redirectUri: string, ): Promise { - this.user.log.trace("LinkedInAuth", "exchangeCode", code); + this.user.log.trace("LinkedInAuth", "exchangeCode"); const tokens = (await this.post("accessToken", { grant_type: "authorization_code", code: code,