operand-meta-sdk 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +25 -0
- package/.prettierrc +4 -0
- package/README.md +435 -0
- package/dist/jest.config.d.ts +3 -0
- package/dist/jest.config.js +14 -0
- package/dist/jest.config.js.map +1 -0
- package/dist/src/__test__/mocks/index.d.ts +2 -0
- package/dist/src/__test__/mocks/index.js +6 -0
- package/dist/src/__test__/mocks/index.js.map +1 -0
- package/dist/src/error/operand-error.d.ts +8 -0
- package/dist/src/error/operand-error.js +196 -0
- package/dist/src/error/operand-error.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +22 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interfaces/ing-publish.d.ts +53 -0
- package/dist/src/interfaces/ing-publish.js +3 -0
- package/dist/src/interfaces/ing-publish.js.map +1 -0
- package/dist/src/interfaces/meta-auth.d.ts +14 -0
- package/dist/src/interfaces/meta-auth.js +3 -0
- package/dist/src/interfaces/meta-auth.js.map +1 -0
- package/dist/src/interfaces/meta-mkt.d.ts +4 -0
- package/dist/src/interfaces/meta-mkt.js +3 -0
- package/dist/src/interfaces/meta-mkt.js.map +1 -0
- package/dist/src/interfaces/meta-response.d.ts +285 -0
- package/dist/src/interfaces/meta-response.js +3 -0
- package/dist/src/interfaces/meta-response.js.map +1 -0
- package/dist/src/interfaces/meta.d.ts +6 -0
- package/dist/src/interfaces/meta.js +3 -0
- package/dist/src/interfaces/meta.js.map +1 -0
- package/dist/src/interfaces/page-publish.d.ts +66 -0
- package/dist/src/interfaces/page-publish.js +3 -0
- package/dist/src/interfaces/page-publish.js.map +1 -0
- package/dist/src/modules/auth/meta-auth.d.ts +35 -0
- package/dist/src/modules/auth/meta-auth.js +131 -0
- package/dist/src/modules/auth/meta-auth.js.map +1 -0
- package/dist/src/modules/auth/meta-auth.spec.d.ts +1 -0
- package/dist/src/modules/auth/meta-auth.spec.js +76 -0
- package/dist/src/modules/auth/meta-auth.spec.js.map +1 -0
- package/dist/src/modules/comments/ing-comments.d.ts +7 -0
- package/dist/src/modules/comments/ing-comments.js +27 -0
- package/dist/src/modules/comments/ing-comments.js.map +1 -0
- package/dist/src/modules/comments/page-comments.d.ts +6 -0
- package/dist/src/modules/comments/page-comments.js +18 -0
- package/dist/src/modules/comments/page-comments.js.map +1 -0
- package/dist/src/modules/insights/ing-insights.d.ts +42 -0
- package/dist/src/modules/insights/ing-insights.js +150 -0
- package/dist/src/modules/insights/ing-insights.js.map +1 -0
- package/dist/src/modules/insights/mkt-insights.d.ts +10 -0
- package/dist/src/modules/insights/mkt-insights.js +46 -0
- package/dist/src/modules/insights/mkt-insights.js.map +1 -0
- package/dist/src/modules/insights/page-insights.d.ts +34 -0
- package/dist/src/modules/insights/page-insights.js +146 -0
- package/dist/src/modules/insights/page-insights.js.map +1 -0
- package/dist/src/modules/meta-ing.d.ts +5 -0
- package/dist/src/modules/meta-ing.js +11 -0
- package/dist/src/modules/meta-ing.js.map +1 -0
- package/dist/src/modules/meta-mkt.d.ts +5 -0
- package/dist/src/modules/meta-mkt.js +11 -0
- package/dist/src/modules/meta-mkt.js.map +1 -0
- package/dist/src/modules/meta-page.d.ts +5 -0
- package/dist/src/modules/meta-page.js +11 -0
- package/dist/src/modules/meta-page.js.map +1 -0
- package/dist/src/modules/meta.d.ts +10 -0
- package/dist/src/modules/meta.js +23 -0
- package/dist/src/modules/meta.js.map +1 -0
- package/dist/src/modules/publish/ing-publish.d.ts +39 -0
- package/dist/src/modules/publish/ing-publish.js +464 -0
- package/dist/src/modules/publish/ing-publish.js.map +1 -0
- package/dist/src/modules/publish/page-publish.d.ts +51 -0
- package/dist/src/modules/publish/page-publish.js +560 -0
- package/dist/src/modules/publish/page-publish.js.map +1 -0
- package/dist/src/modules/publish/page-publish.spec.d.ts +1 -0
- package/dist/src/modules/publish/page-publish.spec.js +280 -0
- package/dist/src/modules/publish/page-publish.spec.js.map +1 -0
- package/dist/src/modules/utils/meta-utils.d.ts +8 -0
- package/dist/src/modules/utils/meta-utils.js +28 -0
- package/dist/src/modules/utils/meta-utils.js.map +1 -0
- package/dist/src/utils/api.d.ts +8 -0
- package/dist/src/utils/api.js +36 -0
- package/dist/src/utils/api.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/jest.config.ts +198 -0
- package/package.json +39 -0
- package/src/__test__/mocks/image.jpeg +0 -0
- package/src/__test__/mocks/index.ts +5 -0
- package/src/__test__/mocks/video-to-post.mp4 +0 -0
- package/src/__test__/mocks/video-to-stories.mp4 +0 -0
- package/src/error/operand-error.ts +217 -0
- package/src/index.ts +21 -0
- package/src/interfaces/ing-publish.ts +58 -0
- package/src/interfaces/meta-auth.ts +52 -0
- package/src/interfaces/meta-mkt.ts +5 -0
- package/src/interfaces/meta-response.ts +319 -0
- package/src/interfaces/meta.ts +7 -0
- package/src/interfaces/page-publish.ts +72 -0
- package/src/modules/auth/meta-auth.spec.ts +93 -0
- package/src/modules/auth/meta-auth.ts +227 -0
- package/src/modules/comments/ing-comments.ts +38 -0
- package/src/modules/comments/page-comments.ts +20 -0
- package/src/modules/insights/ing-insights.ts +275 -0
- package/src/modules/insights/mkt-insights.ts +68 -0
- package/src/modules/insights/page-insights.ts +267 -0
- package/src/modules/meta-ing.ts +8 -0
- package/src/modules/meta-mkt.ts +8 -0
- package/src/modules/meta-page.ts +8 -0
- package/src/modules/meta.ts +31 -0
- package/src/modules/publish/ing-publish.ts +754 -0
- package/src/modules/publish/page-publish.spec.ts +386 -0
- package/src/modules/publish/page-publish.ts +881 -0
- package/src/modules/utils/meta-utils.ts +37 -0
- package/src/utils/api.ts +45 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConstructorPage,
|
|
3
|
+
CreatePost,
|
|
4
|
+
CreateReels,
|
|
5
|
+
CreateStories,
|
|
6
|
+
IPagePublish,
|
|
7
|
+
SetThumbnailToReels,
|
|
8
|
+
VideoMediaItem,
|
|
9
|
+
} from "../../interfaces/page-publish";
|
|
10
|
+
import {
|
|
11
|
+
DeletePagePostResponse,
|
|
12
|
+
GetPagePostsResponse,
|
|
13
|
+
SaveMediaStorageResponse,
|
|
14
|
+
CreatePagePostResponse,
|
|
15
|
+
UpdatePagePostResponse,
|
|
16
|
+
PagePost,
|
|
17
|
+
CreatePhotoStoriesResponse,
|
|
18
|
+
CreateStartVideoUploadResponse,
|
|
19
|
+
CreateFinishVideoUploadResponse,
|
|
20
|
+
GetStoriesPageResponse,
|
|
21
|
+
} from "../../interfaces/meta-response";
|
|
22
|
+
import * as FileType from "file-type";
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as FormData from "form-data";
|
|
25
|
+
import { isAfter, isBefore, addMinutes, addMonths } from "date-fns";
|
|
26
|
+
import { OperandError } from "../../error/operand-error";
|
|
27
|
+
import * as ffmpeg from "fluent-ffmpeg";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
import { MetaUtils } from "../utils/meta-utils";
|
|
30
|
+
|
|
31
|
+
export class PagePublish extends MetaUtils implements IPagePublish {
|
|
32
|
+
protected readonly pageId: string;
|
|
33
|
+
|
|
34
|
+
constructor({ pageAccessToken, pageId, apiVersion }: ConstructorPage) {
|
|
35
|
+
super({ pageAccessToken, apiVersion });
|
|
36
|
+
this.pageId = pageId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected isValidUrl(url: string): boolean {
|
|
40
|
+
const regexUrl =
|
|
41
|
+
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
|
|
42
|
+
|
|
43
|
+
return regexUrl.test(url);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fileTypesPermitted(file: "video" | "photo", type: string): boolean {
|
|
47
|
+
return file === "photo"
|
|
48
|
+
? ["jpeg", "jpg", "png", "gif", "bmp", "tiff", "webp"].includes(type)
|
|
49
|
+
: ["mp4", "avi", "flv", "mkv", "mov", "mpeg", "wmv"].includes(type);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static async verifyVideoSpec(
|
|
53
|
+
videoSource: Buffer | string,
|
|
54
|
+
ext: string,
|
|
55
|
+
to: "reels" | "post" | "stories",
|
|
56
|
+
): Promise<{
|
|
57
|
+
success: boolean;
|
|
58
|
+
warn?: {
|
|
59
|
+
videoChecks: {
|
|
60
|
+
chromaSubsampling: boolean;
|
|
61
|
+
fixedFrameRate: boolean;
|
|
62
|
+
progressive: boolean;
|
|
63
|
+
};
|
|
64
|
+
audioChecks: {
|
|
65
|
+
bitrate: boolean;
|
|
66
|
+
channels: boolean;
|
|
67
|
+
sampleRate: boolean;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
error?: string;
|
|
71
|
+
}> {
|
|
72
|
+
let tempFilePath = "";
|
|
73
|
+
|
|
74
|
+
const isBuffer = videoSource instanceof Buffer;
|
|
75
|
+
|
|
76
|
+
if (isBuffer) {
|
|
77
|
+
tempFilePath = path.resolve(
|
|
78
|
+
__dirname,
|
|
79
|
+
"..",
|
|
80
|
+
"..",
|
|
81
|
+
"temp",
|
|
82
|
+
`${Date.now()}.${ext}`,
|
|
83
|
+
);
|
|
84
|
+
await fs.promises.writeFile(tempFilePath, videoSource);
|
|
85
|
+
} else {
|
|
86
|
+
tempFilePath = videoSource as string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const videoSpecResponse = await new Promise<{
|
|
90
|
+
success: boolean;
|
|
91
|
+
warn?: {
|
|
92
|
+
videoChecks: {
|
|
93
|
+
chromaSubsampling: boolean;
|
|
94
|
+
fixedFrameRate: boolean;
|
|
95
|
+
progressive: boolean;
|
|
96
|
+
};
|
|
97
|
+
audioChecks: {
|
|
98
|
+
bitrate: boolean;
|
|
99
|
+
channels: boolean;
|
|
100
|
+
sampleRate: boolean;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
error?: string;
|
|
104
|
+
}>((resolve, reject) => {
|
|
105
|
+
ffmpeg.ffprobe(tempFilePath, (err, metadata) => {
|
|
106
|
+
if (err) {
|
|
107
|
+
reject(err);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const stream = metadata.streams.find((s) => s.width && s.height);
|
|
111
|
+
|
|
112
|
+
const width = stream?.width;
|
|
113
|
+
const height = stream?.height;
|
|
114
|
+
const fpsString = stream?.avg_frame_rate;
|
|
115
|
+
const fps = fpsString ? eval(fpsString) : 0;
|
|
116
|
+
const ratio = width / height;
|
|
117
|
+
const size = metadata.format.size;
|
|
118
|
+
const duration = metadata.format.duration;
|
|
119
|
+
|
|
120
|
+
const isReelsOrStories = ["reels", "stories"].includes(to);
|
|
121
|
+
|
|
122
|
+
const validResolution = isReelsOrStories
|
|
123
|
+
? width && height && width >= 540 && height >= 960
|
|
124
|
+
: width && height && width <= 1920 && height <= 1080;
|
|
125
|
+
|
|
126
|
+
if (!validResolution) {
|
|
127
|
+
resolve({
|
|
128
|
+
success: false,
|
|
129
|
+
error: `Invalid resolution. The video must have at least ${
|
|
130
|
+
isReelsOrStories
|
|
131
|
+
? "The video must have at least 540x960"
|
|
132
|
+
: "The video must have a maximum of 1920x1080"
|
|
133
|
+
}. The current resolution is ${width}x${height}.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const validFps = fps && fps >= 24 && fps <= 60;
|
|
138
|
+
|
|
139
|
+
if (!validFps) {
|
|
140
|
+
resolve({
|
|
141
|
+
success: false,
|
|
142
|
+
error: `Invalid fps. The video must have between 24 and 60 fps. The current fps is ${fps}.`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const validRatio = isReelsOrStories ? ratio <= 0.5625 : true;
|
|
147
|
+
|
|
148
|
+
if (!validRatio) {
|
|
149
|
+
resolve({
|
|
150
|
+
success: false,
|
|
151
|
+
error: `Invalid ratio. The video must have a ratio of 0.56 or less. The current ratio is ${ratio}.`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const validSize = isReelsOrStories
|
|
156
|
+
? true
|
|
157
|
+
: size < 1024 * 1024 * 1024 * 10;
|
|
158
|
+
|
|
159
|
+
if (!validSize) {
|
|
160
|
+
resolve({
|
|
161
|
+
success: false,
|
|
162
|
+
error: `Invalid size. The video must be a maximum of 10 gigabytes.`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const validDuration = isReelsOrStories
|
|
167
|
+
? duration <= 60
|
|
168
|
+
: duration <= 14400;
|
|
169
|
+
|
|
170
|
+
if (!validDuration) {
|
|
171
|
+
resolve({
|
|
172
|
+
success: false,
|
|
173
|
+
error: `Invalid duration. The video must be a maximum of ${isReelsOrStories ? "60 seconds" : "240 minutes"}.`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const videoStream = metadata.streams.find(
|
|
178
|
+
(s) => s.codec_type === "video",
|
|
179
|
+
);
|
|
180
|
+
const audioStream = metadata.streams.find(
|
|
181
|
+
(s) => s.codec_type === "audio",
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const validVideoCodec = ["h264", "hevc", "vp9", "av1"].includes(
|
|
185
|
+
videoStream?.codec_name ?? "",
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const validAudioCodec = ["aac"].includes(audioStream?.codec_name ?? "");
|
|
189
|
+
|
|
190
|
+
if (!validVideoCodec) {
|
|
191
|
+
resolve({
|
|
192
|
+
success: false,
|
|
193
|
+
error: `Invalid codecs. The video must have the following video codecs: h264, hevc, vp9, av1`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!validAudioCodec) {
|
|
198
|
+
resolve({
|
|
199
|
+
success: false,
|
|
200
|
+
error: `Invalid codecs. The video must be have aac audio codec`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const videoChecks = {
|
|
205
|
+
chromaSubsampling: videoStream?.pix_fmt === "yuv420p",
|
|
206
|
+
fixedFrameRate:
|
|
207
|
+
videoStream?.avg_frame_rate === videoStream?.r_frame_rate,
|
|
208
|
+
progressive:
|
|
209
|
+
videoStream?.field_order === "progressive" ||
|
|
210
|
+
videoStream.progressive === "1" ||
|
|
211
|
+
videoStream.progressive === true ||
|
|
212
|
+
(!videoStream.interlaced &&
|
|
213
|
+
!videoStream.top_field_first &&
|
|
214
|
+
!videoStream.bottom_field_first) ||
|
|
215
|
+
videoStream.interlaced === "0" ||
|
|
216
|
+
videoStream.interlaced === false,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const audioChecks = {
|
|
220
|
+
bitrate: isReelsOrStories
|
|
221
|
+
? parseInt(audioStream?.bit_rate ?? "0") >= 128000
|
|
222
|
+
: parseInt(audioStream?.bit_rate ?? "0") <= 4000000,
|
|
223
|
+
channels: audioStream?.channels === 2,
|
|
224
|
+
sampleRate: audioStream?.sample_rate === 48000,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
resolve({
|
|
228
|
+
success: true,
|
|
229
|
+
warn: { videoChecks, audioChecks },
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (isBuffer) await fs.promises.unlink(tempFilePath);
|
|
235
|
+
|
|
236
|
+
return videoSpecResponse;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async verifyPhotoSize(
|
|
240
|
+
value: string | Buffer,
|
|
241
|
+
isBuffer: boolean,
|
|
242
|
+
): Promise<boolean> {
|
|
243
|
+
if (isBuffer) {
|
|
244
|
+
return (value as Buffer).length / 1024 / 1024 <= 8;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const status = await fs.promises.stat(value as string);
|
|
248
|
+
return status.size / 1024 / 1024 <= 8;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private validatePublishDate(datePublish: Date): boolean {
|
|
252
|
+
const now = new Date();
|
|
253
|
+
|
|
254
|
+
const tenMinutesFromNow = addMinutes(now, 10);
|
|
255
|
+
const sixMonthsFromNow = addMonths(now, 6);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
isBefore(datePublish, tenMinutesFromNow) ||
|
|
259
|
+
isAfter(datePublish, sixMonthsFromNow)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private async savePhotoInMetaStorageByUrl(url: string): Promise<string> {
|
|
264
|
+
const response = await fetch(url);
|
|
265
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
266
|
+
|
|
267
|
+
const fileType = await FileType.fromBuffer(arrayBuffer);
|
|
268
|
+
|
|
269
|
+
if (!fileType) {
|
|
270
|
+
throw new OperandError({
|
|
271
|
+
message: "Impossible to get the file type of file.",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!this.fileTypesPermitted("photo", fileType.ext)) {
|
|
276
|
+
throw new OperandError({
|
|
277
|
+
message:
|
|
278
|
+
"This file type is not permitted. File types permitted: jpeg, jpg, png, gif, bmp, tiff, webp.",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!(await this.verifyPhotoSize(Buffer.from(arrayBuffer), true))) {
|
|
283
|
+
throw new OperandError({
|
|
284
|
+
message: "The photo must be less or equal to 4MB.",
|
|
285
|
+
code: 37,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
await this.api.post<SaveMediaStorageResponse>(`/me/photos`, {
|
|
291
|
+
published: false,
|
|
292
|
+
access_token: this.pageAccessToken,
|
|
293
|
+
url,
|
|
294
|
+
})
|
|
295
|
+
).data.id;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async savePhotoInMetaStorageByPath(path: string): Promise<string> {
|
|
299
|
+
const fileType = await FileType.fromFile(path);
|
|
300
|
+
|
|
301
|
+
if (!fileType) {
|
|
302
|
+
throw new OperandError({
|
|
303
|
+
message: "Impossible to get the file type of file.",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!this.fileTypesPermitted("photo", fileType.ext)) {
|
|
308
|
+
throw new OperandError({
|
|
309
|
+
message:
|
|
310
|
+
"This file type is not permitted. File types permitted: jpeg, jpg, png, gif, bmp, tiff, webp.",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!(await this.verifyPhotoSize(path, false))) {
|
|
315
|
+
throw new OperandError({
|
|
316
|
+
message: "The photo must be less or equal to 4MB.",
|
|
317
|
+
code: 37,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const fileStream = fs.createReadStream(path);
|
|
322
|
+
|
|
323
|
+
const formData = new FormData();
|
|
324
|
+
formData.append("published", "false");
|
|
325
|
+
formData.append("access_token", this.pageAccessToken);
|
|
326
|
+
formData.append("source", fileStream);
|
|
327
|
+
|
|
328
|
+
const response = await this.api.post<SaveMediaStorageResponse>(
|
|
329
|
+
`/me/photos`,
|
|
330
|
+
formData,
|
|
331
|
+
{
|
|
332
|
+
headers: {
|
|
333
|
+
...formData.getHeaders(),
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return response.data.id;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private async saveVideoInMetaStorageMomentaryByUrl(
|
|
342
|
+
video: string,
|
|
343
|
+
to: "stories" | "reels",
|
|
344
|
+
): Promise<string> {
|
|
345
|
+
const response = await fetch(video);
|
|
346
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
347
|
+
const fileType = await FileType.fromBuffer(arrayBuffer);
|
|
348
|
+
|
|
349
|
+
if (!fileType) {
|
|
350
|
+
throw new OperandError({
|
|
351
|
+
message: "Impossible to get the file type of file.",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!this.fileTypesPermitted("video", fileType.ext)) {
|
|
356
|
+
throw new OperandError({
|
|
357
|
+
message:
|
|
358
|
+
"This file type is not permitted. File types permitted: mp4, avi, flv, mkv, mov, mpeg, wmv.",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// await PagePublish.verifyVideoSpec(
|
|
363
|
+
// Buffer.from(arrayBuffer),
|
|
364
|
+
// fileType.ext,
|
|
365
|
+
// to,
|
|
366
|
+
// );
|
|
367
|
+
|
|
368
|
+
const {
|
|
369
|
+
data: { upload_url, video_id },
|
|
370
|
+
} = await this.api.post<CreateStartVideoUploadResponse>(
|
|
371
|
+
`${this.pageId}/${to === "stories" ? "video_stories" : "video_reels"}`,
|
|
372
|
+
{
|
|
373
|
+
upload_phase: "start",
|
|
374
|
+
access_token: this.pageAccessToken,
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
await fetch(upload_url, {
|
|
379
|
+
method: "POST",
|
|
380
|
+
body: JSON.stringify({
|
|
381
|
+
file_url: video,
|
|
382
|
+
}),
|
|
383
|
+
headers: {
|
|
384
|
+
Authorization: `OAuth ${this.pageAccessToken}`,
|
|
385
|
+
file_url: video,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return video_id;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async saveVideoInMetaStorageMomentaryByPath(
|
|
393
|
+
video: string,
|
|
394
|
+
to: "stories" | "reels",
|
|
395
|
+
): Promise<string> {
|
|
396
|
+
const arrayBuffer = await fs.promises.readFile(video);
|
|
397
|
+
|
|
398
|
+
const fileType = await FileType.fromBuffer(arrayBuffer);
|
|
399
|
+
|
|
400
|
+
if (!fileType) {
|
|
401
|
+
throw new OperandError({
|
|
402
|
+
message: "Impossible to get the file type of file.",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!this.fileTypesPermitted("video", fileType.ext)) {
|
|
407
|
+
throw new OperandError({
|
|
408
|
+
message:
|
|
409
|
+
"This file type is not permitted. File types permitted: mp4, avi, flv, mkv, mov, mpeg, wmv.",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// await PagePublish.verifyVideoSpec(
|
|
414
|
+
// await fs.promises.readFile(video),
|
|
415
|
+
// fileType.ext,
|
|
416
|
+
// to,
|
|
417
|
+
// );
|
|
418
|
+
|
|
419
|
+
const {
|
|
420
|
+
data: { upload_url, video_id },
|
|
421
|
+
} = await this.api.post<CreateStartVideoUploadResponse>(
|
|
422
|
+
`${this.pageId}/${to === "stories" ? "video_stories" : "video_reels"}`,
|
|
423
|
+
{
|
|
424
|
+
upload_phase: "start",
|
|
425
|
+
access_token: this.pageAccessToken,
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
await fetch(upload_url, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
body: arrayBuffer,
|
|
432
|
+
headers: {
|
|
433
|
+
Authorization: `OAuth ${this.pageAccessToken}`,
|
|
434
|
+
offset: "0",
|
|
435
|
+
file_size: String(arrayBuffer.byteLength),
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
return video_id;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
public async getAllPosts(): Promise<PagePost[]> {
|
|
443
|
+
const values: PagePost[] = [];
|
|
444
|
+
|
|
445
|
+
let nextUrl = `${this.api.getUri()}/${this.pageId}/feed?access_token=${this.pageAccessToken}`;
|
|
446
|
+
|
|
447
|
+
while (nextUrl) {
|
|
448
|
+
const response = (await (
|
|
449
|
+
await fetch(nextUrl)
|
|
450
|
+
).json()) as GetPagePostsResponse;
|
|
451
|
+
|
|
452
|
+
values.push(...response.data);
|
|
453
|
+
|
|
454
|
+
const isValidNext =
|
|
455
|
+
response.paging?.next && this.isValidUrl(response.paging.next);
|
|
456
|
+
|
|
457
|
+
if (!isValidNext) {
|
|
458
|
+
nextUrl = "";
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
nextUrl = response.paging.next;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return values;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
public getPostUrlById(postId: string): string {
|
|
469
|
+
return `https://www.facebook.com/${this.pageId}/posts/${postId}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private async uploadPhotos(
|
|
473
|
+
photos: Array<{ source: string; value: string }>,
|
|
474
|
+
): Promise<string[]> {
|
|
475
|
+
try {
|
|
476
|
+
const medias = [];
|
|
477
|
+
|
|
478
|
+
for (const photo of photos) {
|
|
479
|
+
medias.push(
|
|
480
|
+
photo.source === "url"
|
|
481
|
+
? await this.savePhotoInMetaStorageByUrl(photo.value)
|
|
482
|
+
: await this.savePhotoInMetaStorageByPath(photo.value),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return medias;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw new OperandError({
|
|
489
|
+
message: "Error when upload photos",
|
|
490
|
+
error,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private async createTextPost(
|
|
496
|
+
message: string,
|
|
497
|
+
publishNow: boolean,
|
|
498
|
+
datePublish?: Date,
|
|
499
|
+
): Promise<string> {
|
|
500
|
+
try {
|
|
501
|
+
const newPost = (
|
|
502
|
+
await this.api.post<CreatePagePostResponse>(`/${this.pageId}/feed`, {
|
|
503
|
+
access_token: this.pageAccessToken,
|
|
504
|
+
message,
|
|
505
|
+
...(!publishNow && {
|
|
506
|
+
scheduled_publish_time: Math.floor(
|
|
507
|
+
new Date(datePublish).getTime() / 1000,
|
|
508
|
+
),
|
|
509
|
+
published: publishNow,
|
|
510
|
+
}),
|
|
511
|
+
})
|
|
512
|
+
).data;
|
|
513
|
+
|
|
514
|
+
return newPost.post_id || newPost.id;
|
|
515
|
+
} catch (error) {
|
|
516
|
+
throw new OperandError({
|
|
517
|
+
message: "Error creating post text ",
|
|
518
|
+
error,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private async createPhotosPost(
|
|
524
|
+
message: string,
|
|
525
|
+
mediaIds: string[],
|
|
526
|
+
publishNow: boolean,
|
|
527
|
+
datePublish?: Date,
|
|
528
|
+
): Promise<string> {
|
|
529
|
+
try {
|
|
530
|
+
const newPost = (
|
|
531
|
+
await this.api.post<CreatePagePostResponse>(`/${this.pageId}/feed`, {
|
|
532
|
+
access_token: this.pageAccessToken,
|
|
533
|
+
message,
|
|
534
|
+
...(!publishNow && {
|
|
535
|
+
scheduled_publish_time: Math.floor(
|
|
536
|
+
new Date(datePublish).getTime() / 1000,
|
|
537
|
+
),
|
|
538
|
+
published: publishNow,
|
|
539
|
+
}),
|
|
540
|
+
attached_media: mediaIds.map((id) => ({
|
|
541
|
+
media_fbid: id,
|
|
542
|
+
})),
|
|
543
|
+
})
|
|
544
|
+
).data;
|
|
545
|
+
|
|
546
|
+
return newPost.post_id || newPost.id;
|
|
547
|
+
} catch (error) {
|
|
548
|
+
throw new OperandError({
|
|
549
|
+
message: "Error when create photos post",
|
|
550
|
+
error,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private async createVideoPost(
|
|
556
|
+
video: VideoMediaItem,
|
|
557
|
+
message: string,
|
|
558
|
+
publishNow: boolean,
|
|
559
|
+
datePublish?: Date,
|
|
560
|
+
): Promise<string> {
|
|
561
|
+
try {
|
|
562
|
+
let arrayBuffer: ArrayBuffer;
|
|
563
|
+
|
|
564
|
+
if (video.source === "url") {
|
|
565
|
+
const response = await fetch(video.value);
|
|
566
|
+
arrayBuffer = await response.arrayBuffer();
|
|
567
|
+
} else {
|
|
568
|
+
arrayBuffer = await fs.promises.readFile(video.value);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const fileType = await FileType.fromBuffer(arrayBuffer);
|
|
572
|
+
|
|
573
|
+
if (!fileType) {
|
|
574
|
+
throw new OperandError({
|
|
575
|
+
message: "Impossible to get the file type of file.",
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!this.fileTypesPermitted("video", fileType.ext)) {
|
|
580
|
+
throw new OperandError({
|
|
581
|
+
message:
|
|
582
|
+
"This file type is not permitted. File types permitted: mp4, avi, flv, mkv, mov, mpeg, wmv.",
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// await PagePublish.verifyVideoSpec(
|
|
587
|
+
// Buffer.from(arrayBuffer),
|
|
588
|
+
// fileType.ext,
|
|
589
|
+
// "post",
|
|
590
|
+
// );
|
|
591
|
+
|
|
592
|
+
const formData = new FormData();
|
|
593
|
+
formData.append("description", message || "");
|
|
594
|
+
formData.append(
|
|
595
|
+
video.source === "url" ? "file_url" : "source",
|
|
596
|
+
video.source === "url" ? video.value : fs.createReadStream(video.value),
|
|
597
|
+
);
|
|
598
|
+
formData.append("access_token", this.pageAccessToken);
|
|
599
|
+
|
|
600
|
+
if (!publishNow) {
|
|
601
|
+
formData.append("published", "false");
|
|
602
|
+
formData.append(
|
|
603
|
+
"scheduled_publish_time",
|
|
604
|
+
Math.floor(new Date(datePublish).getTime() / 1000),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const { data } = await this.apiVideo.post<SaveMediaStorageResponse>(
|
|
609
|
+
`/${this.pageId}/videos`,
|
|
610
|
+
formData,
|
|
611
|
+
{
|
|
612
|
+
headers: {
|
|
613
|
+
...formData.getHeaders(),
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
return data.id;
|
|
619
|
+
} catch (error) {
|
|
620
|
+
throw new OperandError({
|
|
621
|
+
message: "Error when create video post",
|
|
622
|
+
error,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
public async createPost(post: CreatePost): Promise<string> {
|
|
628
|
+
await this.createTempFolder();
|
|
629
|
+
|
|
630
|
+
const { message, publishNow, datePublish, mediaType } = post;
|
|
631
|
+
|
|
632
|
+
if (!publishNow && !datePublish) {
|
|
633
|
+
throw new OperandError({
|
|
634
|
+
message:
|
|
635
|
+
"You must provide the datePublish if you don't want to publish now.",
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (!publishNow && this.validatePublishDate(new Date(datePublish))) {
|
|
640
|
+
throw new OperandError({
|
|
641
|
+
message:
|
|
642
|
+
"The datePublish must be between 10 minutes from now and 6 months from now.",
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!mediaType && message) {
|
|
647
|
+
return this.createTextPost(message, publishNow, datePublish);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (mediaType === "photo") {
|
|
651
|
+
const mediaIds = await this.uploadPhotos(post.photos);
|
|
652
|
+
return this.createPhotosPost(message, mediaIds, publishNow, datePublish);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (mediaType === "video") {
|
|
656
|
+
return this.createVideoPost(post.video, message, publishNow, datePublish);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
throw new OperandError({
|
|
660
|
+
message: "Invalid parameters.",
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
public async updatePost(postId: string, message: string): Promise<boolean> {
|
|
665
|
+
try {
|
|
666
|
+
return (
|
|
667
|
+
await this.api.post<UpdatePagePostResponse>(`/${postId}`, {
|
|
668
|
+
access_token: this.pageAccessToken,
|
|
669
|
+
message,
|
|
670
|
+
})
|
|
671
|
+
).data.success;
|
|
672
|
+
} catch (error) {
|
|
673
|
+
throw new OperandError({
|
|
674
|
+
message: "Error in update post",
|
|
675
|
+
error,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
public async deletePost(postId: string): Promise<boolean> {
|
|
681
|
+
try {
|
|
682
|
+
return (
|
|
683
|
+
await this.api.delete<DeletePagePostResponse>(`/${postId}`, {
|
|
684
|
+
params: {
|
|
685
|
+
access_token: this.pageAccessToken,
|
|
686
|
+
},
|
|
687
|
+
})
|
|
688
|
+
).data.success;
|
|
689
|
+
} catch (error) {
|
|
690
|
+
throw new OperandError({
|
|
691
|
+
message: "Error in delete post",
|
|
692
|
+
error,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private async createPhotoStory(story: CreateStories): Promise<string> {
|
|
698
|
+
try {
|
|
699
|
+
const photoId =
|
|
700
|
+
story.source === "url"
|
|
701
|
+
? await this.savePhotoInMetaStorageByUrl(story.url)
|
|
702
|
+
: await this.savePhotoInMetaStorageByPath(story.path);
|
|
703
|
+
|
|
704
|
+
const response = await this.api.post<CreatePhotoStoriesResponse>(
|
|
705
|
+
`/${this.pageId}/photo_stories`,
|
|
706
|
+
{
|
|
707
|
+
access_token: this.pageAccessToken,
|
|
708
|
+
photo_id: photoId,
|
|
709
|
+
},
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
return response.data.post_id;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
throw new OperandError({
|
|
715
|
+
message: "Error in create photo story",
|
|
716
|
+
error,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private async createVideoStory(story: CreateStories): Promise<string> {
|
|
722
|
+
try {
|
|
723
|
+
const videoId =
|
|
724
|
+
story.source === "url"
|
|
725
|
+
? await this.saveVideoInMetaStorageMomentaryByUrl(
|
|
726
|
+
story.url,
|
|
727
|
+
"stories",
|
|
728
|
+
)
|
|
729
|
+
: await this.saveVideoInMetaStorageMomentaryByPath(
|
|
730
|
+
story.path,
|
|
731
|
+
"stories",
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
const {
|
|
735
|
+
data: { post_id },
|
|
736
|
+
} = await this.api.post<CreateFinishVideoUploadResponse>(
|
|
737
|
+
`${this.pageId}/video_stories`,
|
|
738
|
+
{
|
|
739
|
+
upload_phase: "finish",
|
|
740
|
+
video_id: videoId,
|
|
741
|
+
access_token: this.pageAccessToken,
|
|
742
|
+
},
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
return post_id;
|
|
746
|
+
} catch (error) {
|
|
747
|
+
throw new OperandError({
|
|
748
|
+
message: "Error in create video story",
|
|
749
|
+
error,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
public async createStories(story: CreateStories): Promise<string> {
|
|
755
|
+
await this.createTempFolder();
|
|
756
|
+
|
|
757
|
+
if (story.mediaType === "photo") {
|
|
758
|
+
return this.createPhotoStory(story);
|
|
759
|
+
} else if (story.mediaType === "video") {
|
|
760
|
+
return this.createVideoStory(story);
|
|
761
|
+
} else {
|
|
762
|
+
throw new Error("Unsupported media type.");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
public async createReels(
|
|
767
|
+
reel: CreateReels,
|
|
768
|
+
): Promise<{ postId: string; videoId: string }> {
|
|
769
|
+
try {
|
|
770
|
+
await this.createTempFolder();
|
|
771
|
+
|
|
772
|
+
const videoId =
|
|
773
|
+
reel.source === "url"
|
|
774
|
+
? await this.saveVideoInMetaStorageMomentaryByUrl(reel.url, "reels")
|
|
775
|
+
: await this.saveVideoInMetaStorageMomentaryByPath(
|
|
776
|
+
reel.path,
|
|
777
|
+
"reels",
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
const {
|
|
781
|
+
data: { post_id },
|
|
782
|
+
} = await this.api.post<CreateFinishVideoUploadResponse>(
|
|
783
|
+
`${this.pageId}/video_reels`,
|
|
784
|
+
{},
|
|
785
|
+
{
|
|
786
|
+
params: {
|
|
787
|
+
video_id: videoId,
|
|
788
|
+
upload_phase: "finish",
|
|
789
|
+
video_state: "PUBLISHED",
|
|
790
|
+
description: reel.description,
|
|
791
|
+
title: reel.title,
|
|
792
|
+
access_token: this.pageAccessToken,
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
return {
|
|
798
|
+
postId: post_id,
|
|
799
|
+
videoId,
|
|
800
|
+
};
|
|
801
|
+
} catch (error) {
|
|
802
|
+
throw new OperandError({
|
|
803
|
+
message: "Error in create reels",
|
|
804
|
+
error,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
public async setThumbnailToReels({
|
|
810
|
+
source,
|
|
811
|
+
value,
|
|
812
|
+
videoId,
|
|
813
|
+
}: SetThumbnailToReels) {
|
|
814
|
+
try {
|
|
815
|
+
let buffer: Buffer;
|
|
816
|
+
|
|
817
|
+
if (source === "path") {
|
|
818
|
+
buffer = await fs.promises.readFile(value);
|
|
819
|
+
} else {
|
|
820
|
+
const response = await fetch(value);
|
|
821
|
+
buffer = Buffer.from(await response.arrayBuffer());
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const { ext } = await FileType.fromBuffer(buffer);
|
|
825
|
+
|
|
826
|
+
const pathFile = path.resolve(
|
|
827
|
+
__dirname,
|
|
828
|
+
"..",
|
|
829
|
+
"..",
|
|
830
|
+
"temp",
|
|
831
|
+
`${new Date().getTime()}.${ext}`,
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
await fs.promises.writeFile(pathFile, buffer);
|
|
835
|
+
|
|
836
|
+
const fileStream = fs.createReadStream(pathFile);
|
|
837
|
+
|
|
838
|
+
const formData = new FormData();
|
|
839
|
+
|
|
840
|
+
formData.append("source", fileStream);
|
|
841
|
+
|
|
842
|
+
await this.api.post(`${videoId}/thumbnails`, formData, {
|
|
843
|
+
params: {
|
|
844
|
+
access_token: this.pageAccessToken,
|
|
845
|
+
is_preferred: true,
|
|
846
|
+
},
|
|
847
|
+
headers: {
|
|
848
|
+
...formData.getHeaders(),
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
await fs.promises.unlink(pathFile);
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
success: true,
|
|
856
|
+
};
|
|
857
|
+
} catch (error) {
|
|
858
|
+
throw new OperandError({
|
|
859
|
+
message: "Error in set thumbnail to reels",
|
|
860
|
+
error,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
public async getLinkStories(id: string) {
|
|
866
|
+
try {
|
|
867
|
+
return (
|
|
868
|
+
await this.api.get<GetStoriesPageResponse>(`${this.pageId}/stories`, {
|
|
869
|
+
params: {
|
|
870
|
+
access_token: this.pageAccessToken,
|
|
871
|
+
},
|
|
872
|
+
})
|
|
873
|
+
).data.data.find((data) => data.post_id === id)?.url;
|
|
874
|
+
} catch (error) {
|
|
875
|
+
throw new OperandError({
|
|
876
|
+
message: "Error in get link stories",
|
|
877
|
+
error,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|