diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 677c2af5a..113f4763b 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -31,7 +31,11 @@ import { joinOpenGroupV2, parseOpenGroupV2, } from '../../opengroup/opengroupV2/JoinOpenGroupV2'; -import { downloadPreviewOpenGroupV2 } from '../../opengroup/opengroupV2/OpenGroupAPIV2'; +import { + downloadPreviewOpenGroupV2, + getMessages, +} from '../../opengroup/opengroupV2/OpenGroupAPIV2'; +import { compactFetchEverything } from '../../opengroup/opengroupV2/OpenGroupAPIV2CompactPoll'; export enum SectionType { Profile, @@ -187,10 +191,10 @@ export const ActionsPanel = () => { if (parsedRoom) { setTimeout(async () => { await joinOpenGroupV2(parsedRoom); - // const oldMessages = await getMessages({ - // serverUrl: parsedRoom.serverUrl, - // roomId: parsedRoom.roomId, - // }); + const oldMessages = await getMessages({ + serverUrl: parsedRoom.serverUrl, + roomId: parsedRoom.roomId, + }); // const msg = new OpenGroupMessageV2({ // base64EncodedData: 'dffdldfkldf', // sentTimestamp: Date.now(), @@ -203,6 +207,11 @@ export const ActionsPanel = () => { // serverUrl: parsedRoom.serverUrl, // roomId: parsedRoom.roomId, // }); + + const rooms = [ + { serverUrl: 'https://opengroup.bilb.us', roomId: 'main' }, + ]; + await compactFetchEverything(rooms); }, 6000); } }, []); diff --git a/ts/opengroup/opengroupV2/ApiUtil.ts b/ts/opengroup/opengroupV2/ApiUtil.ts index 182fe96af..8b75f23a9 100644 --- a/ts/opengroup/opengroupV2/ApiUtil.ts +++ b/ts/opengroup/opengroupV2/ApiUtil.ts @@ -1,4 +1,5 @@ import _ from 'underscore'; +import { getV2OpenGroupRoomByRoomId } from '../../data/opengroups'; import { getSodium } from '../../session/crypto'; import { PubKey } from '../../session/types'; import { @@ -27,8 +28,13 @@ export type OpenGroupV2Request = { queryParams?: Record; headers?: Record; isAuthRequired: boolean; - // Always `true` under normal circumstances. You might want to disable this when running over Lokinet. - useOnionRouting?: boolean; +}; + +export type OpenGroupV2CompactPollRequest = { + server: string; + endpoint: string; + body: string; + serverPubKey: string; }; export type OpenGroupV2Info = { @@ -88,11 +94,8 @@ export const setCachedModerators = ( }; export const parseMessages = async ( - onionResult: any + rawMessages: Array> ): Promise> => { - const rawMessages = onionResult?.result?.messages as Array< - Record - >; if (!rawMessages) { window.log.info('no new messages'); return []; diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts index b18bf01e1..ccb17463c 100644 --- a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts @@ -33,7 +33,11 @@ import { } from './OpenGroupAPIV2Parser'; import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; -// This function might throw +/** + * This send function is to be used for all non polling stuff + * download and upload of attachments for instance, but most of the logic happens in + * the compact_poll endpoint + */ async function sendOpenGroupV2Request( request: OpenGroupV2Request ): Promise { @@ -46,77 +50,71 @@ async function sendOpenGroupV2Request( // set the headers sent by the caller, and the roomId. const headers = request.headers || {}; headers.Room = request.room; + console.warn(`sending request: ${builtUrl}`); let body = ''; if (request.method !== 'GET') { body = JSON.stringify(request.queryParams); } - // request.useOnionRouting === undefined defaults to true - if (request.useOnionRouting || request.useOnionRouting === undefined) { - const roomDetails = await getV2OpenGroupRoomByRoomId({ - serverUrl: request.server, + const roomDetails = await getV2OpenGroupRoomByRoomId({ + serverUrl: request.server, + roomId: request.room, + }); + if (!roomDetails?.serverPublicKey) { + throw new Error('PublicKey not found for this server.'); + } + // Because auth happens on a per-room basis, we need both to make an authenticated request + if (request.isAuthRequired && request.room) { + // this call will either return the token on the db, + // or the promise currently fetching a new token for that same room + // or fetch from the open group a new token for that room. + const token = await getAuthToken({ roomId: request.room, + serverUrl: request.server, }); - if (!roomDetails?.serverPublicKey) { - throw new Error('PublicKey not found for this server.'); + if (!token) { + window.log.error('Failed to get token for open group v2'); + return null; } - // Because auth happens on a per-room basis, we need both to make an authenticated request - if (request.isAuthRequired && request.room) { - // this call will either return the token on the db, - // or the promise currently fetching a new token for that same room - // or fetch from the open group a new token for that room. - const token = await getAuthToken({ - roomId: request.room, - serverUrl: request.server, - }); - if (!token) { - window.log.error('Failed to get token for open group v2'); - return null; - } - headers.Authorization = token; - const res = await sendViaOnion( - roomDetails.serverPublicKey, - builtUrl, - { - method: request.method, - headers, - body, - }, - { noJson: true } - ); - - const statusCode = parseStatusCodeFromOnionRequest(res); - if (!statusCode) { - window.log.warn( - 'sendOpenGroupV2Request Got unknown status code; res:', - res - ); - return res as object; - } - // A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an - // indication that the token we're using has expired. - // Note that a 403 has a different meaning; it means that - // we provided a valid token but it doesn't have a high enough permission level for the route in question. - if (statusCode === 401) { - 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); - } - return res as object; - } else { - // no need for auth, just do the onion request - const res = await sendViaOnion(roomDetails.serverPublicKey, builtUrl, { + headers.Authorization = token; + const res = await sendViaOnion( + roomDetails.serverPublicKey, + builtUrl, + { method: request.method, headers, body, - }); + }, + { noJson: true } + ); + + const statusCode = parseStatusCodeFromOnionRequest(res); + if (!statusCode) { + window.log.warn( + 'sendOpenGroupV2Request Got unknown status code; res:', + res + ); return res as object; } + // A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an + // indication that the token we're using has expired. + // Note that a 403 has a different meaning; it means that + // we provided a valid token but it doesn't have a high enough permission level for the route in question. + if (statusCode === 401) { + 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); + } + return res as object; } else { - throw new Error( - "It's currently not allowed to send non onion routed requests." - ); + // no need for auth, just do the onion request + const res = await sendViaOnion(roomDetails.serverPublicKey, builtUrl, { + method: request.method, + headers, + body, + }); + return res as object; } } @@ -342,7 +340,10 @@ export const getMessages = async ({ } // we have a 200 - const validMessages = await parseMessages(result); + const rawMessages = (result as any)?.result?.messages as Array< + Record + >; + const validMessages = await parseMessages(rawMessages); console.warn('validMessages', validMessages); return validMessages; }; @@ -354,7 +355,6 @@ export const postMessage = async ( try { const signedMessage = await message.sign(); const json = signedMessage.toJson(); - console.warn('posting message json', json); const request: OpenGroupV2Request = { method: 'POST', diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts new file mode 100644 index 000000000..16956f834 --- /dev/null +++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts @@ -0,0 +1,211 @@ +import { getV2OpenGroupRoomByRoomId } from '../../data/opengroups'; +import { + OpenGroupRequestCommonType, + OpenGroupV2CompactPollRequest, + parseMessages, +} from './ApiUtil'; +import { parseStatusCodeFromOnionRequest } from './OpenGroupAPIV2Parser'; +import _ from 'lodash'; +import { sendViaOnion } from '../../session/onions/onionSend'; +import { OpenGroupManagerV2 } from './OpenGroupManagerV2'; +import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; + +const COMPACT_POLL_ENDPOINT = 'compact_poll'; + +export const compactFetchEverything = async ( + rooms: Array +): Promise => { + // fetch all we need + const compactPollRequest = await getCompactPollRequest(rooms); + if (!compactPollRequest) { + window.log.info('Nothing found to be fetched. returning'); + return null; + } + + const result = await sendOpenGroupV2RequestCompactPoll(compactPollRequest); + const statusCode = parseStatusCodeFromOnionRequest(result); + if (statusCode !== 200) { + return null; + } + return result; +}; + +/** + * This return body to be used to do the compactPoll + */ +const getCompactPollRequest = async ( + rooms: Array +): Promise => { + // first verify the rooms we got are all from on the same server + let firstUrl: string; + if (rooms) { + firstUrl = rooms[0].serverUrl; + const anotherUrl = rooms.some(r => r.serverUrl !== firstUrl); + if (anotherUrl) { + throw new Error('CompactPoll is designed for a single server'); + } + } else { + window.log.warn('CompactPoll: No room given. nothing to do'); + return null; + } + + const allServerPubKeys: Array = []; + + const roomsRequestInfos = _.compact( + await Promise.all( + rooms.map(async ({ roomId, serverUrl }) => { + try { + const fetchedInfo = await getV2OpenGroupRoomByRoomId({ + serverUrl, + roomId, + }); + if (!fetchedInfo) { + window.log.warn('Could not find this room getMessages'); + return null; + } + + const { + lastMessageFetchedServerID, + lastMessageDeletedServerID, + token, + serverPublicKey, + } = fetchedInfo; + allServerPubKeys.push(serverPublicKey); + const roomRequestContent: Record = { + room_id: roomId, + auth_token: token || '', + }; + if (lastMessageDeletedServerID) { + roomRequestContent.from_deletion_server_id = lastMessageDeletedServerID; + } + + if (lastMessageFetchedServerID) { + roomRequestContent.from_message_server_id = lastMessageFetchedServerID; + } + + return roomRequestContent; + } catch (e) { + window.log.warn('failed to fetch roominfos for room', roomId); + return null; + } + }) + ) + ); + if (!roomsRequestInfos?.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; + } + const body = JSON.stringify({ + requests: roomsRequestInfos, + }); + return { + body, + server: firstUrl, + serverPubKey: firstPubkey, + 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 +): Promise { + const { server, endpoint, body, serverPubKey } = request; + // this will throw if the url is not valid + const builtUrl = new URL(`${server}/${endpoint}`); + + console.warn(`sending compactPoll request: ${request.body}`); + + const res = await sendViaOnion(serverPubKey, builtUrl, { + method: 'POST', + body, + }); + + const statusCode = parseStatusCodeFromOnionRequest(res); + if (!statusCode) { + window.log.warn( + 'sendOpenGroupV2Request Got unknown status code; res:', + res + ); + return res as object; + } + + const results = await parseCompactPollResults(res); + + throw new Error( + 'See how we handle needs of new tokens, and save stuff to db (last deleted, ... conversation commit, etc' + ); + + return res as object; +} + +type ParsedRoomCompactPollResults = { + roomId: string; + deletions: Array; + messages: Array; + moderators: Array; +}; + +const parseCompactPollResult = async ( + singleRoomResult: any +): Promise => { + const { + room_id, + deletions: rawDeletions, + messages: rawMessages, + moderators: rawMods, + } = singleRoomResult; + + if ( + !room_id || + rawDeletions === undefined || + rawMessages === undefined || + rawMods === undefined + ) { + window.log.warn('Invalid compactPoll result', singleRoomResult); + return null; + } + + const validMessages = await parseMessages(rawMessages); + const moderators = rawMods as Array; + const deletions = rawDeletions as Array; + + return { roomId: room_id, deletions, messages: validMessages, moderators }; +}; + +const parseCompactPollResults = async ( + res: any +): Promise | null> => { + if ( + !res || + !res.result || + !res.result.results || + !res.result.results.length + ) { + return null; + } + const arrayOfResults = res.result.results as Array; + + const parsedResults: Array = _.compact( + await Promise.all(arrayOfResults.map(parseCompactPollResult)) + ); + + if (!parsedResults || !parsedResults.length) { + return null; + } + return parsedResults; +}; diff --git a/ts/opengroup/opengroupV2/OpenGroupPollerV2.ts b/ts/opengroup/opengroupV2/OpenGroupPollerV2.ts index d5bd62df1..4a71a1b96 100644 --- a/ts/opengroup/opengroupV2/OpenGroupPollerV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupPollerV2.ts @@ -45,7 +45,7 @@ export class OpenGroupPollerV2 { return; } this.isPolling = true; - window.log.warn('pollForNewMessages TODO'); + window.log.warn('compactPoll TODO'); // use abortController and do not trigger new messages if it was canceled this.isPolling = false; }