From 1ecf1ae99c6a520eae453bfdfe0779bd76ca01f6 Mon Sep 17 00:00:00 2001 From: pike Date: Sun, 15 Oct 2023 11:45:31 +0200 Subject: [PATCH 1/3] feat: Update facebook-api --- docs/Ayrshare.md | 32 ++++ docs/Facebook.md | 2 +- src/platforms/Facebook.ts | 333 ++++++++++++++++++++++---------------- src/platforms/index.ts | 6 +- 4 files changed, 232 insertions(+), 141 deletions(-) create mode 100644 docs/Ayrshare.md diff --git a/docs/Ayrshare.md b/docs/Ayrshare.md new file mode 100644 index 0000000..7bc41f6 --- /dev/null +++ b/docs/Ayrshare.md @@ -0,0 +1,32 @@ +# Platform: Ayrshare + +Ayrshare (https://www.ayrshare.com/) is a platform / service +that does what FairPost does. I don't know why you would +use both. + +But if you have an Ayrshare account, you can enable +it here and enable the platforms that you have connected +to Ayrshare, to publish to those platforms via Ayrshare. + +The Ayrshare platforms supported by FairPost are +- asfacebook +- asinstagram +- aslinkedin +- asreddit +- astiktok +- asyoutube + +## Setting up the Ayrshare platform + +- get an account at Ayrshare +- get your Api key at https://app.ayrshare.com/api +- store this key as FAIRPOST_AYRSHARE_API_KEY + +### Enable and test the facebook platform + - Add one or more of the 'as*' platforms to `FAIRPOST_FEED_PLATFORMS` in `.env` + - call `./fairpost.js test-platforms` + +# Limitations + +Ayrshare applies different limitations to each platform. +For details, check the Ayrshare documentation. \ No newline at end of file diff --git a/docs/Facebook.md b/docs/Facebook.md index f19484a..42905dd 100644 --- a/docs/Facebook.md +++ b/docs/Facebook.md @@ -60,7 +60,7 @@ This token should last forever. It involves get a long-lived user token and then ### Enable and test the facebook platform - Add 'facebook' to your `FAIRPOST_FEED_PLATFORMS` in `.env` - - call `./fairpost.js test --platforms=facebook` + - call `./fairpost.js test-platform --platform=facebook` # Limitations diff --git a/src/platforms/Facebook.ts b/src/platforms/Facebook.ts index ed917a2..7260c3b 100644 --- a/src/platforms/Facebook.ts +++ b/src/platforms/Facebook.ts @@ -60,28 +60,39 @@ export default class Facebook extends Platform { if (post.files.image.length) { for (const image of post.files.image) { attachments.push({ - media_fbid: await this.uploadPhoto(post.folder.path + "/" + image), + media_fbid: ( + await this.uploadPhoto(post.folder.path + "/" + image) + )["id"], }); } } if (!dryrun) { - result = await this.post("feed", { + result = (await this.postJson("%PAGE%/feed", { message: post.body, published: process.env.FAIRPOST_FACEBOOK_PUBLISH_POSTS, - //"scheduled_publish_time":"tomorrow", attached_media: attachments, - }); + })) as { id: string }; } } - post.results.push(result); + post.results.push({ + date: new Date(), + link: "https://facebook.com/" + result.id, + dryrun: dryrun, + result: result, + }); if (result.id) { if (!dryrun) { post.status = PostStatus.PUBLISHED; post.published = new Date(); } } else { - console.error(this.id, "No id returned in post", result); + Logger.error( + "Facebook.publishPost", + this.id, + "No id returned in post", + result, + ); } post.save(); return !!result.id; @@ -92,68 +103,7 @@ export default class Facebook extends Platform { } /* - * Do a GET request on the page. - * - * arguments: - * endpoint: the path to call - * query: query string as object - */ - - private async get( - endpoint: string = "", - query: { [key: string]: string } = {}, - ) { - const url = new URL("https://graph.facebook.com"); - url.pathname = - this.GRAPH_API_VERSION + "/" + process.env.FAIRPOST_FACEBOOK_PAGE_ID; - (url.pathname += "/" + endpoint), - (url.search = new URLSearchParams(query).toString()); - Logger.trace("GET", url.href); - const res = await fetch(url, { - method: "GET", - headers: process.env.FAIRPOST_FACEBOOK_PAGE_ACCESS_TOKEN - ? { - Accept: "application/json", - Authorization: - "Bearer " + process.env.FAIRPOST_FACEBOOK_PAGE_ACCESS_TOKEN, - } - : { - Accept: "application/json", - }, - }); - const result = await res.json(); - return result; - } - - /* - * Do a POST request on the page. - * - * arguments: - * endpoint: the path to call - * body: body as object - */ - - private async post(endpoint: string = "", body = {}) { - const url = new URL("https://graph.facebook.com"); - url.pathname = - this.GRAPH_API_VERSION + "/" + process.env.FAIRPOST_FACEBOOK_PAGE_ID; - (url.pathname += "/" + endpoint), Logger.trace("POST", url.href); - const res = await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: - "Bearer " + process.env.FAIRPOST_FACEBOOK_PAGE_ACCESS_TOKEN, - }, - body: JSON.stringify(body), - }); - const result = await res.json(); - return result; - } - - /* - * POST an image to the /photos endpoint using multipart/form-data + * POST an image to the page/photos endpoint using multipart/form-data * * arguments: * file: path to the file to post @@ -164,41 +114,27 @@ export default class Facebook extends Platform { private async uploadPhoto( file: string = "", published = false, - ): Promise { + ): Promise<{ id: string }> { Logger.trace("Reading file", file); const rawData = fs.readFileSync(file); const blob = new Blob([rawData]); - const url = new URL("https://graph.facebook.com"); - url.pathname = - this.GRAPH_API_VERSION + "/" + process.env.FAIRPOST_FACEBOOK_PAGE_ID; - url.pathname += "/photos"; - const body = new FormData(); body.set("published", published ? "true" : "false"); body.set("source", blob, path.basename(file)); - Logger.trace("POST", url.href); - const res = await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - Authorization: - "Bearer " + process.env.FAIRPOST_FACEBOOK_PAGE_ACCESS_TOKEN, - }, - body, - }); + const result = (await this.postFormData("%PAGE%/photos", body)) as { + id: "string"; + }; - const result = await res.json(); if (!result["id"]) { - console.error(result); throw new Error("No id returned when uploading photo"); } - return result["id"]; + return result; } /* - * POST a video to the page using multipart/form-data + * POST a video to the page/videos endpoint using multipart/form-data * * arguments: * file: path to the video to post @@ -216,31 +152,17 @@ export default class Facebook extends Platform { const rawData = fs.readFileSync(file); const blob = new Blob([rawData]); - const url = new URL("https://graph.facebook.com"); - url.pathname = - this.GRAPH_API_VERSION + "/" + process.env.FAIRPOST_FACEBOOK_PAGE_ID; - url.pathname += "/videos"; - const body = new FormData(); body.set("title", title); body.set("description", description); body.set("published", process.env.FAIRPOST_FACEBOOK_PUBLISH_POSTS); body.set("source", blob, path.basename(file)); - Logger.trace("POST", url.href); - const res = await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - Authorization: - "Bearer " + process.env.FAIRPOST_FACEBOOK_PAGE_ACCESS_TOKEN, - }, - body, - }); + const result = (await this.postFormData("%PAGE%/videos", body)) as { + id: string; + }; - const result = await res.json(); if (!result["id"]) { - console.error(result); throw new Error("No id returned when uploading video"); } return result; @@ -249,60 +171,43 @@ export default class Facebook extends Platform { /* * Return a long lived page access token. * + * appUserId: a app-scoped-user-id * UserAccessToken: a shortlived user access token */ async getPageToken( appUserId: string, userAccessToken: string, ): Promise { - // get a long lived UserAccessToken - - const url = new URL("https://graph.facebook.com"); - url.pathname = this.GRAPH_API_VERSION + "/oauth/access_token"; - const query = { + // 1. Get a long lived UserAccessToken + const query1 = { grant_type: "fb_exchange_token", client_id: process.env.FAIRPOST_FACEBOOK_APP_ID, client_secret: process.env.FAIRPOST_FACEBOOK_APP_SECRET, fb_exchange_token: userAccessToken, }; - url.search = new URLSearchParams(query).toString(); - - Logger.trace("fetching", url.href); - const res1 = await fetch(url, { - method: "GET", - headers: { Accept: "application/json" }, - }); - const data1 = await res1.json(); + const data1 = (await this.get("oauth/access_token", query1)) as { + access_token: string; + }; const llUserAccessToken = data1["access_token"]; - if (!llUserAccessToken) { console.error(data1); throw new Error("No llUserAccessToken access_token in response."); } - // get a long lived PageAccessToken - - const url2 = new URL("https://graph.facebook.com"); - url2.pathname = appUserId + "/accounts"; + // 2. Get a long lived PageAccessToken const query2 = { access_token: llUserAccessToken, }; - url2.search = new URLSearchParams(query2).toString(); - Logger.trace("fetching", url.href); - const res2 = await fetch(url2, { - method: "GET", - headers: { Accept: "application/json" }, - }); - const data2 = await res2.json(); + const data2 = (await this.get(appUserId + "/accounts", query2)) as { + data: { + id: string; + access_token: string; + }[]; + }; + const llPageAccessToken = data2.data?.find( + (page) => page.id === process.env.FAIRPOST_FACEBOOK_PAGE_ID, + )["access_token"]; - let llPageAccessToken = ""; - if (data2.data) { - data2.data.forEach((page) => { - if (page.id === process.env.FAIRPOST_FACEBOOK_PAGE_ID) { - llPageAccessToken = page.access_token; - } - }); - } if (!llPageAccessToken) { console.error(data2); throw new Error( @@ -314,4 +219,156 @@ export default class Facebook extends Platform { return llPageAccessToken; } + + // API implementation ------------------- + + /* + * Do a GET request on the graph. + * + * arguments: + * endpoint: the path to call + * query: query string as object + */ + + private async get( + endpoint: string = "%USER%", + query: { [key: string]: string } = {}, + ): Promise { + endpoint = endpoint.replace( + "%USER%", + process.env.FAIRPOST_FACEBOOK_USER_ID, + ); + endpoint = endpoint.replace( + "%PAGE%", + process.env.FAIRPOST_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: process.env.FAIRPOST_INSTAGRAM_PAGE_ACCESS_TOKEN + ? { + Accept: "application/json", + Authorization: + "Bearer " + process.env.FAIRPOST_INSTAGRAM_PAGE_ACCESS_TOKEN, + } + : { + Accept: "application/json", + }, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /* + * Do a Json POST request on the graph. + * + * arguments: + * endpoint: the path to call + * body: body as object + */ + + private async postJson( + endpoint: string = "%USER%", + body = {}, + ): Promise { + endpoint = endpoint.replace( + "%USER%", + process.env.FAIRPOST_FACEBOOK_USER_ID, + ); + endpoint = endpoint.replace( + "%PAGE%", + process.env.FAIRPOST_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 " + process.env.FAIRPOST_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. + * + * arguments: + * endpoint: the path to call + * body: body as object + */ + + private async postFormData( + endpoint: string, + body: FormData, + ): Promise { + endpoint = endpoint.replace( + "%USER%", + process.env.FAIRPOST_FACEBOOK_USER_ID, + ); + endpoint = endpoint.replace( + "%PAGE%", + process.env.FAIRPOST_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 " + process.env.FAIRPOST_INSTAGRAM_PAGE_ACCESS_TOKEN, + }, + body: body, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /* + * Handle api response + * + */ + private async handleApiResponse(response: Response): Promise { + 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; + Logger.error("Facebook.handleApiResponse", error); + throw new Error(error); + } + Logger.trace("Facebook.handleApiResponse", "success"); + return data; + } + + /* + * Handle api error + * + */ + private handleApiError(error: Error): Promise { + Logger.error("Facebook.handleApiError", error); + throw error; + } } diff --git a/src/platforms/index.ts b/src/platforms/index.ts index 07be3a3..f0b4518 100644 --- a/src/platforms/index.ts +++ b/src/platforms/index.ts @@ -1,3 +1,5 @@ +export { default as Facebook } from "./Facebook"; +export { default as Instagram } from "./Instagram"; export { default as AsYouTube } from "./AsYouTube"; export { default as AsInstagram } from "./AsInstagram"; export { default as AsTwitter } from "./AsTwitter"; @@ -5,10 +7,11 @@ export { default as AsFacebook } from "./AsFacebook"; export { default as AsTikTok } from "./AsTikTok"; export { default as AsLinkedIn } from "./AsLinkedIn"; export { default as AsReddit } from "./AsReddit"; -export { default as Facebook } from "./Facebook"; export enum PlatformId { UNKNOWN = "unknown", + FACEBOOK = "facebook", + INSTAGRAM = "instagram", ASYOUTUBE = "asyoutube", ASINSTAGRAM = "asinstagram", ASFACEBOOK = "asfacebook", @@ -16,5 +19,4 @@ export enum PlatformId { ASTIKTOK = "astiktok", ASLINKEDIN = "aslinkedin", ASREDDIT = "asreddit", - FACEBOOK = "facebook", } From 794dd59ea3667d012e6065299ae72ae0afda1805 Mon Sep 17 00:00:00 2001 From: pike Date: Sun, 15 Oct 2023 11:48:01 +0200 Subject: [PATCH 2/3] feat: Add instagram setup --- docs/Instagram.md | 111 +++++++++++++ src/platforms/Instagram.ts | 309 +++++++++++++++++++++++++++++++++++++ 2 files changed, 420 insertions(+) create mode 100644 docs/Instagram.md create mode 100644 src/platforms/Instagram.ts diff --git a/docs/Instagram.md b/docs/Instagram.md new file mode 100644 index 0000000..ab6a0cd --- /dev/null +++ b/docs/Instagram.md @@ -0,0 +1,111 @@ +# Platform: Instagram + +The `instagram` platform manage a instagram account +that is connected to a facebook **page* +using the plain facebook graph api - no extensions installed. + +It publishes **carousels** on that instagram account. + +## Setting up the Instagram platform + + +### Create a new App in your facebook account + - create an Instagram business account + - connect a Facebook page to your Instagram business account + - go to https://developers.facebook.com/ + - create an app that can manage pages + - include the "Instagram Graph API" product as a new product + - under 'settings', find your app ID + - save this as `FAIRPOST_INSTAGRAM_APP_ID` in your .env + - under 'settings', find your app secret + - save this as `FAIRPOST_INSTAGRAM_APP_SECRET` in your .env + + +### Find your instagram user id + - go to https://www.instagram.com/web/search/topsearch/?query={username} + - find your fbid_v2 + - note the user id + - save this as `FAIRPOST_INSTAGRAM_USER_ID` in your .env + +### Get a (short lived) Page Access Token for the page related to the instagram account you want the app to manage + +This is good for testing, but you'll have to refresh this token often. + + - go to https://developers.facebook.com/tools/explorer/ + - select your app + - add permissions + - pages_manage_engagement + - pages_manage_posts + - pages_read_engagement + - pages_read_user_engagement + - publish_video + - business_management + - instagram_basic + - instagram_content_publish + - request a (short lived) page access token + - save this as `FAIRPOST_INSTAGRAM_PAGE_ACCESS_TOKEN` in your .env + +### Get a (long lived) Page Access Token for the page related to the instagram account you want the app to manage + +This token should last forever. It involves get a long-lived user token and then requesting the 'accounts' for your 'app scoped user id'; but this app provides a tool to help you do that: + + - go to https://developers.facebook.com/tools/explorer/ + - select your app + - add permissions + - pages_manage_engagement + - pages_manage_posts + - pages_read_engagement + - pages_read_user_engagement + - publish_video + - business_management + - instagram_basic + - instagram_content_publish + - request a (short lived) user access token + - click 'submit' to submit the default `?me` query + - remember the `id` in the response as your id + - call `./fairpost.js facebook-get-page-token + --app-user-id={your id} --user-token={your token}` + - note the token returned + - save this as `FAIRPOST_INSTAGRAM_PAGE_ACCESS_TOKEN` in your .env + +### Enable and test the instagram platform + - Add 'instagram' to your `FAIRPOST_FEED_PLATFORMS` in `.env` + - call `./fairpost.js test-platform --platform=instagram` + +# Limitations + +## Images + +- Carousels are limited to 10 images, videos, or a mix of the two. +- Carousel images are all cropped based on the first image in the carousel, with the default being a 1:1 aspect ratio. + + +### Supported Formats +Instagram supports the following formats: + - JPEG + +### File Size + +xxx + +# Random documentation + +https://developers.facebook.com/docs/instagram-api/guides/content-publishing + +- only jpeg +- rate limit w endpoint +- upload media first + +POST /{ig-user-id}/media — upload media and create media containers. +POST /{ig-user-id}/media_publish — publish uploaded media using their media containers. +GET /{ig-container-id}?fields=status_code — check media container publishing eligibility and status. +GET /{ig-user-id}/content_publishing_limit — check app user's current publishing rate limit usage. + +~~~ +GET /{ig-container-id}?fields=status_code endpoint. This endpoint will return one of the following: + +EXPIRED — The container was not published within 24 hours and has expired. +ERROR — The container failed to complete the publishing process. +FINISHED — The container and its media object are ready to be published. +IN_PROGRESS — The container is still in the publishing process. +PUBLISHED — The container's media object has been published. diff --git a/src/platforms/Instagram.ts b/src/platforms/Instagram.ts new file mode 100644 index 0000000..055f902 --- /dev/null +++ b/src/platforms/Instagram.ts @@ -0,0 +1,309 @@ +import Logger from "../Logger"; +import Platform from "../Platform"; +import { PlatformId } from "."; +import Folder from "../Folder"; +import Post from "../Post"; +import { PostStatus } from "../Post"; +import * as fs from "fs"; +import * as path from "path"; +import * as sharp from "sharp"; + +export default class Instagram extends Platform { + id: PlatformId = PlatformId.INSTAGRAM; + GRAPH_API_VERSION: string = "v18.0"; + + constructor() { + super(); + } + + async preparePost(folder: Folder): Promise { + const post = await super.preparePost(folder); + if (post && post.files) { + // instagram: 1 video for reel + if (post.files.video.length) { + Logger.trace("Removing images for instagram reel.."); + post.files.image = []; + if (post.files.video.length > 1) { + Logger.trace("Using first video for instagram reel.."); + post.files.video = [post.files.video[0]]; + } + } + // instagram : scale images, jpeg only + for (const image of post.files.image) { + const metadata = await sharp(post.folder.path + "/" + image).metadata(); + if (metadata.width > 1440) { + Logger.trace("Resizing " + image + " for instagram .."); + const extension = image.split(".")?.pop(); + const basename = path.basename( + image, + extension ? "." + extension : "", + ); + await sharp(post.folder.path + "/" + image) + .resize({ + width: 1440, + }) + .toFile(post.folder.path + "/_instagram-" + basename + ".JPEG"); + post.files.image.push("_instagram-" + image); + post.files.image = post.files.image.filter((file) => file !== image); + } + } + + // instagram: require media + if (post.files.image.length + post.files.video.length === 0) { + post.valid = false; + } + post.save(); + } + return post; + } + + + async publishPost(post: Post, dryrun: boolean = false): Promise { + console.log("Instagram.publishPost",post,dryrun); + throw new Error("not implemented"); + } + + async test() { + return this.testUploadCarousel(); + } + + async testUploadCarousel() { + // upload photo to facebook + const photoId = (await this.uploadPhoto( + "/Users/pike/Desktop/test/test.jpg", + false, + ))["id"]; + if (!photoId) return; + + // get photo link + const photoData = (await this.get(photoId, { + fields: "link,images,picture", + })) as { + images: { + width: number; + height: number; + source: string; + }[]; + }; + if (!photoData) return; + + const maxPhoto = photoData.images?.reduce(function (prev, current) { + return prev && prev.width > current.width ? prev : current; + }); + if (!maxPhoto) return; + + const photoLink = maxPhoto["source"]; + + // upload link to instagram + const uploadId = ( + await this.postJson("%USER%/media", { + is_carousel_item: true, + image_url: photoLink, + }) + )["id"]; + if (!uploadId) return; + + // create carousel + const carouselId = ( + await this.postJson("%USER%/media", { + media_type: "CAROUSEL", + caption: "test", + children: [uploadId, uploadId].join(","), + }) + )["id"]; + if (!carouselId) return; + + // publish carousel + const igMediaId = ( + await this.postJson("%USER%/media_publish", { + creation_id: carouselId, + }) + )["id"]; + if (!igMediaId) return; + + return igMediaId; + } + + /* + * POST an image to the page/photos endpoint using multipart/form-data + * + * arguments: + * file: path to the file to post + * + * returns: + * id of the uploaded photo to use in post attachments + */ + private async uploadPhoto( + file: string = "", + published = false, + ): Promise<{ id: string }> { + Logger.trace("Reading file", file); + const rawData = fs.readFileSync(file); + const blob = new Blob([rawData]); + + const body = new FormData(); + body.set("published", published ? "true" : "false"); + body.set("source", blob, path.basename(file)); + + const result = (await this.postFormData("%PAGE%/photos", body)) as { + id: "string"; + }; + + if (!result["id"]) { + throw new Error("No id returned when uploading photo"); + } + return result; + } + + // API implementation ------------------- + + /* + * Do a GET request on the graph. + * + * arguments: + * endpoint: the path to call + * query: query string as object + */ + + private async get( + endpoint: string = "%USER%", + query: { [key: string]: string } = {}, + ): Promise { + endpoint = endpoint.replace( + "%USER%", + process.env.FAIRPOST_INSTAGRAM_USER_ID, + ); + endpoint = endpoint.replace( + "%PAGE%", + process.env.FAIRPOST_INSTAGRAM_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: process.env.FAIRPOST_INSTAGRAM_PAGE_ACCESS_TOKEN + ? { + Accept: "application/json", + Authorization: + "Bearer " + process.env.FAIRPOST_INSTAGRAM_PAGE_ACCESS_TOKEN, + } + : { + Accept: "application/json", + }, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /* + * Do a Json POST request on the graph. + * + * arguments: + * endpoint: the path to call + * body: body as object + */ + + private async postJson( + endpoint: string = "%USER%", + body = {}, + ): Promise { + endpoint = endpoint.replace( + "%USER%", + process.env.FAIRPOST_INSTAGRAM_USER_ID, + ); + endpoint = endpoint.replace( + "%PAGE%", + process.env.FAIRPOST_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 " + process.env.FAIRPOST_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. + * + * arguments: + * endpoint: the path to call + * body: body as object + */ + + private async postFormData( + endpoint: string, + body: FormData, + ): Promise { + endpoint = endpoint.replace( + "%USER%", + process.env.FAIRPOST_INSTAGRAM_USER_ID, + ); + endpoint = endpoint.replace( + "%PAGE%", + process.env.FAIRPOST_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 " + process.env.FAIRPOST_INSTAGRAM_PAGE_ACCESS_TOKEN, + }, + body: body, + }) + .then((res) => this.handleApiResponse(res)) + .catch((err) => this.handleApiError(err)); + } + + /* + * Handle api response + * + */ + private async handleApiResponse(response: Response): Promise { + 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; + Logger.error("Facebook.handleApiResponse", error); + throw new Error(error); + } + Logger.trace("Facebook.handleApiResponse", "success"); + return data; + } + + /* + * Handle api error + * + */ + private handleApiError(error: Error): Promise { + Logger.error("Facebook.handleApiError", error); + throw error; + } +} From a1837a39b67e2b48e8b1beba76d59ecc06439ff7 Mon Sep 17 00:00:00 2001 From: pike Date: Sun, 15 Oct 2023 12:04:19 +0200 Subject: [PATCH 3/3] fix: Lint --- src/platforms/Instagram.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/platforms/Instagram.ts b/src/platforms/Instagram.ts index 055f902..05f9b1b 100644 --- a/src/platforms/Instagram.ts +++ b/src/platforms/Instagram.ts @@ -3,7 +3,7 @@ import Platform from "../Platform"; import { PlatformId } from "."; import Folder from "../Folder"; import Post from "../Post"; -import { PostStatus } from "../Post"; +//import { PostStatus } from "../Post"; import * as fs from "fs"; import * as path from "path"; import * as sharp from "sharp"; @@ -57,9 +57,8 @@ export default class Instagram extends Platform { return post; } - async publishPost(post: Post, dryrun: boolean = false): Promise { - console.log("Instagram.publishPost",post,dryrun); + console.log("Instagram.publishPost", post, dryrun); throw new Error("not implemented"); } @@ -69,10 +68,9 @@ export default class Instagram extends Platform { async testUploadCarousel() { // upload photo to facebook - const photoId = (await this.uploadPhoto( - "/Users/pike/Desktop/test/test.jpg", - false, - ))["id"]; + const photoId = ( + await this.uploadPhoto("/Users/pike/Desktop/test/test.jpg", false) + )["id"]; if (!photoId) return; // get photo link