bb-fca 2.0.1 → 2.0.3

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.
@@ -0,0 +1,151 @@
1
+ import { randomUUID } from 'crypto';
2
+
3
+ /**
4
+ * @ChoruOfficial
5
+ * @description A module for changing the Facebook account display name via AccountsCenter.
6
+ * The name change process is two-step:
7
+ * 1. Preview: validate and preview the new name
8
+ * 2. Confirm: apply the name change mutation
9
+ * @param {Object} defaultFuncs The default functions provided by the API wrapper.
10
+ * @param {Object} api The full API object.
11
+ * @param {Object} ctx The context object containing the user's session state.
12
+ * @returns {Function} An async function that changes the account's display name.
13
+ */
14
+ export default function(defaultFuncs: any, api: any, ctx: any) {
15
+ const ACCOUNTS_CENTER_URL = 'https://accountscenter.facebook.com/api/graphql/';
16
+
17
+ /**
18
+ * Builds the common form fields required by AccountsCenter GraphQL API.
19
+ * @private
20
+ */
21
+ function buildBaseForm() {
22
+ return {
23
+ av: ctx.userID,
24
+ __user: ctx.userID,
25
+ __a: '1',
26
+ fb_dtsg: ctx.fb_dtsg,
27
+ jazoest: ctx.jazoest,
28
+ lsd: ctx.lsd,
29
+ fb_api_caller_class: 'RelayModern',
30
+ server_timestamps: 'true',
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Step 1 — Preview the new name to validate it and get a confirmation token.
36
+ * Calls `FXIMIdentityNameEditorAuthenticPreviewDialogQuery`.
37
+ * @private
38
+ * @param {string} firstName
39
+ * @param {string} lastName
40
+ * @param {string} [middleName='']
41
+ */
42
+ async function previewName(firstName: string, lastName: string, middleName = '') {
43
+ const variables = {
44
+ first_name: firstName,
45
+ last_name: lastName,
46
+ middle_name: middleName,
47
+ platform: 'FACEBOOK',
48
+ scale: 1,
49
+ };
50
+ const form = {
51
+ ...buildBaseForm(),
52
+ fb_api_req_friendly_name: 'FXIMIdentityNameEditorAuthenticPreviewDialogQuery',
53
+ variables: JSON.stringify(variables),
54
+ doc_id: '26302360049350115',
55
+ };
56
+ const res = await defaultFuncs.post(
57
+ ACCOUNTS_CENTER_URL,
58
+ ctx.jar,
59
+ form,
60
+ {
61
+ headers: {
62
+ 'x-fb-friendly-name': 'FXIMIdentityNameEditorAuthenticPreviewDialogQuery',
63
+ 'x-fb-lsd': ctx.lsd,
64
+ 'x-asbd-id': '359341',
65
+ origin: 'https://accountscenter.facebook.com',
66
+ referer: `https://accountscenter.facebook.com/profiles/${ctx.userID}/name`,
67
+ },
68
+ },
69
+ );
70
+ if (res.data.errors) throw new Error(JSON.stringify(res.data.errors));
71
+ return res.data;
72
+ }
73
+
74
+ /**
75
+ * Step 2 — Confirm / apply the name change mutation.
76
+ * Calls `useFXIMUpdateNameMutation`.
77
+ * @private
78
+ * @param {string} firstName
79
+ * @param {string} lastName
80
+ * @param {string} [middleName='']
81
+ */
82
+ async function confirmName(firstName: string, lastName: string, middleName = '') {
83
+ const fullName = [firstName, middleName, lastName]
84
+ .filter(Boolean)
85
+ .join(' ');
86
+
87
+ const variables = {
88
+ client_mutation_id: randomUUID(),
89
+ family_device_id: 'device_id_fetch_datr',
90
+ identity_ids: [ctx.userID],
91
+ full_name: fullName,
92
+ first_name: firstName,
93
+ middle_name: middleName,
94
+ last_name: lastName,
95
+ interface: 'FB_WEB',
96
+ };
97
+ const form = {
98
+ ...buildBaseForm(),
99
+ fb_api_req_friendly_name: 'useFXIMUpdateNameMutation',
100
+ variables: JSON.stringify(variables),
101
+ doc_id: '9538143859625836',
102
+ };
103
+ const res = await defaultFuncs.post(
104
+ ACCOUNTS_CENTER_URL,
105
+ ctx.jar,
106
+ form,
107
+ {
108
+ headers: {
109
+ 'x-fb-friendly-name': 'useFXIMUpdateNameMutation',
110
+ 'x-fb-lsd': ctx.lsd,
111
+ 'x-asbd-id': '359341',
112
+ origin: 'https://accountscenter.facebook.com',
113
+ referer: `https://accountscenter.facebook.com/profiles/${ctx.userID}/name`,
114
+ },
115
+ },
116
+ );
117
+ if (res.data.errors) throw new Error(JSON.stringify(res.data.errors));
118
+ return res.data;
119
+ }
120
+
121
+ /**
122
+ * Changes the account display name by first previewing, then confirming the mutation.
123
+ * @async
124
+ * @param {string} firstName - New first name (or full first portion).
125
+ * @param {string} lastName - New last name.
126
+ * @param {string} [middleName=''] - Optional middle name.
127
+ * @returns {Promise<{ preview: any; result: any }>} Resolves with preview and mutation result data.
128
+ * @throws {Error} If either the preview or confirm step fails.
129
+ *
130
+ * @example
131
+ * await api.changeName('Van Quy', 'Do');
132
+ * await api.changeName('Van Quy', 'Do', '');
133
+ */
134
+ return async function changeName(
135
+ firstName: string,
136
+ lastName: string,
137
+ middleName = '',
138
+ ): Promise<{ preview: any; result: any }> {
139
+ if (!firstName || !lastName) {
140
+ throw new Error('firstName and lastName are required.');
141
+ }
142
+
143
+ // Step 1: Preview / validate the name
144
+ const preview = await previewName(firstName, lastName, middleName);
145
+
146
+ // Step 2: Apply the name change
147
+ const result = await confirmName(firstName, lastName, middleName);
148
+
149
+ return { preview, result };
150
+ };
151
+ }
@@ -0,0 +1,172 @@
1
+ import * as fs from 'fs';
2
+ import utils = require('../../../utils');
3
+
4
+ /**
5
+ * @description A module for uploading a new profile picture (avatar) to Facebook.
6
+ * Two-step process:
7
+ * 1. Upload: sends the image file via multipart/form-data to /profile/picture/upload/
8
+ * 2. Set: calls ProfileCometProfilePictureSetMutation to apply the photo as avatar with optional caption
9
+ * @param {Object} defaultFuncs The default functions provided by the API wrapper.
10
+ * @param {Object} api The full API object.
11
+ * @param {Object} ctx The context object containing the user's session state.
12
+ * @returns {Function} An async function that uploads an avatar image.
13
+ */
14
+ export default function (defaultFuncs: any, api: any, ctx: any) {
15
+ /**
16
+ * Step 2 — Apply the uploaded photo as avatar via ProfileCometProfilePictureSetMutation.
17
+ * @private
18
+ * @param {string} photoID The ID of the uploaded photo.
19
+ * @param {string} profileID The profile ID.
20
+ * @param {string} caption Optional caption for the avatar.
21
+ */
22
+ async function setProfilePicture(photoID: string, profileID: string, caption = '') {
23
+ const variables = {
24
+ input: {
25
+ attribution_id_v2: `ProfileCometTimelineListViewRoot.react,comet.profile.timeline.list,via_cold_start,${Date.now()},473818,190055527696468,,`,
26
+ caption: caption,
27
+ existing_photo_id: photoID,
28
+ expiration_time: null,
29
+ profile_id: profileID,
30
+ profile_pic_method: 'EXISTING',
31
+ profile_pic_source: 'TIMELINE',
32
+ scaled_crop_rect: { height: 0.56328, width: 1, x: 0, y: 0.21836 },
33
+ skip_cropping: true,
34
+ actor_id: ctx.userID,
35
+ client_mutation_id: Math.floor(Math.random() * 10 + 1).toString(),
36
+ },
37
+ isPage: false,
38
+ isProfile: true,
39
+ sectionToken: 'UNKNOWN',
40
+ collectionToken: 'UNKNOWN',
41
+ scale: 1,
42
+ '__relay_internal__pv__ProfileGeminiIsCoinFlipEnabledrelayprovider': false,
43
+ };
44
+
45
+ const form = {
46
+ av: ctx.userID,
47
+ __user: ctx.userID,
48
+ __a: '1',
49
+ fb_dtsg: ctx.fb_dtsg,
50
+ jazoest: ctx.jazoest,
51
+ lsd: ctx.lsd,
52
+ fb_api_caller_class: 'RelayModern',
53
+ fb_api_req_friendly_name: 'ProfileCometProfilePictureSetMutation',
54
+ variables: JSON.stringify(variables),
55
+ server_timestamps: 'true',
56
+ doc_id: '26101621736123633',
57
+ };
58
+
59
+ const res = await defaultFuncs.post(
60
+ 'https://www.facebook.com/api/graphql/',
61
+ ctx.jar,
62
+ form,
63
+ {},
64
+ );
65
+ if (res.data.errors) throw new Error(JSON.stringify(res.data.errors));
66
+ return res.data;
67
+ }
68
+
69
+ /**
70
+ * Uploads a new profile picture (avatar) for the logged-in user, then sets it as avatar with optional caption.
71
+ * @async
72
+ * @param {string} imagePath The local file path to the image to upload.
73
+ * @param {string} [caption=''] Optional caption for the profile picture post.
74
+ * @param {string} [profileID=ctx.userID] The profile ID to upload the avatar for. Defaults to the logged-in user.
75
+ * @param {Function} [callback] Optional callback function.
76
+ * @returns {Promise<Object>} A promise that resolves with { upload, set } data on success.
77
+ * @throws {Error} If the imagePath is missing, the file does not exist, or any step fails.
78
+ *
79
+ * @example
80
+ * await api.uploadAvatar('/path/to/photo.jpg');
81
+ * await api.uploadAvatar('/path/to/photo.jpg', 'My new avatar!');
82
+ * await api.uploadAvatar('/path/to/photo.jpg', 'Caption', '61588408996667');
83
+ */
84
+ return async function uploadAvatar(
85
+ imagePath: string,
86
+ caption: string = '',
87
+ profileID: string = ctx.userID,
88
+ callback?,
89
+ ): Promise<any> {
90
+ let resolveFunc: Function = function () { };
91
+ let rejectFunc: Function = function () { };
92
+
93
+ const returnPromise = new Promise<any>(function (resolve, reject) {
94
+ resolveFunc = resolve;
95
+ rejectFunc = reject;
96
+ });
97
+
98
+ callback =
99
+ callback ||
100
+ function (err, data) {
101
+ if (err) return rejectFunc(err);
102
+ resolveFunc(data);
103
+ };
104
+
105
+ try {
106
+ if (!imagePath) throw new Error('imagePath is required.');
107
+ if (typeof imagePath !== 'string')
108
+ throw new Error('imagePath must be a string.');
109
+ if (!fs.existsSync(imagePath))
110
+ throw new Error(`File not found: ${imagePath}`);
111
+
112
+ // Step 1: Upload the photo
113
+ const photoStream = fs.createReadStream(imagePath);
114
+
115
+ const url = new URL('https://www.facebook.com/profile/picture/upload/');
116
+ url.searchParams.append('photo_source', '57');
117
+ url.searchParams.append('profile_id', profileID);
118
+ url.searchParams.append('av', ctx.userID);
119
+ url.searchParams.append('__aaid', '0');
120
+ url.searchParams.append('__user', ctx.userID);
121
+ url.searchParams.append('__a', '1');
122
+ url.searchParams.append('fb_dtsg', ctx.fb_dtsg);
123
+ url.searchParams.append('jazoest', ctx.jazoest);
124
+ url.searchParams.append('lsd', ctx.lsd);
125
+
126
+ const uploadResponse = await utils.postFormData(
127
+ url.toString(),
128
+ ctx.jar,
129
+ { file: photoStream },
130
+ ctx.globalOptions,
131
+ ctx,
132
+ );
133
+
134
+ const uploadResult = JSON.parse(
135
+ uploadResponse.body.toString().replace(/^for \(;;\);/, ''),
136
+ );
137
+
138
+ if (uploadResult.error || uploadResult.errors) {
139
+ throw new Error(
140
+ JSON.stringify(uploadResult.error || uploadResult.errors),
141
+ );
142
+ }
143
+
144
+ const photoID =
145
+ uploadResult.payload?.fbid ||
146
+ uploadResult.payload?.photoID ||
147
+ uploadResult.payload?.photo_id ||
148
+ null;
149
+
150
+ if (!photoID) {
151
+ throw new Error('Upload succeeded but could not extract photo ID from response.');
152
+ }
153
+
154
+ // Step 2: Set as profile picture with caption
155
+ const setResult = await setProfilePicture(photoID, profileID, caption);
156
+
157
+ const result = {
158
+ success: true,
159
+ photoID: photoID,
160
+ upload: uploadResult,
161
+ set: setResult,
162
+ };
163
+
164
+ callback(null, result);
165
+ } catch (err) {
166
+ utils.error('uploadAvatar', err);
167
+ callback(err);
168
+ }
169
+
170
+ return returnPromise;
171
+ };
172
+ }
@@ -0,0 +1,167 @@
1
+ import * as fs from 'fs';
2
+ import utils = require('../../../utils');
3
+
4
+ /**
5
+ * @ChoruOfficial
6
+ * @description A module for uploading a new cover photo (ảnh bìa) to Facebook.
7
+ * Two-step process:
8
+ * 1. Upload: sends the image file via multipart/form-data to /profile/cover/comet_upload/
9
+ * 2. Confirm: calls ProfileCometCoverPhotoUpdateMutation to apply the photo as cover
10
+ * @param {Object} defaultFuncs The default functions provided by the API wrapper.
11
+ * @param {Object} api The full API object.
12
+ * @param {Object} ctx The context object containing the user's session state.
13
+ * @returns {Function} An async function that uploads a cover photo.
14
+ */
15
+ export default function (defaultFuncs: any, api: any, ctx: any) {
16
+ /**
17
+ * Step 2 — Confirm the uploaded photo as cover via ProfileCometCoverPhotoUpdateMutation.
18
+ * @private
19
+ * @param {string} photoID The ID of the uploaded photo.
20
+ * @param {string} profileID The profile ID.
21
+ * @param {{ x: number, y: number }} [focus] Optional focus point. Defaults to center {x:0.5, y:0.5}.
22
+ */
23
+ async function setCoverPhoto(
24
+ photoID: string,
25
+ profileID: string,
26
+ focus = { x: 0.5, y: 0.5 },
27
+ ) {
28
+ const variables = {
29
+ input: {
30
+ attribution_id_v2: `ProfileCometAboutTabRoot.react,comet.profile.collection.about,via_cold_start,${Date.now()},670164,,,`,
31
+ cover_photo_id: photoID,
32
+ focus: focus,
33
+ target_user_id: profileID,
34
+ actor_id: ctx.userID,
35
+ client_mutation_id: Math.floor(Math.random() * 10 + 1).toString(),
36
+ },
37
+ scale: 1,
38
+ contextualProfileContext: null,
39
+ };
40
+
41
+ const form = {
42
+ av: ctx.userID,
43
+ __user: ctx.userID,
44
+ __a: '1',
45
+ fb_dtsg: ctx.fb_dtsg,
46
+ jazoest: ctx.jazoest,
47
+ lsd: ctx.lsd,
48
+ fb_api_caller_class: 'RelayModern',
49
+ fb_api_req_friendly_name: 'ProfileCometCoverPhotoUpdateMutation',
50
+ variables: JSON.stringify(variables),
51
+ server_timestamps: 'true',
52
+ doc_id: '31388044007461211',
53
+ };
54
+
55
+ const res = await defaultFuncs.post(
56
+ 'https://www.facebook.com/api/graphql/',
57
+ ctx.jar,
58
+ form,
59
+ {},
60
+ );
61
+ if (res.data.errors) throw new Error(JSON.stringify(res.data.errors));
62
+ return res.data;
63
+ }
64
+
65
+ /**
66
+ * Uploads a new cover photo (ảnh bìa) for the logged-in user, then confirms it as the cover.
67
+ * @async
68
+ * @param {string} imagePath The local file path to the image to upload.
69
+ * @param {string} [profileID=ctx.userID] The profile ID. Defaults to the logged-in user.
70
+ * @param {{ x: number, y: number }} [focus] Optional focus point for cropping. Defaults to {x:0.5, y:0.5}.
71
+ * @param {Function} [callback] Optional callback function.
72
+ * @returns {Promise<Object>} A promise that resolves with { upload, set } data on success.
73
+ * @throws {Error} If the imagePath is missing, the file does not exist, or any step fails.
74
+ *
75
+ * @example
76
+ * await api.uploadCover('/path/to/cover.jpg');
77
+ * await api.uploadCover('/path/to/cover.jpg', '61588408996667');
78
+ * await api.uploadCover('/path/to/cover.jpg', ctx.userID, { x: 0.5, y: 0.3 });
79
+ */
80
+ return async function uploadCover(
81
+ imagePath: string,
82
+ profileID: string = ctx.userID,
83
+ focus = { x: 0.5, y: 0.5 },
84
+ callback?,
85
+ ): Promise<any> {
86
+ let resolveFunc: Function = function () { };
87
+ let rejectFunc: Function = function () { };
88
+
89
+ const returnPromise = new Promise<any>(function (resolve, reject) {
90
+ resolveFunc = resolve;
91
+ rejectFunc = reject;
92
+ });
93
+
94
+ callback =
95
+ callback ||
96
+ function (err, data) {
97
+ if (err) return rejectFunc(err);
98
+ resolveFunc(data);
99
+ };
100
+
101
+ try {
102
+ if (!imagePath) throw new Error('imagePath is required.');
103
+ if (typeof imagePath !== 'string')
104
+ throw new Error('imagePath must be a string.');
105
+ if (!fs.existsSync(imagePath))
106
+ throw new Error(`File not found: ${imagePath}`);
107
+
108
+ // Step 1: Upload the photo
109
+ const photoStream = fs.createReadStream(imagePath);
110
+
111
+ const url = new URL('https://www.facebook.com/profile/cover/comet_upload/');
112
+ url.searchParams.append('profile_id', profileID);
113
+ url.searchParams.append('av', ctx.userID);
114
+ url.searchParams.append('__aaid', '0');
115
+ url.searchParams.append('__user', ctx.userID);
116
+ url.searchParams.append('__a', '1');
117
+ url.searchParams.append('fb_dtsg', ctx.fb_dtsg);
118
+ url.searchParams.append('jazoest', ctx.jazoest);
119
+ url.searchParams.append('lsd', ctx.lsd);
120
+
121
+ const uploadResponse = await utils.postFormData(
122
+ url.toString(),
123
+ ctx.jar,
124
+ { file: photoStream },
125
+ ctx.globalOptions,
126
+ ctx,
127
+ );
128
+
129
+ const uploadResult = JSON.parse(
130
+ uploadResponse.body.toString().replace(/^for \(;;\);/, ''),
131
+ );
132
+
133
+ if (uploadResult.error || uploadResult.errors) {
134
+ throw new Error(
135
+ JSON.stringify(uploadResult.error || uploadResult.errors),
136
+ );
137
+ }
138
+
139
+ const photoID =
140
+ uploadResult.payload?.fbid ||
141
+ uploadResult.payload?.photoID ||
142
+ uploadResult.payload?.photo_id ||
143
+ null;
144
+
145
+ if (!photoID) {
146
+ throw new Error('Upload succeeded but could not extract photo ID from response.');
147
+ }
148
+
149
+ // Step 2: Confirm as cover photo
150
+ const setResult = await setCoverPhoto(photoID, profileID, focus);
151
+
152
+ const result = {
153
+ success: true,
154
+ photoID: photoID,
155
+ upload: uploadResult,
156
+ set: setResult,
157
+ };
158
+
159
+ callback(null, result);
160
+ } catch (err) {
161
+ utils.error('uploadCover', err);
162
+ callback(err);
163
+ }
164
+
165
+ return returnPromise;
166
+ };
167
+ }
@@ -123,6 +123,13 @@ export interface UploadPhotoResult {
123
123
  data: any;
124
124
  }
125
125
 
126
+ export interface ChangeNameResult {
127
+ /** Raw preview response data from AccountsCenter name validation query. */
128
+ preview: any;
129
+ /** Raw mutation result data confirming the name change. */
130
+ result: any;
131
+ }
132
+
126
133
  export interface CurrentFriendInfo {
127
134
  userID: string;
128
135
  name: string;
@@ -637,6 +644,8 @@ export interface API {
637
644
  getCurrentFriends(
638
645
  callback?: Callback<CurrentFriendInfo[]>,
639
646
  ): Promise<CurrentFriendInfo[]>;
647
+ /** Send a friend request to a user by their ID via their profile page. */
648
+ add(userID: UserID, callback?: Callback): Promise<any>;
640
649
  suggest: {
641
650
  list(
642
651
  limit?: number,
@@ -690,6 +699,19 @@ export interface API {
690
699
 
691
700
  // ── Session ────────────────────────────────────────────────────
692
701
 
702
+ /**
703
+ * Changes the Facebook account display name (two-step: preview then confirm).
704
+ * Calls AccountsCenter GraphQL API.
705
+ * @param firstName New first name.
706
+ * @param lastName New last name.
707
+ * @param middleName Optional middle name (default: empty string).
708
+ */
709
+ changeName(
710
+ firstName: string,
711
+ lastName: string,
712
+ middleName?: string,
713
+ ): Promise<ChangeNameResult>;
714
+
693
715
  /** Log out and invalidate the current session. */
694
716
  logout(callback?: (err: any) => void): Promise<void>;
695
717