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.
420 lines
12 KiB
TypeScript
420 lines
12 KiB
TypeScript
import { OpenGroupData } from '../../../../data/opengroups';
|
|
import _, { flatten, isEmpty, isNumber, isObject } from 'lodash';
|
|
import { OnionSending, OnionV4JSONSnodeResponse } from '../../../onions/onionSend';
|
|
import {
|
|
OpenGroupPollingUtils,
|
|
OpenGroupRequestHeaders,
|
|
} from '../opengroupV2/OpenGroupPollingUtils';
|
|
import { addJsonContentTypeToHeaders } from './sogsV3SendMessage';
|
|
import { AbortSignal } from 'abort-controller';
|
|
import { roomHasBlindEnabled } from './sogsV3Capabilities';
|
|
import { Reactions } from '../../../../util/reactions';
|
|
|
|
type BatchFetchRequestOptions = {
|
|
method: 'POST' | 'PUT' | 'GET' | 'DELETE';
|
|
path: string;
|
|
headers?: any;
|
|
};
|
|
|
|
/**
|
|
* Should only have this or the json field but not both at the same time
|
|
*/
|
|
type BatchBodyRequestSharedOptions = {
|
|
method: 'POST' | 'PUT' | 'GET' | 'DELETE';
|
|
path: string;
|
|
headers?: any;
|
|
};
|
|
|
|
interface BatchJsonSubrequestOptions extends BatchBodyRequestSharedOptions {
|
|
json: object;
|
|
}
|
|
|
|
type BatchBodyRequest = BatchJsonSubrequestOptions;
|
|
|
|
type BatchSubRequest = BatchBodyRequest | BatchFetchRequestOptions;
|
|
|
|
type BatchRequest = {
|
|
/** Used by server to process request */
|
|
endpoint: string;
|
|
/** Used by server to process request */
|
|
method: string;
|
|
/** Used by server to process request */
|
|
body: string;
|
|
/** Used by server to process request and authentication */
|
|
headers: OpenGroupRequestHeaders;
|
|
};
|
|
|
|
export type BatchSogsReponse = {
|
|
status_code: number;
|
|
body?: Array<{ body: object; code: number; headers?: Record<string, string> }>;
|
|
};
|
|
|
|
export const sogsBatchSend = async (
|
|
serverUrl: string,
|
|
roomInfos: Set<string>,
|
|
abortSignal: AbortSignal,
|
|
batchRequestOptions: Array<OpenGroupBatchRow>,
|
|
batchType: 'batch' | 'sequence'
|
|
): Promise<BatchSogsReponse | null> => {
|
|
// getting server pk for room
|
|
const [roomId] = roomInfos;
|
|
const fetchedRoomInfo = OpenGroupData.getV2OpenGroupRoomByRoomId({
|
|
serverUrl,
|
|
roomId,
|
|
});
|
|
if (!fetchedRoomInfo || !fetchedRoomInfo?.serverPublicKey) {
|
|
window?.log?.warn('Couldnt get fetched info or server public key -- aborting batch request');
|
|
return null;
|
|
}
|
|
const { serverPublicKey } = fetchedRoomInfo;
|
|
// send with blinding if we need to
|
|
|
|
const requireBlinding = Boolean(roomHasBlindEnabled(fetchedRoomInfo));
|
|
// creating batch request
|
|
const batchRequest = await getBatchRequest(
|
|
serverPublicKey,
|
|
batchRequestOptions,
|
|
requireBlinding,
|
|
batchType
|
|
);
|
|
if (!batchRequest) {
|
|
window?.log?.error('Could not generate batch request. Aborting request');
|
|
return null;
|
|
}
|
|
|
|
const result = await sendSogsBatchRequestOnionV4(
|
|
serverUrl,
|
|
serverPublicKey,
|
|
batchRequest,
|
|
abortSignal
|
|
);
|
|
if (abortSignal.aborted) {
|
|
window.log.info('sendSogsBatchRequestOnionV4 aborted.');
|
|
return null;
|
|
}
|
|
|
|
return result || null;
|
|
};
|
|
|
|
export function parseBatchGlobalStatusCode(
|
|
response?: BatchSogsReponse | OnionV4JSONSnodeResponse | null
|
|
): number | undefined {
|
|
return response?.status_code;
|
|
}
|
|
|
|
export function batchGlobalIsSuccess(
|
|
response?: BatchSogsReponse | OnionV4JSONSnodeResponse | null
|
|
): boolean {
|
|
const status = parseBatchGlobalStatusCode(response);
|
|
return Boolean(status && isNumber(status) && status >= 200 && status <= 300);
|
|
}
|
|
|
|
function parseBatchFirstSubStatusCode(response?: BatchSogsReponse | null): number | undefined {
|
|
return response?.body?.[0].code;
|
|
}
|
|
|
|
export function batchFirstSubIsSuccess(response?: BatchSogsReponse | null): boolean {
|
|
const status = parseBatchFirstSubStatusCode(response);
|
|
return Boolean(status && isNumber(status) && status >= 200 && status <= 300);
|
|
}
|
|
|
|
export type SubrequestOptionType = 'capabilities' | 'messages' | 'pollInfo' | 'inbox';
|
|
|
|
export type SubRequestCapabilitiesType = { type: 'capabilities' };
|
|
|
|
export type SubRequestMessagesObjectType =
|
|
| {
|
|
roomId: string;
|
|
sinceSeqNo?: number;
|
|
}
|
|
| undefined;
|
|
|
|
export type SubRequestMessagesType = {
|
|
type: 'messages';
|
|
messages?: SubRequestMessagesObjectType;
|
|
};
|
|
|
|
export type SubRequestPollInfoType = {
|
|
type: 'pollInfo';
|
|
pollInfo: {
|
|
roomId: string;
|
|
infoUpdated?: number;
|
|
};
|
|
};
|
|
|
|
export type SubRequestInboxType = {
|
|
type: 'inbox';
|
|
inboxSince?: {
|
|
id?: number;
|
|
};
|
|
};
|
|
|
|
export type SubRequestOutboxType = {
|
|
type: 'outbox';
|
|
outboxSince?: {
|
|
id?: number;
|
|
};
|
|
};
|
|
|
|
export type SubRequestDeleteMessageType = {
|
|
type: 'deleteMessage';
|
|
deleteMessage: {
|
|
messageId: number;
|
|
roomId: string;
|
|
};
|
|
};
|
|
|
|
export type SubRequestAddRemoveModeratorType = {
|
|
type: 'addRemoveModerators';
|
|
addRemoveModerators: {
|
|
type: 'add_mods' | 'remove_mods';
|
|
sessionIds: Array<string>; // can be blinded id or not
|
|
roomId: string; // for now we support only granting/removing mods to single rooms from session
|
|
};
|
|
};
|
|
|
|
export type SubRequestBanUnbanUserType = {
|
|
type: 'banUnbanUser';
|
|
banUnbanUser: {
|
|
type: 'ban' | 'unban';
|
|
sessionId: string; // can be blinded id or not
|
|
roomId: string;
|
|
};
|
|
};
|
|
|
|
export type SubRequestDeleteAllUserPostsType = {
|
|
type: 'deleteAllPosts';
|
|
deleteAllPosts: {
|
|
sessionId: string; // can be blinded id or not
|
|
roomId: string;
|
|
};
|
|
};
|
|
|
|
export type SubRequestUpdateRoomType = {
|
|
type: 'updateRoom';
|
|
updateRoom: {
|
|
roomId: string;
|
|
imageId: number; // the fileId uploaded to this sogs and to be referenced as preview/room image
|
|
// name and other options are unsupported for now
|
|
};
|
|
};
|
|
|
|
export type SubRequestDeleteReactionType = {
|
|
type: 'deleteReaction';
|
|
deleteReaction: {
|
|
reaction: string;
|
|
messageId: number;
|
|
roomId: string;
|
|
};
|
|
};
|
|
|
|
export type OpenGroupBatchRow =
|
|
| SubRequestCapabilitiesType
|
|
| SubRequestMessagesType
|
|
| SubRequestPollInfoType
|
|
| SubRequestInboxType
|
|
| SubRequestOutboxType
|
|
| SubRequestDeleteMessageType
|
|
| SubRequestAddRemoveModeratorType
|
|
| SubRequestBanUnbanUserType
|
|
| SubRequestDeleteAllUserPostsType
|
|
| SubRequestUpdateRoomType
|
|
| SubRequestDeleteReactionType;
|
|
|
|
/**
|
|
*
|
|
* @param options Array of subrequest options to be made.
|
|
*/
|
|
const makeBatchRequestPayload = (
|
|
options: OpenGroupBatchRow
|
|
): BatchSubRequest | Array<BatchSubRequest> | null => {
|
|
switch (options.type) {
|
|
case 'capabilities':
|
|
return {
|
|
method: 'GET',
|
|
path: '/capabilities',
|
|
};
|
|
|
|
case 'messages':
|
|
if (options.messages) {
|
|
return {
|
|
method: 'GET',
|
|
path: isNumber(options.messages.sinceSeqNo)
|
|
? `/room/${options.messages.roomId}/messages/since/${options.messages.sinceSeqNo}?t=r&reactors=${Reactions.SOGSReactorsFetchCount}`
|
|
: `/room/${options.messages.roomId}/messages/recent?reactors=${Reactions.SOGSReactorsFetchCount}`,
|
|
};
|
|
}
|
|
break;
|
|
|
|
case 'inbox':
|
|
return {
|
|
method: 'GET',
|
|
path:
|
|
options?.inboxSince?.id && isNumber(options.inboxSince.id)
|
|
? `/inbox/since/${options.inboxSince.id}`
|
|
: '/inbox',
|
|
};
|
|
|
|
case 'outbox':
|
|
return {
|
|
method: 'GET',
|
|
path:
|
|
options?.outboxSince?.id && isNumber(options.outboxSince.id)
|
|
? `/outbox/since/${options.outboxSince.id}`
|
|
: '/outbox',
|
|
};
|
|
|
|
case 'pollInfo':
|
|
return {
|
|
method: 'GET',
|
|
path: `/room/${options.pollInfo.roomId}/pollInfo/${options.pollInfo.infoUpdated}`,
|
|
};
|
|
|
|
case 'deleteMessage':
|
|
return {
|
|
method: 'DELETE',
|
|
path: `/room/${options.deleteMessage.roomId}/message/${options.deleteMessage.messageId}`,
|
|
};
|
|
|
|
case 'addRemoveModerators':
|
|
const isAddMod = Boolean(options.addRemoveModerators.type === 'add_mods');
|
|
return options.addRemoveModerators.sessionIds.map(sessionId => ({
|
|
method: 'POST',
|
|
path: `/user/${sessionId}/moderator`,
|
|
|
|
// An admin has moderator permissions automatically, but removing his admin permissions only will keep him as a moderator.
|
|
// We do not want this currently. When removing an admin from Session Desktop we want to remove all his permissions server side.
|
|
// We'll need to build a complete dialog with options to make the whole admins/moderator/global/visible/hidden logic work as the server was built for.
|
|
json: {
|
|
rooms: [options.addRemoveModerators.roomId],
|
|
global: false,
|
|
visible: true,
|
|
admin: isAddMod,
|
|
moderator: isAddMod,
|
|
},
|
|
}));
|
|
case 'banUnbanUser':
|
|
const isBan = Boolean(options.banUnbanUser.type === 'ban');
|
|
return {
|
|
method: 'POST',
|
|
path: `/user/${options.banUnbanUser.sessionId}/${isBan ? 'ban' : 'unban'}`,
|
|
json: {
|
|
rooms: [options.banUnbanUser.roomId],
|
|
|
|
// watch out ban and unban user do not allow the same args
|
|
// global: false, // for now we do not support the global argument, rooms cannot be set if we use it
|
|
// timeout: null, // for now we do not support the timeout argument
|
|
},
|
|
};
|
|
case 'deleteAllPosts':
|
|
return {
|
|
method: 'DELETE',
|
|
path: `/room/${options.deleteAllPosts.roomId}/all/${options.deleteAllPosts.sessionId}`,
|
|
};
|
|
case 'updateRoom':
|
|
return {
|
|
method: 'PUT',
|
|
path: `/room/${options.updateRoom.roomId}`,
|
|
json: { image: options.updateRoom.imageId },
|
|
};
|
|
case 'deleteReaction':
|
|
return {
|
|
method: 'DELETE',
|
|
path: `/room/${options.deleteReaction.roomId}/reactions/${options.deleteReaction.messageId}/${options.deleteReaction.reaction}`,
|
|
};
|
|
default:
|
|
throw new Error('Invalid batch request row');
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Get the request to get all of the details we care from an opengroup, across all rooms.
|
|
* Only compatible with v4 onion requests.
|
|
*
|
|
* if isSequence is set to true, each rows will be run in order until the first one fails
|
|
*/
|
|
const getBatchRequest = async (
|
|
serverPublicKey: string,
|
|
batchOptions: Array<OpenGroupBatchRow>,
|
|
requireBlinding: boolean,
|
|
batchType: 'batch' | 'sequence'
|
|
): Promise<BatchRequest | undefined> => {
|
|
const batchEndpoint = batchType === 'sequence' ? '/sequence' : '/batch';
|
|
const batchMethod = 'POST';
|
|
if (!batchOptions || isEmpty(batchOptions)) {
|
|
return undefined;
|
|
}
|
|
|
|
const batchBody = flatten(
|
|
batchOptions.map(options => {
|
|
return makeBatchRequestPayload(options);
|
|
})
|
|
);
|
|
|
|
const stringBody = JSON.stringify(batchBody);
|
|
|
|
const headers = await OpenGroupPollingUtils.getOurOpenGroupHeaders(
|
|
serverPublicKey,
|
|
batchEndpoint,
|
|
batchMethod,
|
|
requireBlinding,
|
|
stringBody
|
|
);
|
|
|
|
if (!headers) {
|
|
window?.log?.error('Unable to create headers for batch request - aborting');
|
|
return;
|
|
}
|
|
|
|
return {
|
|
endpoint: batchEndpoint,
|
|
method: batchMethod,
|
|
body: stringBody,
|
|
headers: addJsonContentTypeToHeaders(headers),
|
|
};
|
|
};
|
|
|
|
const sendSogsBatchRequestOnionV4 = async (
|
|
serverUrl: string,
|
|
serverPubkey: string,
|
|
request: BatchRequest,
|
|
abortSignal: AbortSignal
|
|
): Promise<null | BatchSogsReponse> => {
|
|
const { endpoint, headers, method, body } = request;
|
|
if (!endpoint.startsWith('/')) {
|
|
throw new Error('endpoint needs a leading /');
|
|
}
|
|
const builtUrl = new URL(`${serverUrl}${endpoint}`);
|
|
|
|
// this function extracts the body and status_code and JSON.parse it already
|
|
const batchResponse = await OnionSending.sendViaOnionV4ToNonSnodeWithRetries(
|
|
serverPubkey,
|
|
builtUrl,
|
|
{
|
|
method,
|
|
headers,
|
|
body,
|
|
useV4: true,
|
|
},
|
|
false,
|
|
abortSignal
|
|
);
|
|
|
|
if (abortSignal.aborted) {
|
|
return null;
|
|
}
|
|
|
|
if (!batchResponse) {
|
|
window?.log?.error('sogsbatch: Undefined batch response - cancelling batch request');
|
|
return null;
|
|
}
|
|
if (isObject(batchResponse.body)) {
|
|
return batchResponse as BatchSogsReponse;
|
|
}
|
|
|
|
window?.log?.warn('sogsbatch: batch response decoded body is not object. Returning null');
|
|
return null;
|
|
};
|