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..e38e6a3 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"; @@ -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/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 52% rename from src/platforms/Facebook.ts rename to src/platforms/Facebook/Facebook.ts index adc2bf6..e9f4632 100644 --- a/src/platforms/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -2,14 +2,15 @@ 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 FacebookApi from "./FacebookApi"; +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. @@ -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 */ @@ -67,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" } @@ -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..d393c40 --- /dev/null +++ b/src/platforms/Facebook/FacebookApi.ts @@ -0,0 +1,141 @@ +import Logger from "../../services/Logger"; +import Storage from "../../services/Storage"; + +/** + * FacebookApi: support for facebook platform. + */ + +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/auth/FacebookAuth.ts b/src/platforms/Facebook/FacebookAuth.ts similarity index 94% rename from src/auth/FacebookAuth.ts rename to src/platforms/Facebook/FacebookAuth.ts index c3e9925..c88b712 100644 --- a/src/auth/FacebookAuth.ts +++ b/src/platforms/Facebook/FacebookAuth.ts @@ -1,8 +1,8 @@ -import Logger from "../services/Logger"; -import OAuth2Client from "./OAuth2Client"; -import Storage from "../services/Storage"; +import Logger from "../../services/Logger"; +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.ts b/src/platforms/Instagram/Instagram.ts similarity index 65% rename from src/platforms/Instagram.ts rename to src/platforms/Instagram/Instagram.ts index 6b92882..42e9ffa 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 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"; /** * 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 */ @@ -82,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; @@ -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..7cb3c30 --- /dev/null +++ b/src/platforms/Instagram/InstagramApi.ts @@ -0,0 +1,156 @@ +import Logger from "../../services/Logger"; +import Storage from "../../services/Storage"; + +/** + * InstagramApi: support for instagram platform. + */ + +export default class InstagramApi { + GRAPH_API_VERSION = "v18.0"; + + /** + * 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/auth/InstagramAuth.ts b/src/platforms/Instagram/InstagramAuth.ts similarity index 83% rename from src/auth/InstagramAuth.ts rename to src/platforms/Instagram/InstagramAuth.ts index cae66c7..647884f 100644 --- a/src/auth/InstagramAuth.ts +++ b/src/platforms/Instagram/InstagramAuth.ts @@ -1,6 +1,7 @@ -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 OAuth2Service from "../../services/OAuth2Service"; +import Storage from "../../services/Storage"; export default class InstagramAuth extends FacebookAuth { async setup() { @@ -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.ts b/src/platforms/LinkedIn/LinkedIn.ts similarity index 65% rename from src/platforms/LinkedIn.ts rename to src/platforms/LinkedIn/LinkedIn.ts index 65b70f2..98f98e0 100644 --- a/src/platforms/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -2,21 +2,21 @@ 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 LinkedInApi from "./LinkedInApi"; +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; + 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:" + @@ -73,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" } @@ -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..df0f2ba --- /dev/null +++ b/src/platforms/LinkedIn/LinkedInApi.ts @@ -0,0 +1,121 @@ +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"; + + /** + * 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/auth/LinkedInAuth.ts b/src/platforms/LinkedIn/LinkedInAuth.ts similarity index 92% rename from src/auth/LinkedInAuth.ts rename to src/platforms/LinkedIn/LinkedInAuth.ts index 6107e5b..14b47c8 100644 --- a/src/auth/LinkedInAuth.ts +++ b/src/platforms/LinkedIn/LinkedInAuth.ts @@ -1,8 +1,8 @@ -import Logger from "../services/Logger"; -import OAuth2Client from "./OAuth2Client"; -import Storage from "../services/Storage"; +import Logger from "../../services/Logger"; +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.ts b/src/platforms/Reddit/Reddit.ts similarity index 61% rename from src/platforms/Reddit.ts rename to src/platforms/Reddit/Reddit.ts index 29824b2..08d8816 100644 --- a/src/platforms/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -2,14 +2,15 @@ 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 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"], @@ -74,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; @@ -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..8b1c767 --- /dev/null +++ b/src/platforms/Reddit/RedditApi.ts @@ -0,0 +1,132 @@ +import Logger from "../../services/Logger"; +import Storage from "../../services/Storage"; + +/** + * RedditApi: support for reddit platform. + */ + +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/auth/RedditAuth.ts b/src/platforms/Reddit/RedditAuth.ts similarity index 92% rename from src/auth/RedditAuth.ts rename to src/platforms/Reddit/RedditAuth.ts index 786f7dc..21255e5 100644 --- a/src/auth/RedditAuth.ts +++ b/src/platforms/Reddit/RedditAuth.ts @@ -1,8 +1,8 @@ -import Logger from "../services/Logger"; -import OAuth2Client from "./OAuth2Client"; -import Storage from "../services/Storage"; +import Logger from "../../services/Logger"; +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.ts b/src/platforms/Twitter/Twitter.ts similarity index 91% rename from src/platforms/Twitter.ts rename to src/platforms/Twitter/Twitter.ts index 72434e7..ca4a061 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 @@ -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"), diff --git a/src/auth/TwitterAuth.ts b/src/platforms/Twitter/TwitterAuth.ts similarity index 79% rename from src/auth/TwitterAuth.ts rename to src/platforms/Twitter/TwitterAuth.ts index 3d045e4..f6837b6 100644 --- a/src/auth/TwitterAuth.ts +++ b/src/platforms/Twitter/TwitterAuth.ts @@ -1,9 +1,9 @@ -import Logger from "../services/Logger"; -import OAuth2Client from "./OAuth2Client"; -import Storage from "../services/Storage"; +import Logger from "../../services/Logger"; +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/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"; 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); } 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/auth/OAuth2Client.ts b/src/services/OAuth2Service.ts similarity index 91% rename from src/auth/OAuth2Client.ts rename to src/services/OAuth2Service.ts index 0556f79..5b2e394 100644 --- a/src/auth/OAuth2Client.ts +++ b/src/services/OAuth2Service.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as http from "http"; import * as url from "url"; -import Storage from "../services/Storage"; +import Storage from "./Storage"; class DeferredResponseQuery { promise: Promise<{ [key: string]: string | string[] }>; @@ -16,17 +16,17 @@ class DeferredResponseQuery { } /** - * OAuth2Client: abstract handler to launch a webserver for + * OAuth2Service: Static service to launch a webserver for * requesting remote permissions on a service */ -export default class OAuth2Client { - protected getRequestUrl(): string { +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}`; } - protected getCallbackUrl(): string { + public static getCallbackUrl(): string { return this.getRequestUrl() + "/callback"; } @@ -44,7 +44,7 @@ export default class OAuth2Client { * @returns a flat object of returned query */ - protected async requestRemotePermissions( + public static async requestRemotePermissions( serviceName: string, serviceLink: string, ): Promise<{ [key: string]: string | string[] }> { 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; }