Skip to content

Commit

Permalink
refactor(formatters): ⚡ Add thread & friend list formatters WIP
Browse files Browse the repository at this point in the history
In this commit, a lot has been disabled, commented or deleted. However, thread & friend list are functioning. I hope this won't be a disaster. Mentioning #3
  • Loading branch information
makara-filip committed May 22, 2021
1 parent 40bc001 commit 46a0cb3
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 103 deletions.
10 changes: 5 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Jar from './lib/jar';
import cheerio from 'cheerio';
import Api from './lib/api';
import { Response } from 'got';
import { UserInfo } from './lib/types/users';
import { UserID, UserInfo } from './lib/types/users';
import { ThreadInfo } from './lib/types/threads';
import { formatSingleFriend, formatSingleThread } from './lib/formatting/listFormatters';

Expand Down Expand Up @@ -207,7 +207,7 @@ function buildAPI(globalOptions: ApiOptions, html: string, jar: Jar) {

// find these lists from the html response
api.friendList = findFriendList(html);
api.threadList = findThreadList(html);
api.threadList = findThreadList(html, parseInt(userID));

return { ctx, defaultFuncs, api };
}
Expand Down Expand Up @@ -384,10 +384,10 @@ function findFriendList(html: string): UserInfo[] {
// parse object from JSON: (we don't where a JSON object ends)
const parsed = utils.json5parseTillEnd(html.substring(res.index + res[0].length));
// parse loaded friend objects to ts-messenger-api's more user-friendly objects:
return parsed.map(formatSingleFriend);
return parsed.map(formatSingleFriend).filter(Boolean); // only the truthy ones
}

function findThreadList(html: string): ThreadInfo[] {
function findThreadList(html: string, userId: UserID): ThreadInfo[] {
// Facebook is perfect. Why? It sends the thread list twice in a single html file.
// And what is even better than perfect? The 1st list contains 10 threads & the 2nd contains 20.

Expand All @@ -402,5 +402,5 @@ function findThreadList(html: string): ThreadInfo[] {
.map(res => utils.json5parseTillEnd(html.substring(res.index + res[0].length))) // first, parse all the JSON data
.reduce((prev, curr) => (prev.length > curr.length ? prev : curr)); // choose the one list with the most elements
// parse loaded thread objects to ts-messenger-api's more user-friendly objects:
return originalThreadList.map(formatSingleThread);
return originalThreadList.map((thread: unknown) => formatSingleThread(thread, userId));
}
23 changes: 11 additions & 12 deletions lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ import {
import { parseDelta } from './formatting/incomingMessageFormatters';
import { Presence, Typ, IncomingMessageType } from './types/incomingMessages';
import { UserID, UserInfo } from './types/users';
import { formatUserInfoDict } from './formatting/userInfoFormatters';
// import { formatUserInfoDict } from './formatting/userInfoFormatters';
import * as utils from './utils';
import * as formatters from './formatters';
import mqtt from 'mqtt';
import websocket from 'websocket-stream';
import FormData from 'form-data';
import { ThreadHistory, ThreadID, ThreadInfo } from './types/threads';
import {
ThreadHistory,
ThreadID,
ThreadInfo,
ThreadInfo_IS_NOT_BEING_USED_BUT_IS_THERE_FOR_FUTURE
} from './types/threads';
import { getAttachmentID, UploadGeneralAttachmentResponse } from './types/upload-attachment-response';
import { EventEmitter } from 'events';
import * as nodeEmoji from 'node-emoji';
Expand Down Expand Up @@ -573,7 +578,8 @@ export default class Api {
if (resData.error) {
throw resData;
}
return formatUserInfoDict(resData.payload.profiles);
// return formatUserInfoDict(resData.payload.profiles);
throw new Error('Not implemented NI54');
});
}

Expand Down Expand Up @@ -716,19 +722,12 @@ export default class Api {
/** Returns all available information about all user's friends as an array of user objects.
* @category Users */
async getFriendsList(): Promise<UserInfo[]> {
return await this._defaultFuncs
.postFormData('https://www.facebook.com/chat/user_info_all', this.ctx.jar, {}, { viewer: this.ctx.userID })
.then(utils.parseAndCheckLogin(this.ctx, this._defaultFuncs))
.then((resData: any) => {
if (!resData) throw { error: 'getFriendsList returned empty object.' };
if (resData.error) throw resData;
return Object.values(formatUserInfoDict(resData.payload));
});
return this.friendList;
}

/** Returns all available information about a thread with `threadId`.
* @category Threads */
async getThreadInfo(threadId: ThreadID): Promise<ThreadInfo> {
async getThreadInfo(threadId: ThreadID): Promise<ThreadInfo_IS_NOT_BEING_USED_BUT_IS_THERE_FOR_FUTURE> {
const form = {
queries: JSON.stringify({
o0: {
Expand Down
4 changes: 2 additions & 2 deletions lib/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { IncomingEventType } from './types/incomingMessages';
import { HistoryMessage, HistoryMessageType } from './types/threadHistory';
import { ThreadInfo } from './types/threads';
import { ThreadInfo, ThreadInfo_IS_NOT_BEING_USED_BUT_IS_THERE_FOR_FUTURE } from './types/threads';
import { UserID } from './types/users';

export function formatMessagesGraphQLResponse(data: any): HistoryMessage[] {
Expand Down Expand Up @@ -405,7 +405,7 @@ function formatExtensibleAttachment(attachment: any) {
}
}

export function formatThreadInfo(data: any): ThreadInfo {
export function formatThreadInfo(data: any): ThreadInfo_IS_NOT_BEING_USED_BUT_IS_THERE_FOR_FUTURE {
// formatting GraphQL response
const messageThread = data.o0.data.message_thread;
if (!messageThread)
Expand Down
81 changes: 73 additions & 8 deletions lib/formatting/listFormatters.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,80 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */

import { ThreadInfo } from '../types/threads';
import { UserInfo } from '../types/users';
import { ThreadInfo, ThreadParticipant } from '../types/threads';
import { UserID, UserInfo } from '../types/users';

export function formatSingleFriend(originalFriend: any): UserInfo {
// TODO: CRITICAL: finish this
return originalFriend;
export function formatSingleFriend(originalFriend: any): UserInfo | null {
const original = originalFriend?.node?.sts_info?.direct_nav_result;
if (!original) return null;
// single friend can be also FB event or site:
if (original.type.toUpperCase() !== 'FRIEND') return null;

return {
userId: parseInt(original.ent_id),
fullName: original.title,
profilePictureUrlSmall: original.img_url,
profileUrl: original.link_url
};
}

export function formatSingleThread(originalThread: any, currentUserId: UserID): ThreadInfo {
const original = originalThread.node;
if (!original)
throw new Error(
`There was an unknown response. Contact the dev team about this (error code F-22). Data: ${JSON.stringify(
originalThread
)}`
);

const isGroup = original.thread_type.toLowerCase() === 'group';
const isOneToOne = original.thread_type.toLowerCase() === 'one_to_one';
if (!isOneToOne && !isGroup)
throw new Error(
`There was an unknown thread type. Contact the dev team about this (error code F-23). Data: ${JSON.stringify(
originalThread
)}`
);

const common = {
isGroup,
lastUpdated: parseInt(original.updated_time),
participants: formatParticipants(original.all_participants?.edges),
nicknames: formatParticipantCustomisations(original.customization_info?.participant_customizations)
};

if (isGroup)
return {
...common,
threadId: parseInt(original.thread_key?.thread_fbid),
threadName: original.name,
imageUrl: original.image?.uri || original.image?.url
};

const otherParticipant = common.participants.find(p => p.userId != currentUserId);
return {
...common,
threadId: otherParticipant?.userId as UserID,
threadName: otherParticipant?.fullName as string,
imageUrl: otherParticipant?.profilePictureUrlSmall as string
};
}

function formatParticipantCustomisations(array: any[]) {
if (!array) return {};
const nicknames: Record<UserID, string> = {};
for (const obj of array) nicknames[parseInt(obj.participant_id)] = obj.nickname;
return nicknames;
}

export function formatSingleThread(originalThread: any): ThreadInfo {
// TODO: CRITICAL: finish this
return originalThread;
function formatParticipants(array: any[]): ThreadParticipant[] {
if (!array) return [];
return array
.map(one => one.node.messaging_actor)
.map(one => ({
userId: parseInt(one.id),
fullName: one.name,
shortName: one.short_name,
profilePictureUrlSmall: one.profile_picture?.uri || one.profile_picture?.url
}));
}
86 changes: 43 additions & 43 deletions lib/formatting/userInfoFormatters.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,49 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { UserGender, UserID, UserInfo } from '../types/users';
// /* eslint-disable @typescript-eslint/no-explicit-any */
// /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
// import { UserGender, UserID, UserInfo } from '../types/users';

export function formatUserInfoDict(userProfiles: any): Record<UserID, UserInfo> {
const finalObject: Record<UserID, UserInfo> = {};
for (const key of Object.keys(userProfiles)) {
const parsedKey = parseInt(key);
if (!parsedKey)
throw new Error(
`There was an unknown response. Contact the dev team about this (error code 935520). User profiles: ${JSON.stringify(
userProfiles
)}`
);
// filter out some (possibly) hidden accounts - they nevertheless contain no useful information
if (userProfiles[key].id == 0) continue;
// export function formatUserInfoDict(userProfiles: any): Record<UserID, UserInfo> {
// const finalObject: Record<UserID, UserInfo> = {};
// for (const key of Object.keys(userProfiles)) {
// const parsedKey = parseInt(key);
// if (!parsedKey)
// throw new Error(
// `There was an unknown response. Contact the dev team about this (error code 935520). User profiles: ${JSON.stringify(
// userProfiles
// )}`
// );
// // filter out some (possibly) hidden accounts - they nevertheless contain no useful information
// if (userProfiles[key].id == 0) continue;

finalObject[parsedKey] = {
id: parseInt(userProfiles[key].id),
// finalObject[parsedKey] = {
// id: parseInt(userProfiles[key].id),

fullName: userProfiles[key].name,
firstName: userProfiles[key].firstName,
alternateName: userProfiles[key].alternateName || undefined,
gender: getGender(userProfiles[key].gender),
// fullName: userProfiles[key].name,
// firstName: userProfiles[key].firstName,
// alternateName: userProfiles[key].alternateName || undefined,
// gender: getGender(userProfiles[key].gender),

isFriend: userProfiles[key].is_friend,
isBlocked: userProfiles[key].is_blocked,
// isFriend: userProfiles[key].is_friend,
// isBlocked: userProfiles[key].is_blocked,

thumbSrc: userProfiles[key].thumbSrc,
profileUrl: userProfiles[key].uri,
type: userProfiles[key].type,
vanity: userProfiles[key].vanity || undefined
};
}
return finalObject;
}
// thumbSrc: userProfiles[key].thumbSrc,
// profileUrl: userProfiles[key].uri,
// type: userProfiles[key].type,
// vanity: userProfiles[key].vanity || undefined
// };
// }
// return finalObject;
// }

function getGender(gender: any): UserGender {
switch (gender) {
case 1:
return UserGender.Female;
case 2:
return UserGender.Male;
case 6:
return UserGender.Other;
default:
return UserGender.Unknown;
}
}
// function getGender(gender: any): UserGender {
// switch (gender) {
// case 1:
// return UserGender.Female;
// case 2:
// return UserGender.Male;
// case 6:
// return UserGender.Other;
// default:
// return UserGender.Unknown;
// }
// }
32 changes: 27 additions & 5 deletions lib/types/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { UserID } from './users';

export type ThreadID = string | number;

export interface ThreadInfo {
export interface ThreadInfo_IS_NOT_BEING_USED_BUT_IS_THERE_FOR_FUTURE {
/** the thread identifier */
threadId: ThreadID;
/** name of a group chat (`null` if one-to-one chat) */
Expand Down Expand Up @@ -75,10 +75,32 @@ export type ThreadColor = string | null;
export type ThreadEmoji = {
emoji: string;
} | null;
// export type ThreadNickname = {
// userid: UserID;
// nickname: string;
// };

export interface ThreadInfo {
/** the thread identifier */
threadId: ThreadID;
/** name of a group chat (or other person's name if one-to-one chat) */
threadName: string;

/** self-explaining... :-) */
isGroup: boolean;
/** timestamp (in seconds) of last update in a thread (last message sent) */
lastUpdated: number;
/** list of thread's participants and their further information */
participants: ThreadParticipant[];
/** dictionary of user's nicknames by their IDs*/
nicknames: Record<UserID, string>;
/** url to the thread image */
imageUrl: string;
}

export interface ThreadParticipant {
userId: UserID;
fullName: string;
shortName: string;
/** url of a person's profile picture in 40x40 px resolution */
profilePictureUrlSmall: string;
}

/** The thread history consisting of last messages.
* Get an instance from `API.getThreadHistory()` method. */
Expand Down
Loading

0 comments on commit 46a0cb3

Please sign in to comment.