From a7145711504998bf893b0baa3acdf29001fdb110 Mon Sep 17 00:00:00 2001 From: "pike @ minimoon" Date: Sat, 9 Dec 2023 13:57:02 +0100 Subject: [PATCH 1/6] feat: Move platforms into folders --- src/platforms/Ayrshare/AsFacebook.ts | 2 +- src/platforms/Ayrshare/AsInstagram.ts | 2 +- src/platforms/Ayrshare/AsLinkedIn.ts | 2 +- src/platforms/Ayrshare/AsReddit.ts | 2 +- src/platforms/Ayrshare/AsTikTok.ts | 2 +- src/platforms/Ayrshare/AsTwitter.ts | 2 +- src/platforms/Ayrshare/AsYouTube.ts | 2 +- src/platforms/{ => Ayrshare}/Ayrshare.ts | 14 +++++++------- src/platforms/{ => Facebook}/Facebook.ts | 16 ++++++++-------- src/{auth => platforms/Facebook}/FacebookAuth.ts | 6 +++--- src/platforms/{ => Instagram}/Instagram.ts | 16 ++++++++-------- .../Instagram}/InstagramAuth.ts | 6 +++--- src/platforms/{ => LinkedIn}/LinkedIn.ts | 16 ++++++++-------- src/{auth => platforms/LinkedIn}/LinkedInAuth.ts | 6 +++--- src/platforms/{ => Reddit}/Reddit.ts | 16 ++++++++-------- src/{auth => platforms/Reddit}/RedditAuth.ts | 6 +++--- src/platforms/{ => Twitter}/Twitter.ts | 14 +++++++------- src/{auth => platforms/Twitter}/TwitterAuth.ts | 6 +++--- src/platforms/index.ts | 10 +++++----- 19 files changed, 73 insertions(+), 73 deletions(-) rename src/platforms/{ => Ayrshare}/Ayrshare.ts (95%) rename src/platforms/{ => Facebook}/Facebook.ts (96%) rename src/{auth => platforms/Facebook}/FacebookAuth.ts (97%) rename src/platforms/{ => Instagram}/Instagram.ts (97%) rename src/{auth => platforms/Instagram}/InstagramAuth.ts (93%) rename src/platforms/{ => LinkedIn}/LinkedIn.ts (97%) rename src/{auth => platforms/LinkedIn}/LinkedInAuth.ts (97%) rename src/platforms/{ => Reddit}/Reddit.ts (97%) rename src/{auth => platforms/Reddit}/RedditAuth.ts (97%) rename src/platforms/{ => Twitter}/Twitter.ts (92%) rename src/{auth => platforms/Twitter}/TwitterAuth.ts (92%) diff --git a/src/platforms/Ayrshare/AsFacebook.ts b/src/platforms/Ayrshare/AsFacebook.ts index 2b887f9..f9774cb 100644 --- a/src/platforms/Ayrshare/AsFacebook.ts +++ b/src/platforms/Ayrshare/AsFacebook.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as sharp from "sharp"; -import Ayrshare from "../Ayrshare"; +import Ayrshare from "./Ayrshare"; import Folder from "../../models/Folder"; import { PlatformId } from ".."; import Post from "../../models/Post"; diff --git a/src/platforms/Ayrshare/AsInstagram.ts b/src/platforms/Ayrshare/AsInstagram.ts index 571138d..26ce19a 100644 --- a/src/platforms/Ayrshare/AsInstagram.ts +++ b/src/platforms/Ayrshare/AsInstagram.ts @@ -1,7 +1,7 @@ import * as path from "path"; import * as sharp from "sharp"; -import Ayrshare from "../Ayrshare"; +import Ayrshare from "./Ayrshare"; import Folder from "../../models/Folder"; import Logger from "../../services/Logger"; import { PlatformId } from ".."; diff --git a/src/platforms/Ayrshare/AsLinkedIn.ts b/src/platforms/Ayrshare/AsLinkedIn.ts index 871943b..05c1138 100644 --- a/src/platforms/Ayrshare/AsLinkedIn.ts +++ b/src/platforms/Ayrshare/AsLinkedIn.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as sharp from "sharp"; -import Ayrshare from "../Ayrshare"; +import Ayrshare from "./Ayrshare"; import Folder from "../../models/Folder"; import Logger from "../../services/Logger"; import { PlatformId } from ".."; diff --git a/src/platforms/Ayrshare/AsReddit.ts b/src/platforms/Ayrshare/AsReddit.ts index 054394b..245538f 100644 --- a/src/platforms/Ayrshare/AsReddit.ts +++ b/src/platforms/Ayrshare/AsReddit.ts @@ -1,4 +1,4 @@ -import Ayrshare from "../Ayrshare"; +import Ayrshare from "./Ayrshare"; import Folder from "../../models/Folder"; import { PlatformId } from ".."; import Post from "../../models/Post"; diff --git a/src/platforms/Ayrshare/AsTikTok.ts b/src/platforms/Ayrshare/AsTikTok.ts index 2c571c4..bb8d668 100644 --- a/src/platforms/Ayrshare/AsTikTok.ts +++ b/src/platforms/Ayrshare/AsTikTok.ts @@ -1,4 +1,4 @@ -import Ayrshare from "../Ayrshare"; +import Ayrshare from "./Ayrshare"; import Folder from "../../models/Folder"; import { PlatformId } from ".."; import Post from "../../models/Post"; diff --git a/src/platforms/Ayrshare/AsTwitter.ts b/src/platforms/Ayrshare/AsTwitter.ts index c23096f..d568017 100644 --- a/src/platforms/Ayrshare/AsTwitter.ts +++ b/src/platforms/Ayrshare/AsTwitter.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as sharp from "sharp"; -import Ayrshare from "../Ayrshare"; +import Ayrshare from "./Ayrshare"; import Folder from "../../models/Folder"; import Logger from "../../services/Logger"; import { PlatformId } from ".."; diff --git a/src/platforms/Ayrshare/AsYouTube.ts b/src/platforms/Ayrshare/AsYouTube.ts index 16f19ab..882837d 100644 --- a/src/platforms/Ayrshare/AsYouTube.ts +++ b/src/platforms/Ayrshare/AsYouTube.ts @@ -1,4 +1,4 @@ -import Ayrshare from "../Ayrshare"; +import Ayrshare from "./Ayrshare"; import Folder from "../../models/Folder"; import { PlatformId } from ".."; import Post from "../../models/Post"; diff --git a/src/platforms/Ayrshare.ts b/src/platforms/Ayrshare/Ayrshare.ts similarity index 95% rename from src/platforms/Ayrshare.ts rename to src/platforms/Ayrshare/Ayrshare.ts index 187686c..5a89d25 100644 --- a/src/platforms/Ayrshare.ts +++ b/src/platforms/Ayrshare/Ayrshare.ts @@ -1,13 +1,13 @@ import * as fs from "fs"; import * as path from "path"; -import Folder from "../models/Folder"; -import Logger from "../services/Logger"; -import Platform from "../models/Platform"; -import { PlatformId } from "."; -import Post from "../models/Post"; -import { PostStatus } from "../models/Post"; -import Storage from "../services/Storage"; +import Folder from "../../models/Folder"; +import Logger from "../../services/Logger"; +import Platform from "../../models/Platform"; +import { PlatformId } from ".."; +import Post from "../../models/Post"; +import { PostStatus } from "../../models/Post"; +import Storage from "../../services/Storage"; import { randomUUID } from "crypto"; /** diff --git a/src/platforms/Facebook.ts b/src/platforms/Facebook/Facebook.ts similarity index 96% rename from src/platforms/Facebook.ts rename to src/platforms/Facebook/Facebook.ts index adc2bf6..04ef293 100644 --- a/src/platforms/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -2,14 +2,14 @@ import * as fs from "fs"; import * as path from "path"; import * as sharp from "sharp"; -import FacebookAuth from "../auth/FacebookAuth"; -import Folder from "../models/Folder"; -import Logger from "../services/Logger"; -import Platform from "../models/Platform"; -import { PlatformId } from "."; -import Post from "../models/Post"; -import { PostStatus } from "../models/Post"; -import Storage from "../services/Storage"; +import FacebookAuth from "./FacebookAuth"; +import Folder from "../../models/Folder"; +import Logger from "../../services/Logger"; +import Platform from "../../models/Platform"; +import { PlatformId } from ".."; +import Post from "../../models/Post"; +import { PostStatus } from "../../models/Post"; +import Storage from "../../services/Storage"; /** * Facebook: support for facebook platform. diff --git a/src/auth/FacebookAuth.ts b/src/platforms/Facebook/FacebookAuth.ts similarity index 97% rename from src/auth/FacebookAuth.ts rename to src/platforms/Facebook/FacebookAuth.ts index c3e9925..6535e53 100644 --- a/src/auth/FacebookAuth.ts +++ b/src/platforms/Facebook/FacebookAuth.ts @@ -1,6 +1,6 @@ -import Logger from "../services/Logger"; -import OAuth2Client from "./OAuth2Client"; -import Storage from "../services/Storage"; +import Logger from "../../services/Logger"; +import OAuth2Client from "../../auth/OAuth2Client"; +import Storage from "../../services/Storage"; export default class FacebookAuth extends OAuth2Client { GRAPH_API_VERSION: string = "v18.0"; diff --git a/src/platforms/Instagram.ts b/src/platforms/Instagram/Instagram.ts similarity index 97% rename from src/platforms/Instagram.ts rename to src/platforms/Instagram/Instagram.ts index 6b92882..3d28f4a 100644 --- a/src/platforms/Instagram.ts +++ b/src/platforms/Instagram/Instagram.ts @@ -2,14 +2,14 @@ import * as fs from "fs"; import * as path from "path"; import * as sharp from "sharp"; -import Folder from "../models/Folder"; -import InstagramAuth from "../auth/InstagramAuth"; -import Logger from "../services/Logger"; -import Platform from "../models/Platform"; -import { PlatformId } from "."; -import Post from "../models/Post"; -import { PostStatus } from "../models/Post"; -import Storage from "../services/Storage"; +import Folder from "../../models/Folder"; +import InstagramAuth from "./InstagramAuth"; +import Logger from "../../services/Logger"; +import Platform from "../../models/Platform"; +import { PlatformId } from ".."; +import Post from "../../models/Post"; +import { PostStatus } from "../../models/Post"; +import Storage from "../../services/Storage"; /** * Instagram: support for instagram platform. diff --git a/src/auth/InstagramAuth.ts b/src/platforms/Instagram/InstagramAuth.ts similarity index 93% rename from src/auth/InstagramAuth.ts rename to src/platforms/Instagram/InstagramAuth.ts index cae66c7..3ae50d9 100644 --- a/src/auth/InstagramAuth.ts +++ b/src/platforms/Instagram/InstagramAuth.ts @@ -1,6 +1,6 @@ -import FacebookAuth from "./FacebookAuth"; -import Logger from "../services/Logger"; -import Storage from "../services/Storage"; +import FacebookAuth from "../Facebook/FacebookAuth"; +import Logger from "../../services/Logger"; +import Storage from "../../services/Storage"; export default class InstagramAuth extends FacebookAuth { async setup() { diff --git a/src/platforms/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts similarity index 97% rename from src/platforms/LinkedIn.ts rename to src/platforms/LinkedIn/LinkedIn.ts index 65b70f2..a51fab3 100644 --- a/src/platforms/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -2,14 +2,14 @@ import * as fs from "fs"; //import * as path from "path"; import * as sharp from "sharp"; -import Folder from "../models/Folder"; -import LinkedInAuth from "../auth/LinkedInAuth"; -import Logger from "../services/Logger"; -import Platform from "../models/Platform"; -import { PlatformId } from "."; -import Post from "../models/Post"; -import { PostStatus } from "../models/Post"; -import Storage from "../services/Storage"; +import Folder from "../../models/Folder"; +import LinkedInAuth from "./LinkedInAuth"; +import Logger from "../../services/Logger"; +import Platform from "../../models/Platform"; +import { PlatformId } from ".."; +import Post from "../../models/Post"; +import { PostStatus } from "../../models/Post"; +import Storage from "../../services/Storage"; export default class LinkedIn extends Platform { id: PlatformId = PlatformId.LINKEDIN; diff --git a/src/auth/LinkedInAuth.ts b/src/platforms/LinkedIn/LinkedInAuth.ts similarity index 97% rename from src/auth/LinkedInAuth.ts rename to src/platforms/LinkedIn/LinkedInAuth.ts index 6107e5b..d4f5ae7 100644 --- a/src/auth/LinkedInAuth.ts +++ b/src/platforms/LinkedIn/LinkedInAuth.ts @@ -1,6 +1,6 @@ -import Logger from "../services/Logger"; -import OAuth2Client from "./OAuth2Client"; -import Storage from "../services/Storage"; +import Logger from "../../services/Logger"; +import OAuth2Client from "../../auth/OAuth2Client"; +import Storage from "../../services/Storage"; export default class LinkedInAuth extends OAuth2Client { API_VERSION = "v2"; diff --git a/src/platforms/Reddit.ts b/src/platforms/Reddit/Reddit.ts similarity index 97% rename from src/platforms/Reddit.ts rename to src/platforms/Reddit/Reddit.ts index 29824b2..5b1ce39 100644 --- a/src/platforms/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -2,14 +2,14 @@ import * as fs from "fs"; import * as path from "path"; import * as sharp from "sharp"; -import Post, { PostStatus } from "../models/Post"; - -import Folder from "../models/Folder"; -import Logger from "../services/Logger"; -import Platform from "../models/Platform"; -import { PlatformId } from "."; -import RedditAuth from "../auth/RedditAuth"; -import Storage from "../services/Storage"; +import Post, { PostStatus } from "../../models/Post"; + +import Folder from "../../models/Folder"; +import Logger from "../../services/Logger"; +import Platform from "../../models/Platform"; +import { PlatformId } from ".."; +import RedditAuth from "./RedditAuth"; +import Storage from "../../services/Storage"; import { XMLParser } from "fast-xml-parser"; /** diff --git a/src/auth/RedditAuth.ts b/src/platforms/Reddit/RedditAuth.ts similarity index 97% rename from src/auth/RedditAuth.ts rename to src/platforms/Reddit/RedditAuth.ts index 786f7dc..5b41d68 100644 --- a/src/auth/RedditAuth.ts +++ b/src/platforms/Reddit/RedditAuth.ts @@ -1,6 +1,6 @@ -import Logger from "../services/Logger"; -import OAuth2Client from "./OAuth2Client"; -import Storage from "../services/Storage"; +import Logger from "../../services/Logger"; +import OAuth2Client from "../../auth/OAuth2Client"; +import Storage from "../../services/Storage"; export default class RedditAuth extends OAuth2Client { API_VERSION = "v1"; diff --git a/src/platforms/Twitter.ts b/src/platforms/Twitter/Twitter.ts similarity index 92% rename from src/platforms/Twitter.ts rename to src/platforms/Twitter/Twitter.ts index 72434e7..e3aee97 100644 --- a/src/platforms/Twitter.ts +++ b/src/platforms/Twitter/Twitter.ts @@ -1,15 +1,15 @@ import * as fs from "fs"; import * as sharp from "sharp"; -import Post, { PostStatus } from "../models/Post"; +import Post, { PostStatus } from "../../models/Post"; -import Folder from "../models/Folder"; -import Logger from "../services/Logger"; -import Platform from "../models/Platform"; -import { PlatformId } from "."; -import Storage from "../services/Storage"; +import Folder from "../../models/Folder"; +import Logger from "../../services/Logger"; +import Platform from "../../models/Platform"; +import { PlatformId } from ".."; +import Storage from "../../services/Storage"; import { TwitterApi } from "twitter-api-v2"; -import TwitterAuth from "../auth/TwitterAuth"; +import TwitterAuth from "./TwitterAuth"; /** * Twitter: support for twitter platform diff --git a/src/auth/TwitterAuth.ts b/src/platforms/Twitter/TwitterAuth.ts similarity index 92% rename from src/auth/TwitterAuth.ts rename to src/platforms/Twitter/TwitterAuth.ts index 3d045e4..4c96a9d 100644 --- a/src/auth/TwitterAuth.ts +++ b/src/platforms/Twitter/TwitterAuth.ts @@ -1,6 +1,6 @@ -import Logger from "../services/Logger"; -import OAuth2Client from "./OAuth2Client"; -import Storage from "../services/Storage"; +import Logger from "../../services/Logger"; +import OAuth2Client from "../../auth/OAuth2Client"; +import Storage from "../../services/Storage"; import { TwitterApi } from "twitter-api-v2"; export default class TwitterAuth extends OAuth2Client { diff --git a/src/platforms/index.ts b/src/platforms/index.ts index 3f4bd33..e0efd0d 100644 --- a/src/platforms/index.ts +++ b/src/platforms/index.ts @@ -1,8 +1,8 @@ -export { default as Facebook } from "./Facebook"; -export { default as Instagram } from "./Instagram"; -export { default as Twitter } from "./Twitter"; -export { default as Reddit } from "./Reddit"; -export { default as LinkedIn } from "./LinkedIn"; +export { default as Facebook } from "./Facebook/Facebook"; +export { default as Instagram } from "./Instagram/Instagram"; +export { default as Twitter } from "./Twitter/Twitter"; +export { default as Reddit } from "./Reddit/Reddit"; +export { default as LinkedIn } from "./LinkedIn/LinkedIn"; export { default as AsYouTube } from "./Ayrshare/AsYouTube"; export { default as AsInstagram } from "./Ayrshare/AsInstagram"; export { default as AsTwitter } from "./Ayrshare/AsTwitter"; From d585a02d947419312f36d09603fe477a7605aba1 Mon Sep 17 00:00:00 2001 From: "pike @ minimoon" Date: Sat, 9 Dec 2023 14:15:13 +0100 Subject: [PATCH 2/6] feat: Dont rely on fs.edDir to find platform classes --- src/platforms/Ayrshare/AsReddit.ts | 2 +- src/services/Fairpost.ts | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/platforms/Ayrshare/AsReddit.ts b/src/platforms/Ayrshare/AsReddit.ts index 245538f..e38e6a3 100644 --- a/src/platforms/Ayrshare/AsReddit.ts +++ b/src/platforms/Ayrshare/AsReddit.ts @@ -13,7 +13,7 @@ export default class AsReddit extends Ayrshare { constructor() { super(); - this.SUBREDDIT = Storage.get("settings", "AYRSHARE_SUBREDDIT"); + this.SUBREDDIT = Storage.get("settings", "AYRSHARE_SUBREDDIT", ""); } async preparePost(folder: Folder): Promise { diff --git a/src/services/Fairpost.ts b/src/services/Fairpost.ts index 89189c4..6f25353 100644 --- a/src/services/Fairpost.ts +++ b/src/services/Fairpost.ts @@ -1,6 +1,4 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as platforms from "../platforms"; +import * as platformClasses from "../platforms"; import Feed from "../models/Feed"; import Platform from "../models/Platform"; @@ -39,15 +37,9 @@ class Fairpost { "", ).split(","); - const platformClasses = fs.readdirSync( - path.resolve(__dirname + "/../platforms"), - ); - - platformClasses.forEach((file) => { - const constructor = file.replace(".ts", "").replace(".js", ""); - // nb import * as platforms loaded the constructors - if (platforms[constructor] !== undefined) { - const platform = new platforms[constructor](); + Object.values(platformClasses).forEach((platformClass) => { + if (typeof platformClass === "function") { + const platform = new platformClass(); platform.active = activePlatformIds.includes(platform.id); this.platforms.push(platform); } From 017bd2729a1b6ed445f9ffb546dc2a901cb7c3c2 Mon Sep 17 00:00:00 2001 From: "pike @ minimoon" Date: Sat, 9 Dec 2023 14:27:43 +0100 Subject: [PATCH 3/6] feat: Use static OAuth2Service --- src/services/OAuth2Service.ts | 91 +++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/services/OAuth2Service.ts diff --git a/src/services/OAuth2Service.ts b/src/services/OAuth2Service.ts new file mode 100644 index 0000000..5b2e394 --- /dev/null +++ b/src/services/OAuth2Service.ts @@ -0,0 +1,91 @@ +import * as fs from "fs"; +import * as http from "http"; +import * as url from "url"; +import Storage from "./Storage"; + +class DeferredResponseQuery { + promise: Promise<{ [key: string]: string | string[] }>; + reject: Function; // eslint-disable-line + resolve: Function; // eslint-disable-line + constructor() { + this.promise = new Promise((resolve, reject) => { + this.reject = reject; + this.resolve = resolve; + }); + } +} + +/** + * OAuth2Service: Static service to launch a webserver for + * requesting remote permissions on a service + */ +export default class OAuth2Service { + public static getRequestUrl(): string { + const clientHost = Storage.get("settings", "REQUEST_HOSTNAME"); + const clientPort = Number(Storage.get("settings", "REQUEST_PORT")); + return `http://${clientHost}:${clientPort}`; + } + + public static getCallbackUrl(): string { + return this.getRequestUrl() + "/callback"; + } + + /** + * Request remote permissions + * + * starts a webserver on host:port, showing a page with + * serviceLink on it. Keeps the webserver open until the + * client returns with a code, then stops the server and + * resolves the query passed. + * @param serviceName - the name of the remote platform + * @param serviceLink - the uri to the remote platform + * @param clientHost - the host name to serve the local page on + * @param clientPort - the port to serve the local page on + * @returns a flat object of returned query + */ + + public static async requestRemotePermissions( + serviceName: string, + serviceLink: string, + ): Promise<{ [key: string]: string | string[] }> { + const clientHost = Storage.get("settings", "REQUEST_HOSTNAME"); + const clientPort = Number(Storage.get("settings", "REQUEST_PORT")); + const server = http.createServer(); + const deferred = new DeferredResponseQuery(); + + server.listen(clientPort, clientHost, () => { + console.log(`Open a web browser and go to ${this.getRequestUrl()}`); + }); + const requestListener = async function ( + request: http.IncomingMessage, + response: http.ServerResponse, + ) { + const parsed = url.parse(request.url ?? "/", true); + if (parsed.pathname === "/callback") { + let result = ""; + for (const key in parsed.query) { + result += key + " : " + String(parsed.query[key]) + "\n"; + } + let body = fs.readFileSync("public/auth/callback.html", "utf8"); + body = body.replace(/{{serviceName}}/g, serviceName); + body = body.replace(/{{result}}/g, result ?? "UNKNOWN"); + response.setHeader("Content-Type", "text/html"); + response.setHeader("Connection", "close"); + response.writeHead(200); + response.end(body); + server.close(); + deferred.resolve(parsed.query); + } else { + let body = fs.readFileSync("public/auth/request.html", "utf8"); + body = body.replace(/{{serviceLink}}/g, serviceLink); + body = body.replace(/{{serviceName}}/g, serviceName); + response.setHeader("Content-Type", "text/html"); + response.writeHead(200); + response.end(body); + } + }; + server.on("request", requestListener); + + return deferred.promise; + } +} From 82eb02cc3f9ff9e380bc5823498df42589c51d01 Mon Sep 17 00:00:00 2001 From: "pike @ minimoon" Date: Sat, 9 Dec 2023 15:09:42 +0100 Subject: [PATCH 4/6] feat: Move all apis into separate files --- src/auth/OAuth2Client.ts | 91 ----------- src/platforms/Facebook/Facebook.ts | 153 ++----------------- src/platforms/Facebook/FacebookApi.ts | 137 +++++++++++++++++ src/platforms/Facebook/FacebookAuth.ts | 13 +- src/platforms/Instagram/Instagram.ts | 187 +++-------------------- src/platforms/Instagram/InstagramApi.ts | 154 +++++++++++++++++++ src/platforms/Instagram/InstagramAuth.ts | 8 +- src/platforms/LinkedIn/LinkedIn.ts | 159 ++++--------------- src/platforms/LinkedIn/LinkedInApi.ts | 117 ++++++++++++++ src/platforms/LinkedIn/LinkedInAuth.ts | 13 +- src/platforms/Reddit/Reddit.ts | 141 +---------------- src/platforms/Reddit/RedditApi.ts | 128 ++++++++++++++++ src/platforms/Reddit/RedditAuth.ts | 15 +- src/platforms/Twitter/TwitterAuth.ts | 10 +- src/services/Logger.ts | 8 +- src/services/Storage.ts | 4 +- 16 files changed, 646 insertions(+), 692 deletions(-) delete mode 100644 src/auth/OAuth2Client.ts create mode 100644 src/platforms/Facebook/FacebookApi.ts create mode 100644 src/platforms/Instagram/InstagramApi.ts create mode 100644 src/platforms/LinkedIn/LinkedInApi.ts create mode 100644 src/platforms/Reddit/RedditApi.ts diff --git a/src/auth/OAuth2Client.ts b/src/auth/OAuth2Client.ts deleted file mode 100644 index 0556f79..0000000 --- a/src/auth/OAuth2Client.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as fs from "fs"; -import * as http from "http"; -import * as url from "url"; -import Storage from "../services/Storage"; - -class DeferredResponseQuery { - promise: Promise<{ [key: string]: string | string[] }>; - reject: Function; // eslint-disable-line - resolve: Function; // eslint-disable-line - constructor() { - this.promise = new Promise((resolve, reject) => { - this.reject = reject; - this.resolve = resolve; - }); - } -} - -/** - * OAuth2Client: abstract handler to launch a webserver for - * requesting remote permissions on a service - */ -export default class OAuth2Client { - protected getRequestUrl(): string { - const clientHost = Storage.get("settings", "REQUEST_HOSTNAME"); - const clientPort = Number(Storage.get("settings", "REQUEST_PORT")); - return `http://${clientHost}:${clientPort}`; - } - - protected getCallbackUrl(): string { - return this.getRequestUrl() + "/callback"; - } - - /** - * Request remote permissions - * - * starts a webserver on host:port, showing a page with - * serviceLink on it. Keeps the webserver open until the - * client returns with a code, then stops the server and - * resolves the query passed. - * @param serviceName - the name of the remote platform - * @param serviceLink - the uri to the remote platform - * @param clientHost - the host name to serve the local page on - * @param clientPort - the port to serve the local page on - * @returns a flat object of returned query - */ - - protected async requestRemotePermissions( - serviceName: string, - serviceLink: string, - ): Promise<{ [key: string]: string | string[] }> { - const clientHost = Storage.get("settings", "REQUEST_HOSTNAME"); - const clientPort = Number(Storage.get("settings", "REQUEST_PORT")); - const server = http.createServer(); - const deferred = new DeferredResponseQuery(); - - server.listen(clientPort, clientHost, () => { - console.log(`Open a web browser and go to ${this.getRequestUrl()}`); - }); - const requestListener = async function ( - request: http.IncomingMessage, - response: http.ServerResponse, - ) { - const parsed = url.parse(request.url ?? "/", true); - if (parsed.pathname === "/callback") { - let result = ""; - for (const key in parsed.query) { - result += key + " : " + String(parsed.query[key]) + "\n"; - } - let body = fs.readFileSync("public/auth/callback.html", "utf8"); - body = body.replace(/{{serviceName}}/g, serviceName); - body = body.replace(/{{result}}/g, result ?? "UNKNOWN"); - response.setHeader("Content-Type", "text/html"); - response.setHeader("Connection", "close"); - response.writeHead(200); - response.end(body); - server.close(); - deferred.resolve(parsed.query); - } else { - let body = fs.readFileSync("public/auth/request.html", "utf8"); - body = body.replace(/{{serviceLink}}/g, serviceLink); - body = body.replace(/{{serviceName}}/g, serviceName); - response.setHeader("Content-Type", "text/html"); - response.writeHead(200); - response.end(body); - } - }; - server.on("request", requestListener); - - return deferred.promise; - } -} diff --git a/src/platforms/Facebook/Facebook.ts b/src/platforms/Facebook/Facebook.ts index 04ef293..471291a 100644 --- a/src/platforms/Facebook/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import * as path from "path"; import * as sharp from "sharp"; +import FacebookApi from "./FacebookApi"; import FacebookAuth from "./FacebookAuth"; import Folder from "../../models/Folder"; import Logger from "../../services/Logger"; @@ -20,21 +21,23 @@ import Storage from "../../services/Storage"; */ export default class Facebook extends Platform { id: PlatformId = PlatformId.FACEBOOK; - GRAPH_API_VERSION: string = "v18.0"; + api: FacebookApi; + auth: FacebookAuth; constructor() { super(); + this.auth = new FacebookAuth(); + this.api = new FacebookApi(); } /** @inheritdoc */ async setup() { - const auth = new FacebookAuth(); - await auth.setup(); + await this.auth.setup(); } /** @inheritdoc */ async test() { - return this.get("me"); + return this.api.get("me"); } /** @inheritdoc */ @@ -99,7 +102,7 @@ export default class Facebook extends Platform { } } if (!dryrun) { - response = (await this.postJson("%PAGE%/feed", { + response = (await this.api.postJson("%PAGE%/feed", { message: post.body, published: Storage.get("settings", "FACEBOOK_PUBLISH_POSTS"), attached_media: attachments, @@ -154,7 +157,7 @@ export default class Facebook extends Platform { body.set("published", published ? "true" : "false"); body.set("source", blob, path.basename(file)); - const result = (await this.postFormData("%PAGE%/photos", body)) as { + const result = (await this.api.postFormData("%PAGE%/photos", body)) as { id: "string"; }; @@ -190,7 +193,7 @@ export default class Facebook extends Platform { body.set("published", Storage.get("settings", "FACEBOOK_PUBLISH_POSTS")); body.set("source", blob, path.basename(file)); - const result = (await this.postFormData("%PAGE%/videos", body)) as { + const result = (await this.api.postFormData("%PAGE%/videos", body)) as { id: string; }; @@ -199,140 +202,4 @@ export default class Facebook extends Platform { } return result; } - - // API implementation ------------------- - - /** - * Do a GET request on the graph. - * @param endpoint - the path to call - * @param query - query string as object - */ - - private async get( - endpoint: string = "%PAGE%", - query: { [key: string]: string } = {}, - ): Promise { - endpoint = endpoint.replace( - "%PAGE%", - Storage.get("settings", "FACEBOOK_PAGE_ID"), - ); - - const url = new URL("https://graph.facebook.com"); - url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; - url.search = new URLSearchParams(query).toString(); - Logger.trace("GET", url.href); - return await fetch(url, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: - "Bearer " + Storage.get("auth", "FACEBOOK_PAGE_ACCESS_TOKEN"), - }, - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Do a Json POST request on the graph. - * @param endpoint - the path to call - * @param body - body as object - */ - - private async postJson( - endpoint: string = "%PAGE%", - body = {}, - ): Promise { - endpoint = endpoint.replace( - "%PAGE%", - Storage.get("settings", "FACEBOOK_PAGE_ID"), - ); - - const url = new URL("https://graph.facebook.com"); - url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; - Logger.trace("POST", url.href); - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: - "Bearer " + Storage.get("settings", "FACEBOOK_PAGE_ACCESS_TOKEN"), - }, - body: JSON.stringify(body), - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Do a FormData POST request on the graph. - * @param endpoint - the path to call - * @param body - body as object - */ - - private async postFormData( - endpoint: string, - body: FormData, - ): Promise { - endpoint = endpoint.replace( - "%PAGE%", - Storage.get("settings", "FACEBOOK_PAGE_ID"), - ); - - const url = new URL("https://graph.facebook.com"); - url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; - Logger.trace("POST", url.href); - - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - Authorization: - "Bearer " + Storage.get("settings", "FACEBOOK_PAGE_ACCESS_TOKEN"), - }, - body: body, - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Handle api response - * @param response - api response from fetch - * @returns parsed object from response - */ - private async handleApiResponse(response: Response): Promise { - if (!response.ok) { - throw Logger.error( - "Facebook.handleApiResponse", - response, - response.status + ":" + response.statusText, - ); - } - const data = await response.json(); - if (data.error) { - const error = - response.status + - ":" + - data.error.type + - "(" + - data.error.code + - "/" + - data.error.error_subcode + - ") " + - data.error.message; - throw Logger.error("Facebook.handleApiResponse", error); - } - Logger.trace("Facebook.handleApiResponse", "success"); - return data; - } - - /** - * Handle api error - * @param error - the error returned from fetch - */ - private handleApiError(error: Error): never { - throw Logger.error("Facebook.handleApiError", error); - } } diff --git a/src/platforms/Facebook/FacebookApi.ts b/src/platforms/Facebook/FacebookApi.ts new file mode 100644 index 0000000..c9946e9 --- /dev/null +++ b/src/platforms/Facebook/FacebookApi.ts @@ -0,0 +1,137 @@ +import Logger from "../../services/Logger"; +import Storage from "../../services/Storage"; + +export default class FacebookApi { + GRAPH_API_VERSION = "v18.0"; + + /** + * Do a GET request on the graph. + * @param endpoint - the path to call + * @param query - query string as object + */ + + public async get( + endpoint: string = "%PAGE%", + query: { [key: string]: string } = {}, + ): Promise { + endpoint = endpoint.replace( + "%PAGE%", + Storage.get("settings", "FACEBOOK_PAGE_ID"), + ); + + const url = new URL("https://graph.facebook.com"); + url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; + url.search = new URLSearchParams(query).toString(); + Logger.trace("GET", url.href); + return await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + Authorization: + "Bearer " + Storage.get("auth", "FACEBOOK_PAGE_ACCESS_TOKEN"), + }, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Do a Json POST request on the graph. + * @param endpoint - the path to call + * @param body - body as object + */ + + public async postJson( + endpoint: string = "%PAGE%", + body = {}, + ): Promise { + endpoint = endpoint.replace( + "%PAGE%", + Storage.get("settings", "FACEBOOK_PAGE_ID"), + ); + + const url = new URL("https://graph.facebook.com"); + url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; + Logger.trace("POST", url.href); + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: + "Bearer " + Storage.get("settings", "FACEBOOK_PAGE_ACCESS_TOKEN"), + }, + body: JSON.stringify(body), + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Do a FormData POST request on the graph. + * @param endpoint - the path to call + * @param body - body as object + */ + + public async postFormData(endpoint: string, body: FormData): Promise { + endpoint = endpoint.replace( + "%PAGE%", + Storage.get("settings", "FACEBOOK_PAGE_ID"), + ); + + const url = new URL("https://graph.facebook.com"); + url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; + Logger.trace("POST", url.href); + + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + Authorization: + "Bearer " + Storage.get("settings", "FACEBOOK_PAGE_ACCESS_TOKEN"), + }, + body: body, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Handle api response + * @param response - api response from fetch + * @returns parsed object from response + */ + private async handleApiResponse(response: Response): Promise { + if (!response.ok) { + throw Logger.error( + "Facebook.handleApiResponse", + response, + response.status + ":" + response.statusText, + ); + } + const data = await response.json(); + if (data.error) { + const error = + response.status + + ":" + + data.error.type + + "(" + + data.error.code + + "/" + + data.error.error_subcode + + ") " + + data.error.message; + throw Logger.error("Facebook.handleApiResponse", error); + } + Logger.trace("Facebook.handleApiResponse", "success"); + return data; + } + + /** + * Handle api error + * @param error - the error returned from fetch + */ + private handleApiError(error: Error): never { + throw Logger.error("Facebook.handleApiError", error); + } +} diff --git a/src/platforms/Facebook/FacebookAuth.ts b/src/platforms/Facebook/FacebookAuth.ts index 6535e53..c88b712 100644 --- a/src/platforms/Facebook/FacebookAuth.ts +++ b/src/platforms/Facebook/FacebookAuth.ts @@ -1,8 +1,8 @@ import Logger from "../../services/Logger"; -import OAuth2Client from "../../auth/OAuth2Client"; +import OAuth2Service from "../../services/OAuth2Service"; import Storage from "../../services/Storage"; -export default class FacebookAuth extends OAuth2Client { +export default class FacebookAuth { GRAPH_API_VERSION: string = "v18.0"; async setup() { @@ -34,7 +34,7 @@ export default class FacebookAuth extends OAuth2Client { url.pathname = this.GRAPH_API_VERSION + "/dialog/oauth"; const query = { client_id: clientId, - redirect_uri: this.getCallbackUrl(), + redirect_uri: OAuth2Service.getCallbackUrl(), state: state, response_type: "code", scope: [ @@ -48,7 +48,10 @@ export default class FacebookAuth extends OAuth2Client { }; url.search = new URLSearchParams(query).toString(); - const result = await this.requestRemotePermissions("Facebook", url.href); + const result = await OAuth2Service.requestRemotePermissions( + "Facebook", + url.href, + ); if (result["error"]) { const msg = result["error_reason"] + " - " + result["error_description"]; throw Logger.error(msg, result); @@ -69,7 +72,7 @@ export default class FacebookAuth extends OAuth2Client { clientId: string, clientSecret: string, ): Promise { - const redirectUri = this.getCallbackUrl(); + const redirectUri = OAuth2Service.getCallbackUrl(); const result = await this.get("oauth/access_token", { client_id: clientId, diff --git a/src/platforms/Instagram/Instagram.ts b/src/platforms/Instagram/Instagram.ts index 3d28f4a..94639a7 100644 --- a/src/platforms/Instagram/Instagram.ts +++ b/src/platforms/Instagram/Instagram.ts @@ -3,13 +3,13 @@ import * as path from "path"; import * as sharp from "sharp"; import Folder from "../../models/Folder"; +import InstagramApi from "./InstagramApi"; import InstagramAuth from "./InstagramAuth"; import Logger from "../../services/Logger"; import Platform from "../../models/Platform"; import { PlatformId } from ".."; import Post from "../../models/Post"; import { PostStatus } from "../../models/Post"; -import Storage from "../../services/Storage"; /** * Instagram: support for instagram platform. @@ -20,21 +20,23 @@ import Storage from "../../services/Storage"; */ export default class Instagram extends Platform { id: PlatformId = PlatformId.INSTAGRAM; - GRAPH_API_VERSION: string = "v18.0"; + api: InstagramApi; + auth: InstagramAuth; constructor() { super(); + this.auth = new InstagramAuth(); + this.api = new InstagramApi(); } /** @inheritdoc */ async setup() { - const auth = new InstagramAuth(); - await auth.setup(); + await this.auth.setup(); } /** @inheritdoc */ async test() { - return this.get("me"); + return this.api.get("me"); } /** @inheritdoc */ @@ -144,7 +146,7 @@ export default class Instagram extends Platform { ): Promise<{ id: string }> { const photoId = (await this.fbUploadPhoto(file))["id"]; const photoLink = await this.fbGetPhotoLink(photoId); - const container = (await this.postJson("%USER%/media", { + const container = (await this.api.postJson("%USER%/media", { image_url: photoLink, caption: caption, })) as { id: string }; @@ -155,7 +157,7 @@ export default class Instagram extends Platform { if (!dryrun) { // wait for upload ? // https://github.com/fbsamples/reels_publishing_apis/blob/main/insta_reels_publishing_api_sample/utils.js#L23 - const response = (await this.postJson("%USER%/media_publish", { + const response = (await this.api.postJson("%USER%/media_publish", { creation_id: container.id, })) as { id: string }; if (!response?.id) { @@ -184,7 +186,7 @@ export default class Instagram extends Platform { ): Promise<{ id: string }> { const videoId = (await this.fbUploadVideo(file))["id"]; const videoLink = await this.fbGetVideoLink(videoId); - const container = (await this.postJson("%USER%/media", { + const container = (await this.api.postJson("%USER%/media", { video_url: videoLink, caption: caption, })) as { id: string }; @@ -195,7 +197,7 @@ export default class Instagram extends Platform { if (!dryrun) { // wait for upload ? // https://github.com/fbsamples/reels_publishing_apis/blob/main/insta_reels_publishing_api_sample/utils.js#L23 - const response = (await this.postJson("%USER%/media_publish", { + const response = (await this.api.postJson("%USER%/media_publish", { creation_id: container.id, })) as { id: string }; if (!response?.id) { @@ -229,7 +231,7 @@ export default class Instagram extends Platform { const videoLink = await this.fbGetVideoLink(videoId); uploadIds.push( ( - await this.postJson("%USER%/media", { + await this.api.postJson("%USER%/media", { is_carousel_item: true, video_url: videoLink, }) @@ -244,7 +246,7 @@ export default class Instagram extends Platform { const photoLink = await this.fbGetPhotoLink(photoId); uploadIds.push( ( - await this.postJson("%USER%/media", { + await this.api.postJson("%USER%/media", { is_carousel_item: true, image_url: photoLink, }) @@ -253,7 +255,7 @@ export default class Instagram extends Platform { } // create carousel - const container = (await this.postJson("%USER%/media", { + const container = (await this.api.postJson("%USER%/media", { media_type: "CAROUSEL", caption: post.body, children: uploadIds.join(","), @@ -266,7 +268,7 @@ export default class Instagram extends Platform { // publish carousel if (!dryrun) { - const response = (await this.postJson("%USER%/media_publish", { + const response = (await this.api.postJson("%USER%/media_publish", { creation_id: container["id"], })) as { id: string; @@ -298,7 +300,7 @@ export default class Instagram extends Platform { body.set("published", "false"); body.set("source", blob, path.basename(file)); - const result = (await this.postFormData("%PAGE%/photos", body)) as { + const result = (await this.api.postFormData("%PAGE%/photos", body)) as { id: "string"; }; @@ -315,7 +317,7 @@ export default class Instagram extends Platform { */ private async fbGetPhotoLink(id: string): Promise { // get photo derivatives - const photoData = (await this.get(id, { + const photoData = (await this.api.get(id, { fields: "link,images,picture", })) as { link: string; @@ -356,7 +358,7 @@ export default class Instagram extends Platform { body.set("published", "false"); body.set("source", blob, path.basename(file)); - const result = (await this.postFormData("%PAGE%/videos", body)) as { + const result = (await this.api.postFormData("%PAGE%/videos", body)) as { id: string; }; @@ -373,7 +375,7 @@ export default class Instagram extends Platform { */ private async fbGetVideoLink(id: string): Promise { - const videoData = (await this.get(id, { + const videoData = (await this.api.get(id, { fields: "permalink_url,source", })) as { permalink_url: string; @@ -384,155 +386,4 @@ export default class Instagram extends Platform { } return videoData["source"]; } - - // API implementation ------------------- - - /** - * Do a GET request on the graph. - * @param endpoint - the path to call - * @param query - querystring as object - * @returns parsed response - */ - - private async get( - endpoint: string = "%USER%", - query: { [key: string]: string } = {}, - ): Promise { - endpoint = endpoint.replace( - "%USER%", - Storage.get("settings", "INSTAGRAM_USER_ID"), - ); - endpoint = endpoint.replace( - "%PAGE%", - Storage.get("settings", "INSTAGRAM_PAGE_ID"), - ); - - const url = new URL("https://graph.facebook.com"); - url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; - url.search = new URLSearchParams(query).toString(); - const accessToken = Storage.get("auth", "INSTAGRAM_PAGE_ACCESS_TOKEN"); - Logger.trace("GET", url.href); - return await fetch(url, { - method: "GET", - headers: accessToken - ? { - Accept: "application/json", - Authorization: "Bearer " + accessToken, - } - : { - Accept: "application/json", - }, - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Do a Json POST request on the graph. - * @param endpoin - the path to call - * @param body - body as object - * @returns the parsed response as object - */ - - private async postJson( - endpoint: string = "%USER%", - body = {}, - ): Promise { - endpoint = endpoint.replace( - "%USER%", - Storage.get("settings", "INSTAGRAM_USER_ID"), - ); - endpoint = endpoint.replace( - "%PAGE%", - Storage.get("settings", "INSTAGRAM_PAGE_ID"), - ); - - const url = new URL("https://graph.facebook.com"); - url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; - Logger.trace("POST", url.href); - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: - "Bearer " + Storage.get("auth", "INSTAGRAM_PAGE_ACCESS_TOKEN"), - }, - body: JSON.stringify(body), - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Do a FormData POST request on the graph. - * @param endpoint - the path to call - * @param body - body as object - * @returns the parsed response as object - */ - - private async postFormData( - endpoint: string, - body: FormData, - ): Promise { - endpoint = endpoint.replace( - "%USER%", - Storage.get("settings", "INSTAGRAM_USER_ID"), - ); - endpoint = endpoint.replace( - "%PAGE%", - Storage.get("settings", "INSTAGRAM_PAGE_ID"), - ); - - const url = new URL("https://graph.facebook.com"); - url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; - Logger.trace("POST", url.href); - - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - Authorization: - "Bearer " + Storage.get("settings", "INSTAGRAM_PAGE_ACCESS_TOKEN"), - }, - body: body, - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Handle api response - * @param response - the api response from fetch - * @returns the parsed response - */ - private async handleApiResponse(response: Response): Promise { - if (!response.ok) { - throw Logger.error("Ayrshare.handleApiResponse", response); - } - const data = await response.json(); - if (data.error) { - const error = - response.status + - ":" + - data.error.type + - "(" + - data.error.code + - "/" + - data.error.error_subcode + - ") " + - data.error.message; - throw Logger.error("Facebook.handleApiResponse", error); - } - Logger.trace("Facebook.handleApiResponse", "success"); - return data; - } - - /** - * Handle api error - * @param error - the api error returned from fetch - */ - private handleApiError(error: Error): never { - throw Logger.error("Facebook.handleApiError", error); - } } diff --git a/src/platforms/Instagram/InstagramApi.ts b/src/platforms/Instagram/InstagramApi.ts new file mode 100644 index 0000000..5f06598 --- /dev/null +++ b/src/platforms/Instagram/InstagramApi.ts @@ -0,0 +1,154 @@ +import Logger from "../../services/Logger"; +import Storage from "../../services/Storage"; + +export default class InstagramApi { + GRAPH_API_VERSION = "v18.0"; + + // API implementation ------------------- + + /** + * Do a GET request on the graph. + * @param endpoint - the path to call + * @param query - querystring as object + * @returns parsed response + */ + + public async get( + endpoint: string = "%USER%", + query: { [key: string]: string } = {}, + ): Promise { + endpoint = endpoint.replace( + "%USER%", + Storage.get("settings", "INSTAGRAM_USER_ID"), + ); + endpoint = endpoint.replace( + "%PAGE%", + Storage.get("settings", "INSTAGRAM_PAGE_ID"), + ); + + const url = new URL("https://graph.facebook.com"); + url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; + url.search = new URLSearchParams(query).toString(); + const accessToken = Storage.get("auth", "INSTAGRAM_PAGE_ACCESS_TOKEN"); + Logger.trace("GET", url.href); + return await fetch(url, { + method: "GET", + headers: accessToken + ? { + Accept: "application/json", + Authorization: "Bearer " + accessToken, + } + : { + Accept: "application/json", + }, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Do a Json POST request on the graph. + * @param endpoin - the path to call + * @param body - body as object + * @returns the parsed response as object + */ + + public async postJson( + endpoint: string = "%USER%", + body = {}, + ): Promise { + endpoint = endpoint.replace( + "%USER%", + Storage.get("settings", "INSTAGRAM_USER_ID"), + ); + endpoint = endpoint.replace( + "%PAGE%", + Storage.get("settings", "INSTAGRAM_PAGE_ID"), + ); + + const url = new URL("https://graph.facebook.com"); + url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; + Logger.trace("POST", url.href); + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: + "Bearer " + Storage.get("auth", "INSTAGRAM_PAGE_ACCESS_TOKEN"), + }, + body: JSON.stringify(body), + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Do a FormData POST request on the graph. + * @param endpoint - the path to call + * @param body - body as object + * @returns the parsed response as object + */ + + public async postFormData(endpoint: string, body: FormData): Promise { + endpoint = endpoint.replace( + "%USER%", + Storage.get("settings", "INSTAGRAM_USER_ID"), + ); + endpoint = endpoint.replace( + "%PAGE%", + Storage.get("settings", "INSTAGRAM_PAGE_ID"), + ); + + const url = new URL("https://graph.facebook.com"); + url.pathname = this.GRAPH_API_VERSION + "/" + endpoint; + Logger.trace("POST", url.href); + + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + Authorization: + "Bearer " + Storage.get("settings", "INSTAGRAM_PAGE_ACCESS_TOKEN"), + }, + body: body, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Handle api response + * @param response - the api response from fetch + * @returns the parsed response + */ + private async handleApiResponse(response: Response): Promise { + if (!response.ok) { + throw Logger.error("Ayrshare.handleApiResponse", response); + } + const data = await response.json(); + if (data.error) { + const error = + response.status + + ":" + + data.error.type + + "(" + + data.error.code + + "/" + + data.error.error_subcode + + ") " + + data.error.message; + throw Logger.error("Facebook.handleApiResponse", error); + } + Logger.trace("Facebook.handleApiResponse", "success"); + return data; + } + + /** + * Handle api error + * @param error - the api error returned from fetch + */ + private handleApiError(error: Error): never { + throw Logger.error("Facebook.handleApiError", error); + } +} diff --git a/src/platforms/Instagram/InstagramAuth.ts b/src/platforms/Instagram/InstagramAuth.ts index 3ae50d9..647884f 100644 --- a/src/platforms/Instagram/InstagramAuth.ts +++ b/src/platforms/Instagram/InstagramAuth.ts @@ -1,5 +1,6 @@ import FacebookAuth from "../Facebook/FacebookAuth"; import Logger from "../../services/Logger"; +import OAuth2Service from "../../services/OAuth2Service"; import Storage from "../../services/Storage"; export default class InstagramAuth extends FacebookAuth { @@ -32,7 +33,7 @@ export default class InstagramAuth extends FacebookAuth { url.pathname = this.GRAPH_API_VERSION + "/dialog/oauth"; const query = { client_id: clientId, - redirect_uri: this.getCallbackUrl(), + redirect_uri: OAuth2Service.getCallbackUrl(), state: state, response_type: "code", scope: [ @@ -48,7 +49,10 @@ export default class InstagramAuth extends FacebookAuth { }; url.search = new URLSearchParams(query).toString(); - const result = await this.requestRemotePermissions("Instagram", url.href); + const result = await OAuth2Service.requestRemotePermissions( + "Instagram", + url.href, + ); if (result["error"]) { const msg = result["error_reason"] + " - " + result["error_description"]; diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index a51fab3..10c097a 100644 --- a/src/platforms/LinkedIn/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import * as sharp from "sharp"; import Folder from "../../models/Folder"; +import LinkedInApi from "./LinkedInApi"; import LinkedInAuth from "./LinkedInAuth"; import Logger from "../../services/Logger"; import Platform from "../../models/Platform"; @@ -13,10 +14,9 @@ import Storage from "../../services/Storage"; export default class LinkedIn extends Platform { id: PlatformId = PlatformId.LINKEDIN; + api: LinkedInApi; auth: LinkedInAuth; - LGC_API_VERSION = "v2"; - API_VERSION = "202304"; POST_AUTHOR = ""; POST_VISIBILITY = "PUBLIC"; // CONNECTIONS|PUBLIC|LOGGEDIN|CONTAINER POST_DISTRIBUTION = { @@ -28,6 +28,7 @@ export default class LinkedIn extends Platform { constructor() { super(); + this.api = new LinkedInApi(); this.auth = new LinkedInAuth(); this.POST_AUTHOR = "urn:li:organization:" + @@ -158,7 +159,7 @@ export default class LinkedIn extends Platform { // Platform API Specific private async getProfile() { - const me = await this.get("me"); + const me = await this.api.get("me"); if (!me) return false; return { id: me["id"], @@ -177,7 +178,7 @@ export default class LinkedIn extends Platform { lifecycleState: "PUBLISHED", isReshareDisabledByAuthor: this.POST_NORESHARE, }; - return await this.postJson("posts", body); + return await this.api.postJson("posts", body); } private async publishImage(title: string, content: string, image: string) { const leash = await this.getImageLeash(); @@ -196,7 +197,7 @@ export default class LinkedIn extends Platform { lifecycleState: "PUBLISHED", isReshareDisabledByAuthor: this.POST_NORESHARE, }; - return await this.postJson("posts", body); + return await this.api.postJson("posts", body); } private async publishImages(content: string, images: string[]) { @@ -225,7 +226,7 @@ export default class LinkedIn extends Platform { }, }, }; - return await this.postJson("posts", body); + return await this.api.postJson("posts", body); } // untested @@ -246,7 +247,7 @@ export default class LinkedIn extends Platform { lifecycleState: "PUBLISHED", isReshareDisabledByAuthor: this.POST_NORESHARE, }; - return await this.postJson("posts", body); + return await this.api.postJson("posts", body); } private async getImageLeash(): Promise<{ @@ -256,11 +257,14 @@ export default class LinkedIn extends Platform { image: string; }; }> { - const response = (await this.postJson("images?action=initializeUpload", { - initializeUploadRequest: { - owner: this.POST_AUTHOR, + const response = (await this.api.postJson( + "images?action=initializeUpload", + { + initializeUploadRequest: { + owner: this.POST_AUTHOR, + }, }, - })) as { + )) as { value: { uploadUrlExpiresAt: number; uploadUrl: string; @@ -282,7 +286,7 @@ export default class LinkedIn extends Platform { Authorization: "Bearer " + (await this.auth.getAccessToken()), }, body: rawData, - }).then((res) => this.handleApiResponse(res)); + }).then((res) => this.api.handleApiResponse(res)); } // untested @@ -299,14 +303,17 @@ export default class LinkedIn extends Platform { }; }> { const stats = fs.statSync(file); - const response = (await this.postJson("images?videos=initializeUpload", { - initializeUploadRequest: { - owner: this.POST_AUTHOR, - fileSizeBytes: stats.size, - uploadCaptions: false, - uploadThumbnail: false, + const response = (await this.api.postJson( + "images?videos=initializeUpload", + { + initializeUploadRequest: { + owner: this.POST_AUTHOR, + fileSizeBytes: stats.size, + uploadCaptions: false, + uploadThumbnail: false, + }, }, - })) as { + )) as { value: { uploadUrlsExpireAt: number; video: string; @@ -334,118 +341,6 @@ export default class LinkedIn extends Platform { "Content-Type": "application/octet-stream", }, body: rawData, - }).then((res) => this.handleApiResponse(res)); - } - - // API implementation ------------------- - - /** - * Do a GET request on the api. - * @param endpoint - the path to call - * @param query - query string as object - */ - - private async get( - endpoint: string, - query: { [key: string]: string } = {}, - ): Promise { - // nb this is the legacy format - const url = new URL("https://api.linkedin.com"); - url.pathname = this.LGC_API_VERSION + "/" + endpoint; - url.search = new URLSearchParams(query).toString(); - - const accessToken = await this.auth.getAccessToken(); - - Logger.trace("GET", url.href); - return await fetch(url, { - method: "GET", - headers: { - Accept: "application/json", - Connection: "Keep-Alive", - Authorization: "Bearer " + accessToken, - "User-Agent": Storage.get("settings", "USER_AGENT"), - }, - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Do a json POST request on the api. - * @param endpoint - the path to call - * @param body - body as object - */ - - private async postJson(endpoint: string, body = {}): Promise { - const url = new URL("https://api.linkedin.com"); - - const [pathname, search] = endpoint.split("?"); - url.pathname = "rest/" + pathname; - if (search) { - url.search = search; - } - const accessToken = await this.auth.getAccessToken(); - Logger.trace("POST", url.href); - - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - "Linkedin-Version": this.API_VERSION, - Authorization: "Bearer " + accessToken, - }, - body: JSON.stringify(body), - }).then((res) => this.handleApiResponse(res)); - //.catch((err) => this.handleApiError(err)); - } - - /* - * Handle api response - * - */ - private async handleApiResponse(response: Response): Promise { - const text = await response.text(); - let data = {} as { [key: string]: unknown }; - try { - data = JSON.parse(text); - } catch (err) { - data["text"] = text; - } - if (!response.ok) { - Logger.warn("Linkedin.handleApiResponse", response); - Logger.warn(response.headers); - const linkedInErrorResponse = - response.headers["x-linkedin-error-response"]; - - const error = - response.status + - ":" + - response.statusText + - " (" + - data.status + - "/" + - data.serviceErrorCode + - ") " + - data.message + - " - " + - linkedInErrorResponse; - - throw Logger.error(error); - } - data["headers"] = {}; - for (const [name, value] of response.headers) { - data["headers"][name] = value; - } - Logger.trace("Linkedin.handleApiResponse", "success"); - return data; - } - - /* - * Handle api error - * - */ - private handleApiError(error: Error): Promise { - throw Logger.error("Linkedin.handleApiError", error); + }).then((res) => this.api.handleApiResponse(res)); } } diff --git a/src/platforms/LinkedIn/LinkedInApi.ts b/src/platforms/LinkedIn/LinkedInApi.ts new file mode 100644 index 0000000..c251bde --- /dev/null +++ b/src/platforms/LinkedIn/LinkedInApi.ts @@ -0,0 +1,117 @@ +import Logger from "../../services/Logger"; +import Storage from "../../services/Storage"; + +export default class LinkedInApi { + LGC_API_VERSION = "v2"; + API_VERSION = "202304"; + + /** + * Do a GET request on the api. + * @param endpoint - the path to call + * @param query - query string as object + */ + + public async get( + endpoint: string, + query: { [key: string]: string } = {}, + ): Promise { + // nb this is the legacy format + const url = new URL("https://api.linkedin.com"); + url.pathname = this.LGC_API_VERSION + "/" + endpoint; + url.search = new URLSearchParams(query).toString(); + + const accessToken = Storage.get("auth", "LINKEDIN_ACCESS_TOKEN"); + + Logger.trace("GET", url.href); + return await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + Connection: "Keep-Alive", + Authorization: "Bearer " + accessToken, + "User-Agent": Storage.get("settings", "USER_AGENT"), + }, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Do a json POST request on the api. + * @param endpoint - the path to call + * @param body - body as object + */ + + public async postJson(endpoint: string, body = {}): Promise { + const url = new URL("https://api.linkedin.com"); + + const [pathname, search] = endpoint.split("?"); + url.pathname = "rest/" + pathname; + if (search) { + url.search = search; + } + const accessToken = Storage.get("auth", "LINKEDIN_ACCESS_TOKEN"); + Logger.trace("POST", url.href); + + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "Linkedin-Version": this.API_VERSION, + Authorization: "Bearer " + accessToken, + }, + body: JSON.stringify(body), + }).then((res) => this.handleApiResponse(res)); + //.catch((err) => this.handleApiError(err)); + } + + /* + * Handle api response + * + */ + public async handleApiResponse(response: Response): Promise { + const text = await response.text(); + let data = {} as { [key: string]: unknown }; + try { + data = JSON.parse(text); + } catch (err) { + data["text"] = text; + } + if (!response.ok) { + Logger.warn("Linkedin.handleApiResponse", response); + Logger.warn(response.headers); + const linkedInErrorResponse = + response.headers["x-linkedin-error-response"]; + + const error = + response.status + + ":" + + response.statusText + + " (" + + data.status + + "/" + + data.serviceErrorCode + + ") " + + data.message + + " - " + + linkedInErrorResponse; + + throw Logger.error(error); + } + data["headers"] = {}; + for (const [name, value] of response.headers) { + data["headers"][name] = value; + } + Logger.trace("Linkedin.handleApiResponse", "success"); + return data; + } + + /* + * Handle api error + * + */ + public handleApiError(error: Error): Promise { + throw Logger.error("Linkedin.handleApiError", error); + } +} diff --git a/src/platforms/LinkedIn/LinkedInAuth.ts b/src/platforms/LinkedIn/LinkedInAuth.ts index d4f5ae7..14b47c8 100644 --- a/src/platforms/LinkedIn/LinkedInAuth.ts +++ b/src/platforms/LinkedIn/LinkedInAuth.ts @@ -1,8 +1,8 @@ import Logger from "../../services/Logger"; -import OAuth2Client from "../../auth/OAuth2Client"; +import OAuth2Service from "../../services/OAuth2Service"; import Storage from "../../services/Storage"; -export default class LinkedInAuth extends OAuth2Client { +export default class LinkedInAuth { API_VERSION = "v2"; accessToken = ""; @@ -58,7 +58,7 @@ export default class LinkedInAuth extends OAuth2Client { url.pathname = "oauth/" + this.API_VERSION + "/authorization"; const query = { client_id: clientId, - redirect_uri: this.getCallbackUrl(), + redirect_uri: OAuth2Service.getCallbackUrl(), state: state, response_type: "code", duration: "permanent", @@ -70,7 +70,10 @@ export default class LinkedInAuth extends OAuth2Client { }; url.search = new URLSearchParams(query).toString(); - const result = await this.requestRemotePermissions("LinkedIn", url.href); + const result = await OAuth2Service.requestRemotePermissions( + "LinkedIn", + url.href, + ); if (result["error"]) { const msg = result["error_reason"] + " - " + result["error_description"]; throw Logger.error(msg, result); @@ -94,7 +97,7 @@ export default class LinkedInAuth extends OAuth2Client { refresh_token: string; }> { Logger.trace("RedditAuth", "exchangeCode", code); - const redirectUri = this.getCallbackUrl(); + const redirectUri = OAuth2Service.getCallbackUrl(); const result = (await this.post("accessToken", { grant_type: "authorization_code", diff --git a/src/platforms/Reddit/Reddit.ts b/src/platforms/Reddit/Reddit.ts index 5b1ce39..f63cdd2 100644 --- a/src/platforms/Reddit/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -8,6 +8,7 @@ import Folder from "../../models/Folder"; import Logger from "../../services/Logger"; import Platform from "../../models/Platform"; import { PlatformId } from ".."; +import RedditApi from "./RedditApi"; import RedditAuth from "./RedditAuth"; import Storage from "../../services/Storage"; import { XMLParser } from "fast-xml-parser"; @@ -19,12 +20,13 @@ export default class Reddit extends Platform { id = PlatformId.REDDIT; SUBREDDIT: string; - API_VERSION = "v1"; + api: RedditApi; auth: RedditAuth; constructor() { super(); this.SUBREDDIT = Storage.get("settings", "REDDIT_SUBREDDIT", ""); + this.api = new RedditApi(); this.auth = new RedditAuth(); } @@ -35,7 +37,7 @@ export default class Reddit extends Platform { /** @inheritdoc */ async test() { - const me = await this.get("me"); + const me = await this.api.get("me"); if (!me) return false; return { id: me["id"], @@ -126,7 +128,7 @@ export default class Reddit extends Platform { ): Promise { Logger.trace("Reddit.publishText"); if (!dryrun) { - return (await this.post("submit", { + return (await this.api.post("submit", { sr: this.SUBREDDIT, kind: "self", title: title, @@ -157,7 +159,7 @@ export default class Reddit extends Platform { const lease = await this.getUploadLease(file); const imageUrl = await this.uploadFile(lease, file); if (!dryrun) { - return (await this.post("submit", { + return (await this.api.post("submit", { sr: this.SUBREDDIT, kind: "image", title: title, @@ -188,7 +190,7 @@ export default class Reddit extends Platform { const lease = await this.getUploadLease(file); const videoUrl = await this.uploadFile(lease, file); if (!dryrun) { - return (await this.post("submit", { + return (await this.api.post("submit", { sr: this.SUBREDDIT, kind: "video", title: title, @@ -224,7 +226,7 @@ export default class Reddit extends Platform { form.append("filepath", filename); form.append("mimetype", mimetype); - const lease = (await this.postFormData("media/asset.json", form)) as { + const lease = (await this.api.postFormData("media/asset.json", form)) as { args: { action: string; fields: { @@ -288,131 +290,4 @@ export default class Reddit extends Platform { throw Logger.error(msg, response, e); } } - - // API implementation ------------------- - - /** - * Do a GET request on the api. - * @param endpoint - the path to call - * @param query - query string as object - */ - - private async get( - endpoint: string, - query: { [key: string]: string } = {}, - ): Promise { - const url = new URL("https://oauth.reddit.com"); - url.pathname = "api/" + this.API_VERSION + "/" + endpoint; - url.search = new URLSearchParams(query).toString(); - - const accessToken = await this.auth.getAccessToken(); - - Logger.trace("GET", url.href); - return await fetch(url, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: "Bearer " + accessToken, - "User-Agent": Storage.get("settings", "USER_AGENT"), - }, - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Do a url-encoded POST request on the api. - * @param endpoint - the path to call - * @param body - body as object - */ - - private async post( - endpoint: string, - body: { [key: string]: string }, - ): Promise { - const url = new URL("https://oauth.reddit.com"); - //url.pathname = "api/" + this.API_VERSION + "/" + endpoint; - url.pathname = "api/" + endpoint; - - const accessToken = await this.auth.getAccessToken(); - Logger.trace("POST", url.href); - - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/x-www-form-urlencoded", - Authorization: "Bearer " + accessToken, - "User-Agent": Storage.get("settings", "USER_AGENT"), - }, - body: new URLSearchParams(body), - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Do a FormData POST request on the api. - * @param endpoint - the path to call - * @param body - body as object - */ - - private async postFormData( - endpoint: string, - body: FormData, - ): Promise { - const url = new URL("https://oauth.reddit.com"); - //url.pathname = "api/" + this.API_VERSION + "/" + endpoint; - url.pathname = "api/" + endpoint; - - const accessToken = await this.auth.getAccessToken(); - Logger.trace("POST", url.href); - - return await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - Authorization: "Bearer " + accessToken, - "User-Agent": Storage.get("settings", "USER_AGENT"), - }, - body: body, - }) - .then((res) => this.handleApiResponse(res)) - .catch((err) => this.handleApiError(err)); - } - - /** - * Handle api response - * @param response - api response from fetch - * @returns parsed object from response - */ - private async handleApiResponse(response: Response): Promise { - if (!response.ok) { - throw Logger.error( - "Reddit.handleApiResponse", - "not ok", - response.status + ":" + response.statusText, - ); - } - const data = await response.json(); - if (data.json?.errors?.length) { - const error = - response.status + - ":" + - data.json.errors[0] + - "-" + - data.json.errors.slice(1).join(); - throw Logger.error("Reddit.handleApiResponse", error); - } - Logger.trace("Reddit.handleApiResponse", "success"); - return data; - } - - /** - * Handle api error - * @param error - the error returned from fetch - */ - private handleApiError(error: Error): never { - throw Logger.error("Reddit.handleApiError", error); - } } diff --git a/src/platforms/Reddit/RedditApi.ts b/src/platforms/Reddit/RedditApi.ts new file mode 100644 index 0000000..a3cd052 --- /dev/null +++ b/src/platforms/Reddit/RedditApi.ts @@ -0,0 +1,128 @@ +import Logger from "../../services/Logger"; +import Storage from "../../services/Storage"; + +export default class RedditApi { + API_VERSION = "v1"; + + /** + * Do a GET request on the api. + * @param endpoint - the path to call + * @param query - query string as object + */ + + public async get( + endpoint: string, + query: { [key: string]: string } = {}, + ): Promise { + const url = new URL("https://oauth.reddit.com"); + url.pathname = "api/" + this.API_VERSION + "/" + endpoint; + url.search = new URLSearchParams(query).toString(); + + const accessToken = Storage.get("auth", "REDDIT_ACCESS_TOKEN"); + + Logger.trace("GET", url.href); + return await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + Authorization: "Bearer " + accessToken, + "User-Agent": Storage.get("settings", "USER_AGENT"), + }, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Do a url-encoded POST request on the api. + * @param endpoint - the path to call + * @param body - body as object + */ + + public async post( + endpoint: string, + body: { [key: string]: string }, + ): Promise { + const url = new URL("https://oauth.reddit.com"); + //url.pathname = "api/" + this.API_VERSION + "/" + endpoint; + url.pathname = "api/" + endpoint; + + const accessToken = Storage.get("auth", "REDDIT_ACCESS_TOKEN"); + Logger.trace("POST", url.href); + + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + Authorization: "Bearer " + accessToken, + "User-Agent": Storage.get("settings", "USER_AGENT"), + }, + body: new URLSearchParams(body), + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Do a FormData POST request on the api. + * @param endpoint - the path to call + * @param body - body as object + */ + + public async postFormData(endpoint: string, body: FormData): Promise { + const url = new URL("https://oauth.reddit.com"); + //url.pathname = "api/" + this.API_VERSION + "/" + endpoint; + url.pathname = "api/" + endpoint; + + const accessToken = Storage.get("auth", "REDDIT_ACCESS_TOKEN"); + Logger.trace("POST", url.href); + + return await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + Authorization: "Bearer " + accessToken, + "User-Agent": Storage.get("settings", "USER_AGENT"), + }, + body: body, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /** + * Handle api response + * @param response - api response from fetch + * @returns parsed object from response + */ + private async handleApiResponse(response: Response): Promise { + if (!response.ok) { + throw Logger.error( + "Reddit.handleApiResponse", + "not ok", + response.status + ":" + response.statusText, + ); + } + const data = await response.json(); + if (data.json?.errors?.length) { + const error = + response.status + + ":" + + data.json.errors[0] + + "-" + + data.json.errors.slice(1).join(); + throw Logger.error("Reddit.handleApiResponse", error); + } + Logger.trace("Reddit.handleApiResponse", "success"); + return data; + } + + /** + * Handle api error + * @param error - the error returned from fetch + */ + private handleApiError(error: Error): never { + throw Logger.error("Reddit.handleApiError", error); + } +} diff --git a/src/platforms/Reddit/RedditAuth.ts b/src/platforms/Reddit/RedditAuth.ts index 5b41d68..21255e5 100644 --- a/src/platforms/Reddit/RedditAuth.ts +++ b/src/platforms/Reddit/RedditAuth.ts @@ -1,8 +1,8 @@ import Logger from "../../services/Logger"; -import OAuth2Client from "../../auth/OAuth2Client"; +import OAuth2Service from "../../services/OAuth2Service"; import Storage from "../../services/Storage"; -export default class RedditAuth extends OAuth2Client { +export default class RedditAuth { API_VERSION = "v1"; accessToken = ""; @@ -25,6 +25,8 @@ export default class RedditAuth extends OAuth2Client { * of using an access token from the Storage, the Reddit * platform gets its token from here, which refreshes * it if needed using the refresh_token + * + * ~~ TODO the api cant access these * @returns The access token */ public async refreshAccessToken(): Promise { @@ -54,7 +56,7 @@ export default class RedditAuth extends OAuth2Client { url.pathname = "api/" + this.API_VERSION + "/authorize"; const query = { client_id: clientId, - redirect_uri: this.getCallbackUrl(), + redirect_uri: OAuth2Service.getCallbackUrl(), state: state, response_type: "code", duration: "permanent", @@ -62,7 +64,10 @@ export default class RedditAuth extends OAuth2Client { }; url.search = new URLSearchParams(query).toString(); - const result = await this.requestRemotePermissions("Reddit", url.href); + const result = await OAuth2Service.requestRemotePermissions( + "Reddit", + url.href, + ); if (result["error"]) { const msg = result["error_reason"] + " - " + result["error_description"]; throw Logger.error(msg, result); @@ -86,7 +91,7 @@ export default class RedditAuth extends OAuth2Client { refresh_token: string; }> { Logger.trace("RedditAuth", "exchangeCode", code); - const redirectUri = this.getCallbackUrl(); + const redirectUri = OAuth2Service.getCallbackUrl(); const result = (await this.post("access_token", { grant_type: "authorization_code", diff --git a/src/platforms/Twitter/TwitterAuth.ts b/src/platforms/Twitter/TwitterAuth.ts index 4c96a9d..f6837b6 100644 --- a/src/platforms/Twitter/TwitterAuth.ts +++ b/src/platforms/Twitter/TwitterAuth.ts @@ -1,9 +1,9 @@ import Logger from "../../services/Logger"; -import OAuth2Client from "../../auth/OAuth2Client"; +import OAuth2Service from "../../services/OAuth2Service"; import Storage from "../../services/Storage"; import { TwitterApi } from "twitter-api-v2"; -export default class TwitterAuth extends OAuth2Client { +export default class TwitterAuth extends OAuth2Service { async setup() { const tokens = await this.requestAccessToken(); Storage.set("auth", "TWITTER_ACCESS_TOKEN", tokens["accessToken"]); @@ -21,13 +21,13 @@ export default class TwitterAuth extends OAuth2Client { clientSecret: Storage.get("settings", "TWITTER_CLIENT_SECRET"), }); const { url, codeVerifier, state } = client.generateOAuth2AuthLink( - this.getCallbackUrl(), + OAuth2Service.getCallbackUrl(), { scope: ["users.read", "tweet.read", "tweet.write", "offline.access"], }, ); - const result = await this.requestRemotePermissions("Twitter", url); + const result = await OAuth2Service.requestRemotePermissions("Twitter", url); if (result["error"]) { const msg = result["error_reason"] + " - " + result["error_description"]; throw Logger.error(msg, result); @@ -44,7 +44,7 @@ export default class TwitterAuth extends OAuth2Client { const tokens = await client.loginWithOAuth2({ code: result["code"] as string, codeVerifier: codeVerifier, - redirectUri: this.getCallbackUrl(), + redirectUri: OAuth2Service.getCallbackUrl(), }); if (!tokens["accessToken"]) { throw Logger.error("An accessToken was not returned"); diff --git a/src/services/Logger.ts b/src/services/Logger.ts index 525ef92..4952375 100644 --- a/src/services/Logger.ts +++ b/src/services/Logger.ts @@ -63,13 +63,17 @@ class Logger { } error(...args): Error { this.logger.error(args); - return new Error(args.filter((arg) => typeof arg === "string").join("; ")); + return new Error( + "Error: " + args.filter((arg) => typeof arg === "string").join("; "), + ); } fatal(...args): Error { this.logger.fatal(args); const code = parseInt(args[0]); process.exitCode = code || 1; - return new Error(args.filter((arg) => typeof arg === "string").join("; ")); + return new Error( + "Fatal: " + args.filter((arg) => typeof arg === "string").join("; "), + ); } } export default Logger.getInstance(); diff --git a/src/services/Storage.ts b/src/services/Storage.ts index fdb0c10..76c50a1 100644 --- a/src/services/Storage.ts +++ b/src/services/Storage.ts @@ -40,7 +40,9 @@ class Storage { } if (!value) { if (def === undefined) { - throw Logger.error("Value " + key + " not found in store " + store); + throw Logger.error( + "Value " + key + " not found in store '" + store + "'", + ); } value = def; } From df653befa83f21788fa361ee372b3353fa73c12d Mon Sep 17 00:00:00 2001 From: "pike @ minimoon" Date: Sat, 9 Dec 2023 15:27:14 +0100 Subject: [PATCH 5/6] chore: Add docblocks --- src/platforms/Facebook/FacebookApi.ts | 4 ++++ src/platforms/Instagram/InstagramApi.ts | 6 ++++-- src/platforms/LinkedIn/LinkedInApi.ts | 4 ++++ src/platforms/Reddit/RedditApi.ts | 4 ++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/platforms/Facebook/FacebookApi.ts b/src/platforms/Facebook/FacebookApi.ts index c9946e9..d393c40 100644 --- a/src/platforms/Facebook/FacebookApi.ts +++ b/src/platforms/Facebook/FacebookApi.ts @@ -1,6 +1,10 @@ import Logger from "../../services/Logger"; import Storage from "../../services/Storage"; +/** + * FacebookApi: support for facebook platform. + */ + export default class FacebookApi { GRAPH_API_VERSION = "v18.0"; diff --git a/src/platforms/Instagram/InstagramApi.ts b/src/platforms/Instagram/InstagramApi.ts index 5f06598..7cb3c30 100644 --- a/src/platforms/Instagram/InstagramApi.ts +++ b/src/platforms/Instagram/InstagramApi.ts @@ -1,11 +1,13 @@ import Logger from "../../services/Logger"; import Storage from "../../services/Storage"; +/** + * InstagramApi: support for instagram platform. + */ + export default class InstagramApi { GRAPH_API_VERSION = "v18.0"; - // API implementation ------------------- - /** * Do a GET request on the graph. * @param endpoint - the path to call diff --git a/src/platforms/LinkedIn/LinkedInApi.ts b/src/platforms/LinkedIn/LinkedInApi.ts index c251bde..df0f2ba 100644 --- a/src/platforms/LinkedIn/LinkedInApi.ts +++ b/src/platforms/LinkedIn/LinkedInApi.ts @@ -1,6 +1,10 @@ import Logger from "../../services/Logger"; import Storage from "../../services/Storage"; +/** + * LinkedInApi: support for linkedin platform. + */ + export default class LinkedInApi { LGC_API_VERSION = "v2"; API_VERSION = "202304"; diff --git a/src/platforms/Reddit/RedditApi.ts b/src/platforms/Reddit/RedditApi.ts index a3cd052..8b1c767 100644 --- a/src/platforms/Reddit/RedditApi.ts +++ b/src/platforms/Reddit/RedditApi.ts @@ -1,6 +1,10 @@ import Logger from "../../services/Logger"; import Storage from "../../services/Storage"; +/** + * RedditApi: support for reddit platform. + */ + export default class RedditApi { API_VERSION = "v1"; From 259d35021a52f455def432848617965a66c64e63 Mon Sep 17 00:00:00 2001 From: pike Date: Sat, 9 Dec 2023 20:54:22 +0100 Subject: [PATCH 6/6] chore: Tested and working .. .. except, twitter and reddit keys are not automatically refreshed anymore. will solve this differently. --- src/platforms/Facebook/Facebook.ts | 2 +- src/platforms/Instagram/Instagram.ts | 2 +- src/platforms/LinkedIn/LinkedIn.ts | 2 +- src/platforms/Reddit/Reddit.ts | 2 +- src/platforms/Twitter/Twitter.ts | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/platforms/Facebook/Facebook.ts b/src/platforms/Facebook/Facebook.ts index 471291a..e9f4632 100644 --- a/src/platforms/Facebook/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -70,7 +70,7 @@ export default class Facebook extends Platform { /** @inheritdoc */ async publishPost(post: Post, dryrun: boolean = false): Promise { - Logger.trace("Facebook.publishPost", post, dryrun); + Logger.trace("Facebook.publishPost", post.id, dryrun); let response = dryrun ? { id: "-99" } diff --git a/src/platforms/Instagram/Instagram.ts b/src/platforms/Instagram/Instagram.ts index 94639a7..42e9ffa 100644 --- a/src/platforms/Instagram/Instagram.ts +++ b/src/platforms/Instagram/Instagram.ts @@ -84,7 +84,7 @@ export default class Instagram extends Platform { /** @inheritdoc */ async publishPost(post: Post, dryrun: boolean = false): Promise { - Logger.trace("Instagram.publishPost", post, dryrun); + Logger.trace("Instagram.publishPost", post.id, dryrun); let response = dryrun ? { id: "-99" } : ({} as { id: string }); let error = undefined; diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index 10c097a..98f98e0 100644 --- a/src/platforms/LinkedIn/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -74,7 +74,7 @@ export default class LinkedIn extends Platform { } async publishPost(post: Post, dryrun: boolean = false): Promise { - Logger.trace("LinkedIn.publishPost", post, dryrun); + Logger.trace("LinkedIn.publishPost", post.id, dryrun); let response = dryrun ? { id: "-99" } diff --git a/src/platforms/Reddit/Reddit.ts b/src/platforms/Reddit/Reddit.ts index f63cdd2..08d8816 100644 --- a/src/platforms/Reddit/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -76,7 +76,7 @@ export default class Reddit extends Platform { } async publishPost(post: Post, dryrun: boolean = false): Promise { - Logger.trace("Reddit.publishPost", post, dryrun); + Logger.trace("Reddit.publishPost", post.id, dryrun); let response = dryrun ? { dryrun: true } : {}; let error = undefined; diff --git a/src/platforms/Twitter/Twitter.ts b/src/platforms/Twitter/Twitter.ts index e3aee97..ca4a061 100644 --- a/src/platforms/Twitter/Twitter.ts +++ b/src/platforms/Twitter/Twitter.ts @@ -71,6 +71,7 @@ export default class Twitter extends Platform { } async publishPost(post: Post, dryrun: boolean = false): Promise { + Logger.trace("Twitter.publishPost", post.id, dryrun); const client1 = new TwitterApi({ appKey: Storage.get("settings", "TWITTER_OA1_API_KEY"), appSecret: Storage.get("settings", "TWITTER_OA1_API_KEY_SECRET"),