UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

360 lines (308 loc) 10.4 kB
/** * 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; } }