UNPKG

@ably/chat

Version:

Ably Chat is a set of purpose-built APIs for a host of chat features enabling you to create 1:1, 1:Many, Many:1 and Many:Many chat rooms for any scale. It is designed to meet a wide range of chat use cases, such as livestreams, in-game communication, cust

316 lines (272 loc) 9.9 kB
import * as Ably from 'ably'; import { Logger } from './logger.js'; import { Message, MessageHeaders, MessageMetadata, MessageOperationMetadata } from './message.js'; import { OrderBy } from './messages.js'; import { OccupancyData } from './occupancy-parser.js'; import { PaginatedResult } from './query.js'; import { messageFromRest, RestMessage } from './rest-types.js'; export interface HistoryQueryParams { start?: number; end?: number; orderBy?: OrderBy; limit?: number; /** * Serial indicating the starting point for message retrieval. * This serial is specific to the region of the channel the client is connected to. Messages published within * the same region of the channel are guaranteed to be received in increasing serial order. * @defaultValue undefined (not used if not specified) */ fromSerial?: string; } /** * In the REST API, we currently use the `direction` query parameter to specify the order of messages instead * of orderBy. So define this type for conversion purposes. */ type ApiHistoryQueryParams = Omit<HistoryQueryParams, 'orderBy'> & { direction?: 'forwards' | 'backwards'; }; export interface CreateMessageResponse { serial: string; createdAt: number; } interface SendMessageParams { text: string; metadata?: MessageMetadata; headers?: MessageHeaders; } /** * Represents the response for deleting or updating a message. */ export interface MessageOperationResponse { /** * The new message version. */ version: string; /** * The timestamp of the operation. */ timestamp: number; /** * The message that was created or updated. */ message: RestMessage; } type UpdateMessageResponse = MessageOperationResponse; type DeleteMessageResponse = MessageOperationResponse; interface UpdateMessageParams { /** * Message data to update. All fields are updated and, if omitted, they are * set to empty. */ message: { text: string; metadata?: MessageMetadata; headers?: MessageHeaders; }; /** Description of the update action */ description?: string; /** Metadata of the update action */ metadata?: MessageOperationMetadata; } interface DeleteMessageParams { /** Description of the delete action */ description?: string; /** Metadata of the delete action */ metadata?: MessageOperationMetadata; } /** * Parameters for sending a message reaction. */ export interface SendMessageReactionParams { /** * The type of reaction, must be one of {@link MessageReactionType}. */ type: string; /** * The reaction name to add; ie. the emoji. */ name: string; /** * The count of the reaction for type {@link MessageReactionType.Multiple}. * Defaults to 1 if not set. Not supported for other reaction types. * @defaultValue 1 */ count?: number; } /** * Parameters for deleting a message reaction. */ export interface DeleteMessageReactionParams { /** * The type of reaction, must be one of {@link MessageReactionType}. */ type: string; /** * The reaction name to remove, ie. the emoji. Required for all reaction types * except {@link MessageReactionType.Unique}. */ name?: string; } /** * Chat SDK Backend */ export class ChatApi { private readonly _realtime: Ably.Realtime; private readonly _logger: Logger; private readonly _apiProtocolVersion: number = 3; constructor(realtime: Ably.Realtime, logger: Logger) { this._realtime = realtime; this._logger = logger; } async history(roomName: string, params: HistoryQueryParams): Promise<PaginatedResult<Message>> { roomName = encodeURIComponent(roomName); // convert the params into internal format const apiParams: ApiHistoryQueryParams = { ...params }; if (params.orderBy) { switch (params.orderBy) { case OrderBy.NewestFirst: { apiParams.direction = 'backwards'; break; } case OrderBy.OldestFirst: { apiParams.direction = 'forwards'; break; } default: { // in vanilla JS use-cases, without types, we need to check non-enum values // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Ably.ErrorInfo(`invalid orderBy value: ${params.orderBy}`, 40000, 400); } } } const data = await this._makeAuthorizedPaginatedRequest<RestMessage>( `/chat/v3/rooms/${roomName}/messages`, apiParams, ); return this._recursivePaginateMessages(data); } private _recursivePaginateMessages(data: PaginatedResult<RestMessage>): PaginatedResult<Message> { const result: PaginatedResult<Message> = {} as PaginatedResult<Message>; result.items = data.items.map((payload) => messageFromRest(payload)); // Recursively map the next paginated data // eslint-disable-next-line unicorn/no-null result.next = () => data.next().then((nextData) => (nextData ? this._recursivePaginateMessages(nextData) : null)); result.first = () => data.first().then((firstData) => this._recursivePaginateMessages(firstData)); result.current = () => data.current().then((currentData) => this._recursivePaginateMessages(currentData)); result.hasNext = () => data.hasNext(); result.isLast = () => data.isLast(); return { ...data, ...result }; } async getMessage(roomName: string, serial: string): Promise<Message> { const encodedSerial = encodeURIComponent(serial); roomName = encodeURIComponent(roomName); const restMessage = await this._makeAuthorizedRequest<RestMessage>( `/chat/v3/rooms/${roomName}/messages/${encodedSerial}`, 'GET', ); return messageFromRest(restMessage); } deleteMessage(roomName: string, serial: string, params?: DeleteMessageParams): Promise<DeleteMessageResponse> { const body: { description?: string; metadata?: MessageOperationMetadata } = { description: params?.description, metadata: params?.metadata, }; serial = encodeURIComponent(serial); roomName = encodeURIComponent(roomName); return this._makeAuthorizedRequest<DeleteMessageResponse>( `/chat/v3/rooms/${roomName}/messages/${serial}/delete`, 'POST', body, {}, ); } sendMessage(roomName: string, params: SendMessageParams): Promise<CreateMessageResponse> { const body: { text: string; metadata?: MessageMetadata; headers?: MessageHeaders; } = { text: params.text }; if (params.metadata) { body.metadata = params.metadata; } if (params.headers) { body.headers = params.headers; } roomName = encodeURIComponent(roomName); return this._makeAuthorizedRequest<CreateMessageResponse>(`/chat/v3/rooms/${roomName}/messages`, 'POST', body); } updateMessage(roomName: string, serial: string, params: UpdateMessageParams): Promise<UpdateMessageResponse> { const encodedSerial = encodeURIComponent(serial); roomName = encodeURIComponent(roomName); return this._makeAuthorizedRequest<UpdateMessageResponse>( `/chat/v3/rooms/${roomName}/messages/${encodedSerial}`, 'PUT', params, ); } sendMessageReaction(roomName: string, serial: string, data: SendMessageReactionParams): Promise<void> { const encodedSerial = encodeURIComponent(serial); roomName = encodeURIComponent(roomName); return this._makeAuthorizedRequest(`/chat/v3/rooms/${roomName}/messages/${encodedSerial}/reactions`, 'POST', data); } deleteMessageReaction(roomName: string, serial: string, data: DeleteMessageReactionParams): Promise<void> { const encodedSerial = encodeURIComponent(serial); roomName = encodeURIComponent(roomName); return this._makeAuthorizedRequest( `/chat/v3/rooms/${roomName}/messages/${encodedSerial}/reactions`, 'DELETE', undefined, data, ); } getClientReactions(roomName: string, serial: string, clientId?: string): Promise<Message['reactions']> { const encodedSerial = encodeURIComponent(serial); roomName = encodeURIComponent(roomName); const params = clientId ? { forClientId: clientId } : {}; return this._makeAuthorizedRequest<Message['reactions']>( `/chat/v3/rooms/${roomName}/messages/${encodedSerial}/client-reactions`, 'GET', undefined, params, ); } getOccupancy(roomName: string): Promise<OccupancyData> { roomName = encodeURIComponent(roomName); return this._makeAuthorizedRequest<OccupancyData>(`/chat/v3/rooms/${roomName}/occupancy`, 'GET'); } private async _makeAuthorizedRequest<RES = undefined>( url: string, method: 'POST' | 'GET' | 'PUT' | 'DELETE' | 'PATCH', body?: unknown, params?: unknown, ): Promise<RES> { const response = await this._realtime.request<RES>(method, url, this._apiProtocolVersion, params, body); if (!response.success) { this._logger.error('ChatApi._makeAuthorizedRequest(); failed to make request', { url, statusCode: response.statusCode, errorCode: response.errorCode, errorMessage: response.errorMessage, }); throw new Ably.ErrorInfo(response.errorMessage, response.errorCode, response.statusCode); } return response.items[0] as RES; } private async _makeAuthorizedPaginatedRequest<RES>( url: string, params?: unknown, body?: unknown, ): Promise<PaginatedResult<RES>> { const response = await this._realtime.request('GET', url, this._apiProtocolVersion, params, body); if (!response.success) { this._logger.error('ChatApi._makeAuthorizedPaginatedRequest(); failed to make request', { url, statusCode: response.statusCode, errorCode: response.errorCode, errorMessage: response.errorMessage, }); throw new Ably.ErrorInfo(response.errorMessage, response.errorCode, response.statusCode); } return response; } }