You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			381 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			381 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
| import {
 | |
|   getV2OpenGroupRoomByRoomId,
 | |
|   OpenGroupV2Room,
 | |
|   saveV2OpenGroupRoom,
 | |
| } from '../../data/opengroups';
 | |
| import { OpenGroupV2CompactPollRequest, parseMessages } from './ApiUtil';
 | |
| import { parseStatusCodeFromOnionRequest } from './OpenGroupAPIV2Parser';
 | |
| import _ from 'lodash';
 | |
| import { sendViaOnionToNonSnode } from '../../session/onions/onionSend';
 | |
| import { OpenGroupMessageV2 } from './OpenGroupMessageV2';
 | |
| import { downloadPreviewOpenGroupV2, getMemberCount } from './OpenGroupAPIV2';
 | |
| import { getAuthToken } from './ApiAuth';
 | |
| import { DURATION } from '../../session/constants';
 | |
| 
 | |
| const COMPACT_POLL_ENDPOINT = 'compact_poll';
 | |
| 
 | |
| export const compactFetchEverything = async (
 | |
|   serverUrl: string,
 | |
|   rooms: Set<string>,
 | |
|   abortSignal: AbortSignal
 | |
| ): Promise<Array<ParsedRoomCompactPollResults> | null> => {
 | |
|   // fetch all we need
 | |
|   const compactPollRequest = await getCompactPollRequest(serverUrl, rooms);
 | |
|   if (!compactPollRequest) {
 | |
|     window?.log?.info('Nothing found to be fetched. returning');
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const result = await sendOpenGroupV2RequestCompactPoll(compactPollRequest, abortSignal);
 | |
|   return result ? result : null;
 | |
| };
 | |
| 
 | |
| export const getAllBase64AvatarForRooms = async (
 | |
|   serverUrl: string,
 | |
|   rooms: Set<string>,
 | |
|   abortSignal: AbortSignal
 | |
| ): Promise<Array<ParsedBase64Avatar> | null> => {
 | |
|   // fetch all we need
 | |
|   const allValidRoomInfos = await getAllValidRoomInfos(serverUrl, rooms);
 | |
|   if (!allValidRoomInfos?.length) {
 | |
|     window?.log?.info('getAllBase64AvatarForRooms: no valid roominfos got.');
 | |
|     return null;
 | |
|   }
 | |
|   if (abortSignal.aborted) {
 | |
|     window?.log?.info('preview download aborted, returning null');
 | |
|     return null;
 | |
|   }
 | |
|   // Currently this call will not abort if AbortSignal is aborted,
 | |
|   // but the call will return null.
 | |
|   const validPreviewBase64 = _.compact(
 | |
|     await Promise.all(
 | |
|       allValidRoomInfos.map(async room => {
 | |
|         try {
 | |
|           const base64 = await downloadPreviewOpenGroupV2(room);
 | |
|           if (base64) {
 | |
|             return {
 | |
|               roomId: room.roomId,
 | |
|               base64,
 | |
|             };
 | |
|           }
 | |
|         } catch (e) {
 | |
|           window?.log?.warn('getPreview failed for room', room);
 | |
|         }
 | |
|         return null;
 | |
|       })
 | |
|     )
 | |
|   );
 | |
| 
 | |
|   if (abortSignal.aborted) {
 | |
|     window?.log?.info('preview download aborted, returning null');
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   return validPreviewBase64 ? validPreviewBase64 : null;
 | |
| };
 | |
| 
 | |
| export const getAllMemberCount = async (
 | |
|   serverUrl: string,
 | |
|   rooms: Set<string>,
 | |
|   abortSignal: AbortSignal
 | |
| ): Promise<Array<ParsedMemberCount> | null> => {
 | |
|   // fetch all we need
 | |
|   const allValidRoomInfos = await getAllValidRoomInfos(serverUrl, rooms);
 | |
|   if (!allValidRoomInfos?.length) {
 | |
|     window?.log?.info('getAllMemberCount: no valid roominfos got.');
 | |
|     return null;
 | |
|   }
 | |
|   if (abortSignal.aborted) {
 | |
|     window?.log?.info('memberCount aborted, returning null');
 | |
|     return null;
 | |
|   }
 | |
|   // Currently this call will not abort if AbortSignal is aborted,
 | |
|   // but the call will return null.
 | |
|   const validMemberCount = _.compact(
 | |
|     await Promise.all(
 | |
|       allValidRoomInfos.map(async room => {
 | |
|         try {
 | |
|           const memberCount = await getMemberCount(room);
 | |
|           if (memberCount !== undefined) {
 | |
|             return {
 | |
|               roomId: room.roomId,
 | |
|               memberCount,
 | |
|             };
 | |
|           }
 | |
|         } catch (e) {
 | |
|           window?.log?.warn('getPreview failed for room', room);
 | |
|         }
 | |
|         return null;
 | |
|       })
 | |
|     )
 | |
|   );
 | |
| 
 | |
|   if (abortSignal.aborted) {
 | |
|     window?.log?.info('getMemberCount aborted, returning null');
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   return validMemberCount ? validMemberCount : null;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * This function fetches the valid roomInfos from the database.
 | |
|  * It also makes sure that the pubkey for all those rooms are the same, or returns null.
 | |
|  */
 | |
| const getAllValidRoomInfos = async (
 | |
|   serverUrl: string,
 | |
|   rooms: Set<string>
 | |
| ): Promise<Array<OpenGroupV2Room> | null> => {
 | |
|   const allServerPubKeys: Array<string> = [];
 | |
| 
 | |
|   // fetch all the roomInfos for the specified rooms.
 | |
|   // those invalid (like, not found in db) are excluded (with lodash compact)
 | |
|   const validRoomInfos = _.compact(
 | |
|     await Promise.all(
 | |
|       [...rooms].map(async roomId => {
 | |
|         try {
 | |
|           const fetchedInfo = await getV2OpenGroupRoomByRoomId({
 | |
|             serverUrl,
 | |
|             roomId,
 | |
|           });
 | |
|           if (!fetchedInfo) {
 | |
|             window?.log?.warn('Could not find this room getMessages');
 | |
|             return null;
 | |
|           }
 | |
|           allServerPubKeys.push(fetchedInfo.serverPublicKey);
 | |
|           const tokenInProgress = await getAuthToken({ serverUrl, roomId });
 | |
| 
 | |
|           return { ...fetchedInfo, token: tokenInProgress || undefined };
 | |
|         } catch (e) {
 | |
|           window?.log?.warn('failed to fetch roominfos for room', roomId);
 | |
|           return null;
 | |
|         }
 | |
|       })
 | |
|     )
 | |
|   );
 | |
|   if (!validRoomInfos?.length) {
 | |
|     return null;
 | |
|   }
 | |
|   // double check that all those server pubkeys are the same
 | |
|   let firstPubkey: string;
 | |
|   if (allServerPubKeys?.length) {
 | |
|     firstPubkey = allServerPubKeys[0];
 | |
|     const allMatch = allServerPubKeys.every(p => p === firstPubkey);
 | |
|     if (!allMatch) {
 | |
|       window?.log?.warn('All pubkeys do not match:', allServerPubKeys);
 | |
|       return null;
 | |
|     }
 | |
|   } else {
 | |
|     window?.log?.warn('No pubkeys found:', allServerPubKeys);
 | |
|     return null;
 | |
|   }
 | |
|   return validRoomInfos;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * This return body to be used to do the compactPoll
 | |
|  */
 | |
| const getCompactPollRequest = async (
 | |
|   serverUrl: string,
 | |
|   rooms: Set<string>
 | |
| ): Promise<null | OpenGroupV2CompactPollRequest> => {
 | |
|   const allValidRoomInfos = await getAllValidRoomInfos(serverUrl, rooms);
 | |
|   if (!allValidRoomInfos?.length) {
 | |
|     window?.log?.info('compactPoll: no valid roominfos got.');
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const roomsRequestInfos = _.compact(
 | |
|     allValidRoomInfos.map(validRoomInfos => {
 | |
|       try {
 | |
|         const {
 | |
|           lastMessageFetchedServerID,
 | |
|           lastFetchTimestamp,
 | |
|           lastMessageDeletedServerID,
 | |
|           token,
 | |
|           roomId,
 | |
|         } = validRoomInfos;
 | |
|         const roomRequestContent: Record<string, any> = {
 | |
|           room_id: roomId,
 | |
|           auth_token: token || '',
 | |
|         };
 | |
|         roomRequestContent.from_deletion_server_id = lastMessageDeletedServerID;
 | |
|         if (Date.now() - (lastFetchTimestamp || 0) <= DURATION.DAYS * 14) {
 | |
|           roomRequestContent.from_message_server_id = lastMessageFetchedServerID;
 | |
|         } else {
 | |
|           window?.log?.info(
 | |
|             `We've been away for a long time... Only fetching last messages of room '${roomId}'`
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         return roomRequestContent;
 | |
|       } catch (e) {
 | |
|         window?.log?.warn('failed to fetch roominfos for room', validRoomInfos.roomId);
 | |
|         return null;
 | |
|       }
 | |
|     })
 | |
|   );
 | |
|   if (!roomsRequestInfos?.length) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const body = JSON.stringify({
 | |
|     requests: roomsRequestInfos,
 | |
|   });
 | |
| 
 | |
|   // getAllValidRoomInfos return null if the room have not all the same serverPublicKey.
 | |
|   // so being here, we know this is the case
 | |
|   return {
 | |
|     body,
 | |
|     server: serverUrl,
 | |
|     serverPubKey: allValidRoomInfos[0].serverPublicKey,
 | |
|     endpoint: COMPACT_POLL_ENDPOINT,
 | |
|   };
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * This call is separate as a lot of the logic is custom (statusCode handled separately, etc)
 | |
|  */
 | |
| async function sendOpenGroupV2RequestCompactPoll(
 | |
|   request: OpenGroupV2CompactPollRequest,
 | |
|   abortSignal: AbortSignal
 | |
| ): Promise<Array<ParsedRoomCompactPollResults> | null> {
 | |
|   const { server: serverUrl, endpoint, body, serverPubKey } = request;
 | |
|   // this will throw if the url is not valid
 | |
|   const builtUrl = new URL(`${serverUrl}/${endpoint}`);
 | |
| 
 | |
|   const res = await sendViaOnionToNonSnode(
 | |
|     serverPubKey,
 | |
|     builtUrl,
 | |
|     {
 | |
|       method: 'POST',
 | |
|       body,
 | |
|     },
 | |
|     {},
 | |
|     abortSignal
 | |
|   );
 | |
| 
 | |
|   const statusCode = parseStatusCodeFromOnionRequest(res);
 | |
|   if (!statusCode) {
 | |
|     window?.log?.warn('sendOpenGroupV2RequestCompactPoll Got unknown status code; res:', res);
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const results = await parseCompactPollResults(res, serverUrl);
 | |
|   if (!results) {
 | |
|     window?.log?.info('got empty compactPollResults');
 | |
|     return null;
 | |
|   }
 | |
|   // get all roomIds which needs a refreshed token
 | |
|   const roomWithTokensToRefresh = results.filter(ret => ret.statusCode === 401).map(r => r.roomId);
 | |
| 
 | |
|   // this holds only the poll results which are valid
 | |
|   const roomPollValidResults = results.filter(ret => ret.statusCode === 200);
 | |
| 
 | |
|   if (roomWithTokensToRefresh?.length) {
 | |
|     window.log.info('We got those rooms to refresh the token with:', roomWithTokensToRefresh);
 | |
|     await Promise.all(
 | |
|       roomWithTokensToRefresh.map(async roomId => {
 | |
|         const roomDetails = await getV2OpenGroupRoomByRoomId({
 | |
|           serverUrl,
 | |
|           roomId,
 | |
|         });
 | |
|         if (!roomDetails) {
 | |
|           return;
 | |
|         }
 | |
|         roomDetails.token = undefined;
 | |
|         // we might need to retry doing the request here, but how to make sure we don't retry indefinetely?
 | |
|         await saveV2OpenGroupRoom(roomDetails);
 | |
|         // we should not await for that. We have a only one at a time logic on a per room basis
 | |
|         await getAuthToken({ serverUrl, roomId });
 | |
|       })
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   return roomPollValidResults;
 | |
| }
 | |
| 
 | |
| export type ParsedDeletions = Array<{ id: number; deleted_message_id: number }>;
 | |
| 
 | |
| type StatusCodeType = {
 | |
|   statusCode: number;
 | |
| };
 | |
| 
 | |
| export type ParsedRoomCompactPollResults = StatusCodeType & {
 | |
|   roomId: string;
 | |
|   deletions: ParsedDeletions;
 | |
|   messages: Array<OpenGroupMessageV2>;
 | |
|   moderators: Array<string>;
 | |
| };
 | |
| 
 | |
| export type ParsedBase64Avatar = {
 | |
|   roomId: string;
 | |
|   base64: string;
 | |
| };
 | |
| 
 | |
| export type ParsedMemberCount = {
 | |
|   roomId: string;
 | |
|   memberCount: number;
 | |
| };
 | |
| 
 | |
| const parseCompactPollResult = async (
 | |
|   singleRoomResult: any,
 | |
|   _serverUrl: string
 | |
| ): Promise<ParsedRoomCompactPollResults | null> => {
 | |
|   const {
 | |
|     room_id,
 | |
|     deletions: rawDeletions,
 | |
|     messages: rawMessages,
 | |
|     moderators: rawMods,
 | |
|     status_code: rawStatusCode,
 | |
|   } = singleRoomResult;
 | |
| 
 | |
|   if (
 | |
|     !room_id ||
 | |
|     rawDeletions === undefined ||
 | |
|     rawMessages === undefined ||
 | |
|     rawMods === undefined ||
 | |
|     !rawStatusCode
 | |
|   ) {
 | |
|     window?.log?.warn('Invalid compactPoll result', singleRoomResult);
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const validMessages = await parseMessages(rawMessages);
 | |
| 
 | |
|   const moderators = rawMods.sort() as Array<string>;
 | |
|   const deletions = rawDeletions as ParsedDeletions;
 | |
|   const statusCode = rawStatusCode as number;
 | |
| 
 | |
|   return {
 | |
|     roomId: room_id,
 | |
|     deletions,
 | |
|     messages: validMessages,
 | |
|     moderators,
 | |
|     statusCode,
 | |
|   };
 | |
| };
 | |
| 
 | |
| const parseCompactPollResults = async (
 | |
|   res: any,
 | |
|   serverUrl: string
 | |
| ): Promise<Array<ParsedRoomCompactPollResults> | null> => {
 | |
|   if (!res || !res.result || !res.result.results || !res.result.results.length) {
 | |
|     return null;
 | |
|   }
 | |
|   const arrayOfResults = res.result.results as Array<any>;
 | |
| 
 | |
|   const parsedResults: Array<ParsedRoomCompactPollResults> = _.compact(
 | |
|     await Promise.all(
 | |
|       arrayOfResults.map(async m => {
 | |
|         return parseCompactPollResult(m, serverUrl);
 | |
|       })
 | |
|     )
 | |
|   );
 | |
| 
 | |
|   if (!parsedResults || !parsedResults.length) {
 | |
|     return null;
 | |
|   }
 | |
|   return parsedResults;
 | |
| };
 |