pubnub
Version:
Publish & Subscribe Real-time Messaging with PubNub
360 lines (308 loc) • 10.4 kB
text/typescript
/**
* Fetch messages REST API module.
*
* @internal
*/
import { TransportResponse } from '../types/transport-response';
import { ICryptoModule } from '../interfaces/crypto-module';
import { AbstractRequest } from '../components/request';
import * as FileSharing from '../types/api/file-sharing';
import RequestOperation from '../constants/operations';
import { KeySet, Payload, Query } from '../types/api';
import * as History from '../types/api/history';
import { encodeNames } from '../utils';
// --------------------------------------------------------
// ---------------------- Defaults ------------------------
// --------------------------------------------------------
// region Defaults
/**
* Whether verbose logging enabled or not.
*/
const LOG_VERBOSITY = false;
/**
* Whether message type should be returned or not.
*/
const INCLUDE_MESSAGE_TYPE = true;
/**
* Whether timetokens should be returned as strings by default or not.
*/
const STRINGIFY_TIMETOKENS = false;
/**
* Whether message publisher `uuid` should be returned or not.
*/
const INCLUDE_UUID = true;
/**
* Default number of messages which can be returned for single channel, and it is maximum as well.
*/
const SINGLE_CHANNEL_MESSAGES_COUNT = 100;
/**
* Default number of messages which can be returned for multiple channels or when fetched
* message actions.
*/
const MULTIPLE_CHANNELS_MESSAGES_COUNT = 25;
// endregion
// --------------------------------------------------------
// ------------------------ Types -------------------------
// --------------------------------------------------------
// region Types
/**
* Request configuration parameters.
*/
type RequestParameters = History.FetchMessagesParameters & {
/**
* PubNub REST API access key set.
*/
keySet: KeySet;
/**
* Published data encryption module.
*/
crypto?: ICryptoModule;
/**
* File download Url generation function.
*
* @param parameters - File download Url request configuration parameters.
*
* @returns File download Url.
*/
getFileUrl: (parameters: FileSharing.FileUrlParameters) => string;
/**
* Whether verbose logging enabled or not.
*
* @default `false`
*/
logVerbosity?: boolean;
};
/**
* Service success response.
*/
type ServiceResponse = {
/**
* Request result status code.
*/
status: number;
/**
* Whether service response represent error or not.
*/
error: boolean;
/**
* Human-readable error explanation.
*/
error_message: string;
/**
* List of previously published messages per requested channel.
*/
channels: {
[p: string]: {
/**
* Message payload (decrypted).
*/
message: History.FetchedMessage['message'];
/**
* When message has been received by PubNub service.
*/
timetoken: string;
/**
* Message publisher unique identifier.
*/
uuid?: string;
/**
* User-provided message type.
*/
custom_message_type?: string;
/**
* PubNub-defined message type.
*/
message_type?: History.PubNubMessageType | null;
/**
* Additional data which has been published along with message to be used with real-time
* events filter expression.
*/
meta?: Payload;
/**
* List of message reactions.
*/
actions?: History.Actions;
/**
* Custom published data type (user-provided).
*/
type?: string;
/**
* Space in which message has been received.
*/
space_id?: string;
}[];
};
/**
* Additional message actions fetch information.
*/
more?: History.MoreActions;
};
// endregion
/**
* Fetch messages from channels request.
*
* @internal
*/
export class FetchMessagesRequest extends AbstractRequest<History.FetchMessagesResponse, ServiceResponse> {
constructor(private readonly parameters: RequestParameters) {
super();
// Apply defaults.
const includeMessageActions = parameters.includeMessageActions ?? false;
const defaultCount =
parameters.channels.length > 1 || includeMessageActions
? MULTIPLE_CHANNELS_MESSAGES_COUNT
: SINGLE_CHANNEL_MESSAGES_COUNT;
if (!parameters.count) parameters.count = defaultCount;
else parameters.count = Math.min(parameters.count, defaultCount);
if (parameters.includeUuid) parameters.includeUUID = parameters.includeUuid;
else parameters.includeUUID ??= INCLUDE_UUID;
parameters.stringifiedTimeToken ??= STRINGIFY_TIMETOKENS;
parameters.includeMessageType ??= INCLUDE_MESSAGE_TYPE;
parameters.logVerbosity ??= LOG_VERBOSITY;
}
operation(): RequestOperation {
return RequestOperation.PNFetchMessagesOperation;
}
validate(): string | undefined {
const {
keySet: { subscribeKey },
channels,
includeMessageActions,
} = this.parameters;
if (!subscribeKey) return 'Missing Subscribe Key';
if (!channels) return 'Missing channels';
if (includeMessageActions !== undefined && includeMessageActions && channels.length > 1)
return (
'History can return actions data for a single channel only. Either pass a single channel ' +
'or disable the includeMessageActions flag.'
);
}
async parse(response: TransportResponse): Promise<History.FetchMessagesResponse> {
const serviceResponse = this.deserializeResponse(response);
const responseChannels = serviceResponse.channels ?? {};
const channels: History.FetchMessagesResponse['channels'] = {};
Object.keys(responseChannels).forEach((channel) => {
// Map service response to expected data object type structure.
channels[channel] = responseChannels[channel].map((payload) => {
// `null` message type means regular message.
if (payload.message_type === null) payload.message_type = History.PubNubMessageType.Message;
const processedPayload = this.processPayload(channel, payload);
const item = {
channel,
timetoken: payload.timetoken,
message: processedPayload.payload,
messageType: payload.message_type,
...(payload.custom_message_type ? { customMessageType: payload.custom_message_type } : {}),
uuid: payload.uuid,
};
if (payload.actions) {
const itemWithActions = item as unknown as History.FetchedMessageWithActions;
itemWithActions.actions = payload.actions;
// Backward compatibility for existing users.
// TODO: Remove in next release.
itemWithActions.data = payload.actions;
}
if (payload.meta) (item as History.FetchedMessage).meta = payload.meta;
if (processedPayload.error) (item as History.FetchedMessage).error = processedPayload.error;
return item as History.FetchedMessage;
});
});
if (serviceResponse.more)
return { channels, more: serviceResponse.more } as History.FetchMessagesWithActionsResponse;
return { channels } as History.FetchMessagesResponse;
}
protected get path(): string {
const {
keySet: { subscribeKey },
channels,
includeMessageActions,
} = this.parameters;
const endpoint = !includeMessageActions! ? 'history' : 'history-with-actions';
return `/v3/${endpoint}/sub-key/${subscribeKey}/channel/${encodeNames(channels)}`;
}
protected get queryParameters(): Query {
const {
start,
end,
count,
includeCustomMessageType,
includeMessageType,
includeMeta,
includeUUID,
stringifiedTimeToken,
} = this.parameters;
return {
max: count!,
...(start ? { start } : {}),
...(end ? { end } : {}),
...(stringifiedTimeToken! ? { string_message_token: 'true' } : {}),
...(includeMeta !== undefined && includeMeta ? { include_meta: 'true' } : {}),
...(includeUUID! ? { include_uuid: 'true' } : {}),
...(includeCustomMessageType !== undefined && includeCustomMessageType !== null
? { include_custom_message_type: includeCustomMessageType ? 'true' : 'false' }
: {}),
...(includeMessageType! ? { include_message_type: 'true' } : {}),
};
}
/**
* Parse single channel data entry.
*
* @param channel - Channel for which {@link payload} should be processed.
* @param payload - Source payload which should be processed and parsed to expected type.
*
* @returns
*/
private processPayload(
channel: string,
payload: ServiceResponse['channels'][string][number],
): {
payload: History.FetchedMessage['message'];
error?: string;
} {
const { crypto, logVerbosity } = this.parameters;
if (!crypto || typeof payload.message !== 'string') return { payload: payload.message };
let decryptedPayload: History.FetchedMessage['message'];
let error: string | undefined;
try {
const decryptedData = crypto.decrypt(payload.message);
decryptedPayload =
decryptedData instanceof ArrayBuffer
? JSON.parse(FetchMessagesRequest.decoder.decode(decryptedData))
: decryptedData;
} catch (err) {
if (logVerbosity!) console.log(`decryption error`, (err as Error).message);
decryptedPayload = payload.message;
error = `Error while decrypting message content: ${(err as Error).message}`;
}
if (
!error &&
decryptedPayload &&
payload.message_type == History.PubNubMessageType.Files &&
typeof decryptedPayload === 'object' &&
this.isFileMessage(decryptedPayload)
) {
const fileMessage = decryptedPayload;
return {
payload: {
message: fileMessage.message,
file: {
...fileMessage.file,
url: this.parameters.getFileUrl({ channel, id: fileMessage.file.id, name: fileMessage.file.name }),
},
},
error,
};
}
return { payload: decryptedPayload, error };
}
/**
* Check whether `payload` potentially represents file message.
*
* @param payload - Fetched message payload.
*
* @returns `true` if payload can be {@link History#FileMessage|FileMessage}.
*/
private isFileMessage(payload: History.FetchedMessage['message']): payload is History.FileMessage['message'] {
return (payload as History.FileMessage['message']).file !== undefined;
}
}