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.
session-desktop/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts

507 lines
14 KiB
TypeScript

import _ from 'lodash';
import {
getV2OpenGroupRoomByRoomId,
saveV2OpenGroupRoom,
} from '../../data/opengroups';
import { ConversationController } from '../../session/conversations';
import { getSodium } from '../../session/crypto';
import { sendViaOnion } from '../../session/onions/onionSend';
import { PubKey } from '../../session/types';
import { allowOnlyOneAtATime } from '../../session/utils/Promise';
import {
fromBase64ToArray,
fromBase64ToArrayBuffer,
fromHexToArray,
toHex,
} from '../../session/utils/String';
import {
getIdentityKeyPair,
getOurPubKeyStrFromCache,
} from '../../session/utils/User';
import { getOpenGroupV2ConversationId } from '../utils/OpenGroupUtils';
import {
buildUrl,
cachedModerators,
OpenGroupRequestCommonType,
OpenGroupV2Info,
OpenGroupV2Request,
parseMessages,
setCachedModerators,
} from './ApiUtil';
import { OpenGroupMessageV2 } from './OpenGroupMessageV2';
// This function might throw
async function sendOpenGroupV2Request(
request: OpenGroupV2Request
): Promise<Object> {
const builtUrl = buildUrl(request);
if (!builtUrl) {
throw new Error('Invalid request');
}
// 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,
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 (!token) {
throw new Error('Failed to get token for open group v2');
}
headers.Authorization = token;
const res = await sendViaOnion(roomDetails.serverPublicKey, builtUrl, {
method: request.method,
headers,
body,
});
const statusCode = res?.result?.status_code;
if (!statusCode) {
window.log.warn(
'sendOpenGroupV2Request Got unknown status code; res:',
res
);
return res;
}
// 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;
} else {
// no need for auth, just do the onion request
const res = await sendViaOnion(roomDetails.serverPublicKey, builtUrl, {
method: request.method,
headers,
body,
});
return res;
}
return {};
} else {
throw new Error(
"It's currently not allowed to send non onion routed requests."
);
}
}
// tslint:disable: member-ordering
export async function requestNewAuthToken({
serverUrl,
roomId,
}: OpenGroupRequestCommonType): Promise<string> {
const userKeyPair = await getIdentityKeyPair();
if (!userKeyPair) {
throw new Error('Failed to fetch user keypair');
}
const ourPubkey = getOurPubKeyStrFromCache();
const parameters = {} as Record<string, string>;
parameters.public_key = ourPubkey;
const request: OpenGroupV2Request = {
method: 'GET',
room: roomId,
server: serverUrl,
queryParams: parameters,
isAuthRequired: false,
endpoint: 'auth_token_challenge',
};
const json = (await sendOpenGroupV2Request(request)) as any;
// parse the json
if (!json || !json?.result?.challenge) {
throw new Error('Parsing failed');
}
const {
ciphertext: base64EncodedCiphertext,
ephemeral_public_key: base64EncodedEphemeralPublicKey,
} = json?.result?.challenge;
if (!base64EncodedCiphertext || !base64EncodedEphemeralPublicKey) {
throw new Error('Parsing failed');
}
const ciphertext = fromBase64ToArrayBuffer(base64EncodedCiphertext);
const ephemeralPublicKey = fromBase64ToArrayBuffer(
base64EncodedEphemeralPublicKey
);
try {
const symmetricKey = await window.libloki.crypto.deriveSymmetricKey(
ephemeralPublicKey,
userKeyPair.privKey
);
const plaintextBuffer = await window.libloki.crypto.DecryptAESGCM(
symmetricKey,
ciphertext
);
const token = toHex(plaintextBuffer);
console.warn('token', token);
return token;
} catch (e) {
window.log.error('Failed to decrypt token open group v2');
throw e;
}
}
/**
* This function might throw
*
*/
export async function openGroupV2GetRoomInfo({
serverUrl,
roomId,
}: {
roomId: string;
serverUrl: string;
}): Promise<OpenGroupV2Info> {
const request: OpenGroupV2Request = {
method: 'GET',
server: serverUrl,
room: roomId,
isAuthRequired: false,
endpoint: `rooms/${roomId}`,
};
const result = (await sendOpenGroupV2Request(request)) as any;
if (result?.result?.room) {
const { id, name, image_id: imageId } = result?.result?.room;
if (!id || !name) {
throw new Error('Parsing failed');
}
const info: OpenGroupV2Info = {
id,
name,
imageId,
};
return info;
}
throw new Error('getInfo failed');
}
async function claimAuthToken(
authToken: string,
serverUrl: string,
roomId: string
): Promise<string> {
// Set explicitly here because is isn't in the database yet at this point
const headers = { Authorization: authToken };
const request: OpenGroupV2Request = {
method: 'POST',
headers,
room: roomId,
server: serverUrl,
queryParams: { public_key: getOurPubKeyStrFromCache() },
isAuthRequired: false,
endpoint: 'claim_auth_token',
};
const result = (await sendOpenGroupV2Request(request)) as any;
if (result?.result?.status_code !== 200) {
throw new Error(
`Could not claim token, status code: ${result?.result?.status_code}`
);
}
return authToken;
}
export async function getAuthToken({
serverUrl,
roomId,
}: OpenGroupRequestCommonType): Promise<string> {
// first try to fetch from db a saved token.
const roomDetails = await getV2OpenGroupRoomByRoomId({ serverUrl, roomId });
if (!roomDetails) {
throw new Error('getAuthToken Room does not exist.');
}
if (roomDetails?.token) {
return roomDetails?.token;
}
await allowOnlyOneAtATime(
`getAuthTokenV2${serverUrl}:${roomId}`,
async () => {
try {
const token = await requestNewAuthToken({ serverUrl, roomId });
// claimAuthToken throws if the status code is not valid
const claimedToken = await claimAuthToken(token, serverUrl, roomId);
roomDetails.token = token;
await saveV2OpenGroupRoom(roomDetails);
} catch (e) {
window.log.error('Failed to getAuthToken', e);
throw e;
}
}
);
return 'token';
}
export const deleteAuthToken = async ({
serverUrl,
roomId,
}: OpenGroupRequestCommonType) => {
const request: OpenGroupV2Request = {
method: 'DELETE',
room: roomId,
server: serverUrl,
isAuthRequired: false,
endpoint: 'auth_token',
};
const result = (await sendOpenGroupV2Request(request)) as any;
if (result?.result?.status_code !== 200) {
throw new Error(
`Could not deleteAuthToken, status code: ${result?.result?.status_code}`
);
}
};
export const getMessages = async ({
serverUrl,
roomId,
}: OpenGroupRequestCommonType): Promise<Array<OpenGroupMessageV2>> => {
const roomInfos = await getV2OpenGroupRoomByRoomId({ serverUrl, roomId });
if (!roomInfos) {
throw new Error('Could not find this room getMessages');
}
const { lastMessageFetchedServerID } = roomInfos;
const queryParams = {} as Record<string, any>;
if (lastMessageFetchedServerID) {
queryParams.from_server_id = lastMessageFetchedServerID;
}
const request: OpenGroupV2Request = {
method: 'GET',
room: roomId,
server: serverUrl,
isAuthRequired: true,
endpoint: 'messages',
};
const result = (await sendOpenGroupV2Request(request)) as any;
if (result?.result?.status_code !== 200) {
throw new Error(
`Could not getMessages, status code: ${result?.result?.status_code}`
);
}
// we have a 200
const rawMessages = result?.result?.messages as Array<Record<string, any>>;
if (!rawMessages) {
window.log.info('no new messages');
return [];
}
const validMessages = await parseMessages(rawMessages);
console.warn('validMessages', validMessages);
return validMessages;
};
export const postMessage = async (
message: OpenGroupMessageV2,
room: OpenGroupRequestCommonType
) => {
try {
const signedMessage = await message.sign();
const json = signedMessage.toJson();
console.warn('posting message json', json);
const request: OpenGroupV2Request = {
method: 'POST',
room: room.roomId,
server: room.serverUrl,
queryParams: json,
isAuthRequired: true,
endpoint: 'messages',
};
const result = (await sendOpenGroupV2Request(request)) as any;
if (result?.result?.status_code !== 200) {
throw new Error(
`Could not postMessage, status code: ${result?.result?.status_code}`
);
}
const rawMessage = result?.result?.message;
if (!rawMessage) {
throw new Error('postMessage parsing failed');
}
// this will throw if the json is not valid
return OpenGroupMessageV2.fromJson(rawMessage);
} catch (e) {
window.log.error('Failed to post message to open group v2', e);
throw e;
}
};
/** Those functions are related to moderators management */
export const getModerators = async ({
serverUrl,
roomId,
}: OpenGroupRequestCommonType): Promise<Array<string>> => {
const request: OpenGroupV2Request = {
method: 'GET',
room: roomId,
server: serverUrl,
isAuthRequired: true,
endpoint: 'moderators',
};
const result = (await sendOpenGroupV2Request(request)) as any;
if (result?.result?.status_code !== 200) {
throw new Error(
`Could not getModerators, status code: ${result?.result?.status_code}`
);
}
const moderatorsGot = result?.result?.moderators;
if (moderatorsGot === undefined) {
throw new Error(
'Could not getModerators, got no moderatorsGot at all in json.'
);
}
setCachedModerators(serverUrl, roomId, moderatorsGot || []);
return moderatorsGot || [];
};
export const isUserModerator = (
publicKey: string,
roomInfos: OpenGroupRequestCommonType
): boolean => {
return (
cachedModerators
?.get(roomInfos.serverUrl)
?.get(roomInfos.roomId)
?.has(publicKey) || false
);
};
export const banUser = async (
publicKey: string,
roomInfos: OpenGroupRequestCommonType
): Promise<void> => {
const queryParams = { public_key: publicKey };
const request: OpenGroupV2Request = {
method: 'POST',
room: roomInfos.roomId,
server: roomInfos.serverUrl,
isAuthRequired: true,
queryParams,
endpoint: 'block_list',
};
await sendOpenGroupV2Request(request);
};
export const unbanUser = async (
publicKey: string,
roomInfos: OpenGroupRequestCommonType
): Promise<void> => {
const request: OpenGroupV2Request = {
method: 'DELETE',
room: roomInfos.roomId,
server: roomInfos.serverUrl,
isAuthRequired: true,
endpoint: `block_list/${publicKey}`,
};
await sendOpenGroupV2Request(request);
};
export const getAllRoomInfos = async (
roomInfos: OpenGroupRequestCommonType
) => {
// room should not be required here
const request: OpenGroupV2Request = {
method: 'GET',
room: roomInfos.roomId,
server: roomInfos.serverUrl,
isAuthRequired: false,
endpoint: 'rooms',
};
const result = (await sendOpenGroupV2Request(request)) as any;
if (result?.result?.status_code !== 200) {
window.log.warn('getAllRoomInfos failed invalid status code');
return;
}
const rooms = result?.result?.rooms as Array<any>;
if (!rooms || !rooms.length) {
window.log.warn('getAllRoomInfos failed invalid infos');
return;
}
return _.compact(
rooms.map(room => {
// check that the room is correctly filled
const { id, name, image_id: imageId } = room;
if (!id || !name) {
window.log.info('getAllRoomInfos: Got invalid room details, skipping');
return null;
}
return { id, name, imageId } as OpenGroupV2Info;
})
);
};
export const getMemberCount = async (
roomInfos: OpenGroupRequestCommonType
): Promise<void> => {
const request: OpenGroupV2Request = {
method: 'GET',
room: roomInfos.roomId,
server: roomInfos.serverUrl,
isAuthRequired: true,
endpoint: 'member_count',
};
const result = (await sendOpenGroupV2Request(request)) as any;
if (result?.result?.status_code !== 200) {
window.log.warn('getMemberCount failed invalid status code');
return;
}
const count = result?.result?.member_count as number;
if (count === undefined) {
window.log.warn('getMemberCount failed invalid count');
return;
}
const conversationId = getOpenGroupV2ConversationId(
roomInfos.serverUrl,
roomInfos.roomId
);
const convo = ConversationController.getInstance().get(conversationId);
if (!convo) {
window.log.warn(
'cannot update conversation memberCount as it does not exist'
);
return;
}
if (convo.get('subscriberCount') !== count) {
convo.set({ subscriberCount: count });
// triggers the save to db and the refresh of the UI
await convo.commit();
}
};