/* eslint no-unused-vars: "off" */ /* global process */ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import https from 'https'; import WebSocket from 'isomorphic-ws'; import { Channel } from './room'; import { ClientState } from './belo_chat_state'; import { StableWSConnection } from './connection'; import { CheckSignature, DevToken, JWTUserToken } from './signing'; import { TokenManager } from './token_manager'; import { WSConnectionFallback } from './connection_fallback'; import { Campaign } from './campaign'; import { Segment } from './segment'; import { isErrorResponse, isWSFailure } from './errors'; import { addFileToFormData, axiosParamsSerializer, chatCodes, isFunction, isOnline, isOwnUserBaseProperty, normalizeQuerySort, randomId, retryInterval, sleep, } from './utils'; import { APIErrorResponse, APIResponse, AppSettings, AppSettingsAPIResponse, BannedUsersFilters, BannedUsersPaginationOptions, BannedUsersResponse, BannedUsersSort, BanUserOptions, BaseDeviceFields, BlockList, BlockListResponse, CampaignResponse, CampaignData, CampaignFilters, CampaignQueryOptions, ChannelAPIResponse, ChannelData, ChannelFilters, ChannelMute, ChannelOptions, ChannelResponse, ChannelSort, ChannelStateOptions, CheckPushResponse, CheckSNSResponse, CheckSQSResponse, Configs, ConnectAPIResponse, CreateChannelOptions, CreateChannelResponse, CreateCommandOptions, CreateCommandResponse, CreateImportOptions, CreateImportResponse, CreateImportURLResponse, CustomPermissionOptions, DeactivateUsersOptions, DefaultGenerics, DeleteChannelsResponse, DeleteCommandResponse, DeleteUserOptions, Device, EndpointName, ErrorFromResponse, Event, EventHandler, ExportChannelOptions, ExportChannelRequest, ExportChannelResponse, ExportChannelStatusResponse, ExportUsersRequest, ExportUsersResponse, ExtendableGenerics, FlagMessageResponse, FlagReportsFilters, FlagReportsPaginationOptions, FlagReportsResponse, FlagsFilters, FlagsPaginationOptions, FlagsResponse, FlagUserResponse, GetCallTokenResponse, GetChannelTypeResponse, GetCommandResponse, GetImportResponse, GetMessageAPIResponse, GetRateLimitsResponse, QueryThreadsAPIResponse, GetUnreadCountAPIResponse, GetUnreadCountBatchAPIResponse, ListChannelResponse, ListCommandsResponse, ListImportsPaginationOptions, ListImportsResponse, Logger, MarkChannelsReadOptions, Message, MessageFilters, MessageFlagsFilters, MessageFlagsPaginationOptions, MessageFlagsResponse, MessageResponse, Mute, MuteUserOptions, MuteUserResponse, OGAttachment, OwnUserResponse, PartialMessageUpdate, PartialUserUpdate, PermissionAPIResponse, PermissionsAPIResponse, PushProvider, PushProviderConfig, PushProviderID, PushProviderListResponse, PushProviderUpsertResponse, QueryChannelsAPIResponse, QuerySegmentsOptions, ReactionResponse, ReactivateUserOptions, ReactivateUsersOptions, ReservedMessageFields, ReviewFlagReportOptions, ReviewFlagReportResponse, SearchAPIResponse, SearchMessageSortBase, SearchOptions, SearchPayload, SegmentResponse, SegmentData, SegmentType, SendFileAPIResponse, ErmisChatOptions, SyncOptions, SyncResponse, TaskResponse, TaskStatus, TestPushDataInput, TestSNSDataInput, TestSQSDataInput, TokenOrProvider, UnBanUserOptions, UpdateChannelOptions, UpdateChannelResponse, UpdateCommandOptions, UpdateCommandResponse, UpdatedMessage, UpdateMessageAPIResponse, UpdateMessageOptions, UpdateSegmentData, UserCustomEvent, UserFilters, UserOptions, UserResponse, UserSort, GetThreadAPIResponse, PartialThreadUpdate, QueryThreadsOptions, GetThreadOptions, CampaignSort, SegmentTargetsResponse, QuerySegmentTargetsFilter, SortParam, GetMessageOptions, } from './types'; import { InsightMetrics } from './insights'; import { Thread } from './thread'; function isString(x: unknown): x is string { return typeof x === 'string' || x instanceof String; } export class ErmisChat { private static _instance?: unknown | ErmisChat; _user?: OwnUserResponse | UserResponse; activeChannels: { [key: string]: Channel; }; anonymous: boolean; persistUserOnConnectionFailure?: boolean; axiosInstance: AxiosInstance; baseURL?: string; browser: boolean; cleaningIntervalRef?: NodeJS.Timeout; clientID?: string; configs: Configs; key: string; listeners: Record) => void>>; logger: Logger; recoverStateOnReconnect?: boolean; mutedChannels: ChannelMute[]; mutedUsers: Mute[]; node: boolean; options: ErmisChatOptions; secret?: string; setUserPromise: ConnectAPIResponse | null; state: ClientState; tokenManager: TokenManager; user?: OwnUserResponse | UserResponse; userAgent?: string; userID?: string; wsBaseURL?: string; wsConnection: StableWSConnection | null; wsFallback?: WSConnectionFallback; wsPromise: ConnectAPIResponse | null; consecutiveFailures: number; insightMetrics: InsightMetrics; defaultWSTimeoutWithFallback: number; defaultWSTimeout: number; private nextRequestAbortController: AbortController | null = null; constructor(key: string, options?: ErmisChatOptions); constructor(key: string, secret?: string, options?: ErmisChatOptions); constructor(key: string, secretOrOptions?: ErmisChatOptions | string, options?: ErmisChatOptions) { this.key = key; this.listeners = {}; this.state = new ClientState(); this.mutedChannels = []; this.mutedUsers = []; if (secretOrOptions && isString(secretOrOptions)) { this.secret = secretOrOptions; } const inputOptions = options ? options : secretOrOptions && !isString(secretOrOptions) ? secretOrOptions : {}; this.browser = typeof inputOptions.browser !== 'undefined' ? inputOptions.browser : typeof window !== 'undefined'; this.node = !this.browser; this.options = { timeout: 6000, withCredentials: false, warmUp: false, recoverStateOnReconnect: true, ...inputOptions, }; if (this.node && !this.options.httpsAgent) { this.options.httpsAgent = new https.Agent({ keepAlive: true, keepAliveMsecs: 6000, }); } this.axiosInstance = axios.create(this.options); this.setBaseURL(this.options.baseURL || 'https://api.ermis.network'); if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_RUN) { this.setBaseURL('http://localhost:3030'); } if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_HOST) { this.setBaseURL('http://' + process.env.STREAM_LOCAL_TEST_HOST); } this.wsConnection = null; this.wsPromise = null; this.setUserPromise = null; this.activeChannels = {}; this.configs = {}; this.anonymous = false; this.persistUserOnConnectionFailure = this.options?.persistUserOnConnectionFailure; this.tokenManager = new TokenManager(this.secret); this.consecutiveFailures = 0; this.insightMetrics = new InsightMetrics(); this.defaultWSTimeoutWithFallback = 6000; this.defaultWSTimeout = 15000; this.axiosInstance.defaults.paramsSerializer = axiosParamsSerializer; this.logger = isFunction(inputOptions.logger) ? inputOptions.logger : () => null; this.recoverStateOnReconnect = this.options.recoverStateOnReconnect; } public static getInstance( key: string, options?: ErmisChatOptions, ): ErmisChat; public static getInstance( key: string, secret?: string, options?: ErmisChatOptions, ): ErmisChat; public static getInstance( key: string, secretOrOptions?: ErmisChatOptions | string, options?: ErmisChatOptions, ): ErmisChat { if (!ErmisChat._instance) { if (typeof secretOrOptions === 'string') { ErmisChat._instance = new ErmisChat(key, secretOrOptions, options); } else { ErmisChat._instance = new ErmisChat(key, secretOrOptions); } } return ErmisChat._instance as ErmisChat; } devToken(userID: string) { return DevToken(userID); } getAuthType() { return this.anonymous ? 'anonymous' : 'jwt'; } setBaseURL(baseURL: string) { this.baseURL = baseURL; this.wsBaseURL = this.baseURL.replace('http', 'ws').replace(':3030', ':8800'); } _getConnectionID = () => this.wsConnection?.connectionID || this.wsFallback?.connectionID; _hasConnectionID = () => Boolean(this._getConnectionID()); connectUser = async ( user: OwnUserResponse | UserResponse, userTokenOrProvider: TokenOrProvider, ) => { if (!user.id) { throw new Error('The "id" field on the user is missing'); } if (this.userID === user.id && this.setUserPromise) { console.warn( 'Consecutive calls to connectUser is detected, ideally you should only call this function once in your app.', ); return this.setUserPromise; } if (this.userID) { throw new Error( 'Use client.disconnect() before trying to connect as a different user. connectUser was called twice.', ); } if ((this._isUsingServerAuth() || this.node) && !this.options.allowServerSideConnect) { console.warn( 'Please do not use connectUser server side. connectUser impacts MAU and concurrent connection usage and thus your bill. If you have a valid use-case, add "allowServerSideConnect: true" to the client options to disable this warning.', ); } this.userID = user.id; this.anonymous = false; const setTokenPromise = this._setToken(user, userTokenOrProvider); this._setUser(user); const wsPromise = this.openConnection(); this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then((result) => { result[1]; }); try { return await this.setUserPromise; } catch (err) { if (this.persistUserOnConnectionFailure) { this.closeConnection(); } else { this.disconnectUser(); } throw err; } }; setUser = this.connectUser; _setToken = (user: UserResponse, userTokenOrProvider: TokenOrProvider) => this.tokenManager.setTokenOrProvider(userTokenOrProvider, user); _setUser(user: OwnUserResponse | UserResponse) { this.user = user; this.userID = user.id; this._user = { ...user }; } closeConnection = async (timeout?: number) => { if (this.cleaningIntervalRef != null) { clearInterval(this.cleaningIntervalRef); this.cleaningIntervalRef = undefined; } await Promise.all([this.wsConnection?.disconnect(timeout), this.wsFallback?.disconnect(timeout)]); return Promise.resolve(); }; openConnection = async () => { if (!this.userID) { throw Error('User is not set on client, use client.connectUser or client.connectAnonymousUser instead'); } if (this.wsConnection?.isConnecting && this.wsPromise) { this.logger('info', 'client:openConnection() - connection already in progress', { tags: ['connection', 'client'], }); return this.wsPromise; } if ((this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) && this._hasConnectionID()) { this.logger('info', 'client:openConnection() - openConnection called twice, healthy connection already exists', { tags: ['connection', 'client'], }); return Promise.resolve(); } this.clientID = `${this.userID}--${randomId()}`; this.wsPromise = this.connect(); this._startCleaning(); return this.wsPromise; }; _setupConnection = this.openConnection; async updateAppSettings(options: AppSettings) { const apn_config = options.apn_config; if (apn_config?.p12_cert) { options = { ...options, apn_config: { ...apn_config, p12_cert: Buffer.from(apn_config.p12_cert).toString('base64'), team_id: apn_config.team_id || 'HELLO', }, }; } return await this.patch(this.baseURL + '/app', options); } _normalizeDate = (before: Date | string | null): string | null => { if (before instanceof Date) { before = before.toISOString(); } if (before === '') { throw new Error("Don't pass blank string for since, use null instead if resetting the token revoke"); } return before; }; async revokeTokens(before: Date | string | null) { return await this.updateAppSettings({ revoke_tokens_issued_before: this._normalizeDate(before), }); } async revokeUserToken(userID: string, before?: Date | string | null) { return await this.revokeUsersToken([userID], before); } async revokeUsersToken(userIDs: string[], before?: Date | string | null) { if (before === undefined) { before = new Date().toISOString(); } else { before = this._normalizeDate(before); } const users: PartialUserUpdate[] = []; for (const userID of userIDs) { users.push({ id: userID, set: >>{ revoke_tokens_issued_before: before, }, }); } return await this.partialUpdateUsers(users); } async getAppSettings() { return await this.get>(this.baseURL + '/app'); } async testPushSettings(userID: string, data: TestPushDataInput = {}) { return await this.post(this.baseURL + '/check_push', { user_id: userID, ...(data.messageID ? { message_id: data.messageID } : {}), ...(data.apnTemplate ? { apn_template: data.apnTemplate } : {}), ...(data.firebaseTemplate ? { firebase_template: data.firebaseTemplate } : {}), ...(data.firebaseDataTemplate ? { firebase_data_template: data.firebaseDataTemplate } : {}), ...(data.skipDevices ? { skip_devices: true } : {}), ...(data.pushProviderName ? { push_provider_name: data.pushProviderName } : {}), ...(data.pushProviderType ? { push_provider_type: data.pushProviderType } : {}), }); } async testSQSSettings(data: TestSQSDataInput = {}) { return await this.post(this.baseURL + '/check_sqs', data); } async testSNSSettings(data: TestSNSDataInput = {}) { return await this.post(this.baseURL + '/check_sns', data); } disconnectUser = async (timeout?: number) => { this.logger('info', 'client:disconnect() - Disconnecting the client', { tags: ['connection', 'client'], }); delete this.user; delete this._user; delete this.userID; this.anonymous = false; const closePromise = this.closeConnection(timeout); for (const channel of Object.values(this.activeChannels)) { channel._disconnect(); } this.activeChannels = {}; this.state = new ClientState(); setTimeout(this.tokenManager.reset); return closePromise; }; disconnect = this.disconnectUser; connectAnonymousUser = () => { if ((this._isUsingServerAuth() || this.node) && !this.options.allowServerSideConnect) { console.warn( 'Please do not use connectUser server side. connectUser impacts MAU and concurrent connection usage and thus your bill. If you have a valid use-case, add "allowServerSideConnect: true" to the client options to disable this warning.', ); } this.anonymous = true; this.userID = randomId(); const anonymousUser = { id: this.userID, anon: true, } as UserResponse; this._setToken(anonymousUser, ''); this._setUser(anonymousUser); return this._setupConnection(); }; setAnonymousUser = this.connectAnonymousUser; async setGuestUser(user: UserResponse) { let response: { access_token: string; user: UserResponse } | undefined; this.anonymous = true; try { response = await this.post< APIResponse & { access_token: string; user: UserResponse; } >(this.baseURL + '/guest', { user }); } catch (e) { this.anonymous = false; throw e; } this.anonymous = false; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { created_at, updated_at, last_active, online, ...guestUser } = response.user; return await this.connectUser(guestUser as UserResponse, response.access_token); } createToken(userID: string, exp?: number, iat?: number) { if (this.secret == null) { throw Error(`tokens can only be created server-side using the API Secret`); } const extra: { exp?: number; iat?: number } = {}; if (exp) { extra.exp = exp; } if (iat) { extra.iat = iat; } return JWTUserToken(this.secret, userID, extra, {}); } on(callback: EventHandler): { unsubscribe: () => void }; on(eventType: string, callback: EventHandler): { unsubscribe: () => void }; on( callbackOrString: EventHandler | string, callbackOrNothing?: EventHandler, ): { unsubscribe: () => void } { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; const callback = callbackOrNothing ? callbackOrNothing : (callbackOrString as EventHandler); if (!(key in this.listeners)) { this.listeners[key] = []; } this.logger('info', `Attaching listener for ${key} event`, { tags: ['event', 'client'], }); this.listeners[key].push(callback); return { unsubscribe: () => { this.logger('info', `Removing listener for ${key} event`, { tags: ['event', 'client'], }); this.listeners[key] = this.listeners[key].filter((el) => el !== callback); }, }; } off(callback: EventHandler): void; off(eventType: string, callback: EventHandler): void; off(callbackOrString: EventHandler | string, callbackOrNothing?: EventHandler) { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; const callback = callbackOrNothing ? callbackOrNothing : (callbackOrString as EventHandler); if (!(key in this.listeners)) { this.listeners[key] = []; } this.logger('info', `Removing listener for ${key} event`, { tags: ['event', 'client'], }); this.listeners[key] = this.listeners[key].filter((value) => value !== callback); } _logApiRequest( type: string, url: string, data: unknown, config: AxiosRequestConfig & { config?: AxiosRequestConfig & { maxBodyLength?: number }; }, ) { this.logger('info', `client: ${type} - Request - ${url}`, { tags: ['api', 'api_request', 'client'], url, payload: data, config, }); } _logApiResponse(type: string, url: string, response: AxiosResponse) { this.logger('info', `client:${type} - Response - url: ${url} > status ${response.status}`, { tags: ['api', 'api_response', 'client'], url, response, }); } _logApiError(type: string, url: string, error: unknown) { this.logger('error', `client:${type} - Error - url: ${url}`, { tags: ['api', 'api_response', 'client'], url, error, }); } doAxiosRequest = async ( type: string, url: string, data?: unknown, options: AxiosRequestConfig & { config?: AxiosRequestConfig & { maxBodyLength?: number }; } = {}, ): Promise => { const requestConfig = this._enrichAxiosOptions(options); try { let response: AxiosResponse; this._logApiRequest(type, url, data, requestConfig); switch (type) { case 'get': response = await this.axiosInstance.get(url, requestConfig); break; case 'delete': response = await this.axiosInstance.delete(url, requestConfig); break; case 'post': response = await this.axiosInstance.post(url, data, requestConfig); break; case 'postForm': response = await this.axiosInstance.postForm(url, data, requestConfig); break; case 'put': response = await this.axiosInstance.put(url, data, requestConfig); break; case 'patch': response = await this.axiosInstance.patch(url, data, requestConfig); break; case 'options': response = await this.axiosInstance.options(url, requestConfig); break; default: throw new Error('Invalid request type'); } this._logApiResponse(type, url, response); this.consecutiveFailures = 0; return this.handleResponse(response); } catch (e: any) { e.client_request_id = requestConfig.headers?.['x-client-request-id']; this._logApiError(type, url, e); this.consecutiveFailures += 1; if (e.response) { if (e.response.data.code === chatCodes.TOKEN_EXPIRED && !this.tokenManager.isStatic()) { if (this.consecutiveFailures > 1) { await sleep(retryInterval(this.consecutiveFailures)); } this.tokenManager.loadToken(); return await this.doAxiosRequest(type, url, data, options); } return this.handleResponse(e.response); } else { throw e as AxiosError; } } }; get(url: string, params?: AxiosRequestConfig['params']) { return this.doAxiosRequest('get', url, null, { params }); } put(url: string, data?: unknown) { return this.doAxiosRequest('put', url, data); } post(url: string, data?: unknown) { return this.doAxiosRequest('post', url, data); } patch(url: string, data?: unknown) { return this.doAxiosRequest('patch', url, data); } delete(url: string, params?: AxiosRequestConfig['params']) { return this.doAxiosRequest('delete', url, null, { params }); } sendFile( url: string, uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string, user?: UserResponse, ) { const data = addFileToFormData(uri, name, contentType || 'multipart/form-data'); if (user != null) data.append('user', JSON.stringify(user)); return this.doAxiosRequest('postForm', url, data, { headers: data.getHeaders ? data.getHeaders() : {}, config: { timeout: 0, maxContentLength: Infinity, maxBodyLength: Infinity, }, }); } errorFromResponse(response: AxiosResponse): ErrorFromResponse { let err: ErrorFromResponse; err = new ErrorFromResponse(`ErmisChat error HTTP code: ${response.status}`); if (response.data && response.data.code) { err = new Error(`ErmisChat error code ${response.data.code}: ${response.data.message}`); err.code = response.data.code; } err.response = response; err.status = response.status; return err; } handleResponse(response: AxiosResponse) { const data = response.data; if (isErrorResponse(response)) { throw this.errorFromResponse(response); } return data; } dispatchEvent = (event: Event) => { if (!event.received_at) event.received_at = new Date(); const postListenerCallbacks = this._handleClientEvent(event); const cid = event.cid; const channel = cid ? this.activeChannels[cid] : undefined; if (channel) { channel._handleChannelEvent(event); } this._callClientListeners(event); if (channel) { channel._callChannelListeners(event); } postListenerCallbacks.forEach((c) => c()); }; handleEvent = (messageEvent: WebSocket.MessageEvent) => { const jsonString = messageEvent.data as string; const event = JSON.parse(jsonString) as Event; this.dispatchEvent(event); }; _updateMemberWatcherReferences = (user: UserResponse) => { const refMap = this.state.userChannelReferences[user.id] || {}; for (const channelID in refMap) { const channel = this.activeChannels[channelID]; if (channel?.state) { if (channel.state.members[user.id]) { channel.state.members[user.id].user = user; } if (channel.state.watchers[user.id]) { channel.state.watchers[user.id] = user; } if (channel.state.read[user.id]) { channel.state.read[user.id].user = user; } } } }; _updateUserReferences = this._updateMemberWatcherReferences; _updateUserMessageReferences = (user: UserResponse) => { const refMap = this.state.userChannelReferences[user.id] || {}; for (const channelID in refMap) { const channel = this.activeChannels[channelID]; if (!channel) continue; const state = channel.state; state?.updateUserMessages(user); } }; _deleteUserMessageReference = (user: UserResponse, hardDelete = false) => { const refMap = this.state.userChannelReferences[user.id] || {}; for (const channelID in refMap) { const channel = this.activeChannels[channelID]; const state = channel.state; state?.deleteUserMessages(user, hardDelete); } }; _handleUserEvent = (event: Event) => { if (!event.user) { return; } if (event.type === 'user.presence.changed' || event.type === 'user.updated') { if (event.user.id === this.userID) { const user = { ...(this.user || {}) }; const _user = { ...(this._user || {}) }; // Remove deleted properties from user objects. for (const key in this.user) { if (key in event.user || isOwnUserBaseProperty(key)) { continue; } delete user[key]; delete _user[key]; } /** Updating only available properties in _user object. */ for (const key in event.user) { if (_user && key in _user) { _user[key] = event.user[key]; } } // @ts-expect-error this._user = { ..._user }; this.user = { ...user, ...event.user }; } this.state.updateUser(event.user); this._updateMemberWatcherReferences(event.user); } if (event.type === 'user.updated') { this._updateUserMessageReferences(event.user); } if (event.type === 'user.deleted' && event.user.deleted_at && (event.mark_messages_deleted || event.hard_delete)) { this._deleteUserMessageReference(event.user, event.hard_delete); } }; _handleClientEvent(event: Event) { const client = this; const postListenerCallbacks = []; this.logger('info', `client:_handleClientEvent - Received event of type { ${event.type} }`, { tags: ['event', 'client'], event, }); if (event.type === 'user.presence.changed' || event.type === 'user.updated' || event.type === 'user.deleted') { this._handleUserEvent(event); } if (event.type === 'health.check' && event.me) { client.user = event.me; client.state.updateUser(event.me); client.mutedChannels = event.me.channel_mutes; client.mutedUsers = event.me.mutes; } if (event.channel && event.type === 'notification.message_new') { this._addChannelConfig(event.channel); } if (event.type === 'notification.channel_mutes_updated' && event.me?.channel_mutes) { const currentMutedChannelIds: string[] = []; const nextMutedChannelIds: string[] = []; this.mutedChannels.forEach((mute) => mute.channel && currentMutedChannelIds.push(mute.channel.cid)); event.me.channel_mutes.forEach((mute) => mute.channel && nextMutedChannelIds.push(mute.channel.cid)); /** Set the unread count of un-muted channels to 0, which is the behaviour of backend */ currentMutedChannelIds.forEach((cid) => { if (!nextMutedChannelIds.includes(cid) && this.activeChannels[cid]) { this.activeChannels[cid].state.unreadCount = 0; } }); this.mutedChannels = event.me.channel_mutes; } if (event.type === 'notification.mutes_updated' && event.me?.mutes) { this.mutedUsers = event.me.mutes; } if (event.type === 'notification.mark_read' && event.unread_channels === 0) { const activeChannelKeys = Object.keys(this.activeChannels); activeChannelKeys.forEach((activeChannelKey) => (this.activeChannels[activeChannelKey].state.unreadCount = 0)); } if ((event.type === 'channel.deleted' || event.type === 'notification.channel_deleted') && event.cid) { client.state.deleteAllChannelReference(event.cid); this.activeChannels[event.cid]?._disconnect(); postListenerCallbacks.push(() => { if (!event.cid) return; delete this.activeChannels[event.cid]; }); } return postListenerCallbacks; } // _muteStatus(cid: string) { // let muteStatus; // for (let i = 0; i < this.mutedChannels.length; i++) { // const mute = this.mutedChannels[i]; // if (mute.channel?.cid === cid) { // muteStatus = { // muted: mute.expires ? new Date(mute.expires).getTime() > new Date().getTime() : true, // createdAt: mute.created_at ? new Date(mute.created_at) : new Date(), // expiresAt: mute.expires ? new Date(mute.expires) : null, // }; // break; // } // } // if (muteStatus) { // return muteStatus; // } // return { // muted: false, // createdAt: null, // expiresAt: null, // }; // } _callClientListeners = (event: Event) => { const client = this; // gather and call the listeners const listeners: Array<(event: Event) => void> = []; if (client.listeners.all) { listeners.push(...client.listeners.all); } if (client.listeners[event.type]) { listeners.push(...client.listeners[event.type]); } // call the event and send it to the listeners for (const listener of listeners) { listener(event); } }; recoverState = async () => { this.logger('info', `client:recoverState() - Start of recoverState with connectionID ${this._getConnectionID()}`, { tags: ['connection'], }); const cids = Object.keys(this.activeChannels); if (cids.length && this.recoverStateOnReconnect) { this.logger('info', `client:recoverState() - Start the querying of ${cids.length} channels`, { tags: ['connection', 'client'], }); await this.queryChannels( { cid: { $in: cids } } as ChannelFilters, { last_message_at: -1 }, { limit: 30 }, ); this.logger('info', 'client:recoverState() - Querying channels finished', { tags: ['connection', 'client'] }); this.dispatchEvent({ type: 'connection.recovered', } as Event); } else { this.dispatchEvent({ type: 'connection.recovered', } as Event); } this.wsPromise = Promise.resolve(); this.setUserPromise = Promise.resolve(); }; /** * @private */ async connect() { if (!this.userID || !this._user) { throw Error('Call connectUser or connectAnonymousUser before starting the connection'); } if (!this.wsBaseURL) { throw Error('Websocket base url not set'); } if (!this.clientID) { throw Error('clientID is not set'); } if (!this.wsConnection && (this.options.warmUp || this.options.enableInsights)) { // this._sayHi(); } // The StableWSConnection handles all the reconnection logic. if (this.options.wsConnection && this.node) { // Intentionally avoiding adding ts generics on wsConnection in options since its only useful for unit test purpose. ((this.options.wsConnection as unknown) as StableWSConnection).setClient(this); this.wsConnection = (this.options.wsConnection as unknown) as StableWSConnection; } else { // await this.queryChannels( // { cid: { $in: ['team:HELLO', 'Team:HELLO'] } } as ChannelFilters, // { last_message_at: -1 }, // { limit: 30 }, // ); this.wsConnection = new StableWSConnection({ client: this, }); } try { // if fallback is used before, continue using it instead of waiting for WS to fail if (this.wsFallback) { return await this.wsFallback.connect(); } // if WSFallback is enabled, ws connect should timeout faster so fallback can try return await this.wsConnection.connect( this.options.enableWSFallback ? this.defaultWSTimeoutWithFallback : this.defaultWSTimeout, ); } catch (err: any) { // run fallback only if it's WS/Network error and not a normal API error // make sure browser is online before even trying the longpoll if (this.options.enableWSFallback && isWSFailure(err) && isOnline()) { this.logger('info', 'client:connect() - WS failed, fallback to longpoll', { tags: ['connection', 'client'] }); this.dispatchEvent({ type: 'transport.changed', mode: 'longpoll' }); this.wsConnection._destroyCurrentWSConnection(); this.wsConnection.disconnect().then(); // close WS so no retry this.wsFallback = new WSConnectionFallback({ client: this, }); return await this.wsFallback.connect(); } throw err; } } /** * Check the connectivity with server for warmup purpose. * * @private */ _sayHi() { const client_request_id = randomId(); const opts = { headers: { 'x-client-request-id': client_request_id } }; this.doAxiosRequest('get', this.baseURL + '/hi', null, opts).catch((e) => {}); } /** * queryUsers - Query users and watch user presence * * @param {UserFilters} filterConditions MongoDB style filter conditions * @param {UserSort} sort Sort options, for instance [{last_active: -1}]. * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_active: -1}, {created_at: 1}] * @param {UserOptions} options Option object, {presence: true} * * @return {Promise<{ users: Array> }>} User Query Response */ async queryUsers( filterConditions: UserFilters, sort: UserSort = [], options: UserOptions = {}, ) { const defaultOptions = { presence: false, }; // Make sure we wait for the connect promise if there is a pending one await this.wsPromise; if (!this._hasConnectionID()) { defaultOptions.presence = false; } // Return a list of users const data = await this.get> }>( this.baseURL + '/users', { payload: { filter_conditions: filterConditions, sort: normalizeQuerySort(sort), ...defaultOptions, ...options, }, }, ); this.state.updateUsers(data.users); return data; } /** * queryBannedUsers - Query user bans * * @param {BannedUsersFilters} filterConditions MongoDB style filter conditions * @param {BannedUsersSort} sort Sort options [{created_at: 1}]. * @param {BannedUsersPaginationOptions} options Option object, {limit: 10, offset:0, exclude_expired_bans: true} * * @return {Promise>} Ban Query Response */ async queryBannedUsers( filterConditions: BannedUsersFilters = {}, sort: BannedUsersSort = [], options: BannedUsersPaginationOptions = {}, ) { // Return a list of user bans return await this.get>(this.baseURL + '/query_banned_users', { payload: { filter_conditions: filterConditions, sort: normalizeQuerySort(sort), ...options, }, }); } /** * queryMessageFlags - Query message flags * * @param {MessageFlagsFilters} filterConditions MongoDB style filter conditions * @param {MessageFlagsPaginationOptions} options Option object, {limit: 10, offset:0} * * @return {Promise>} Message Flags Response */ async queryMessageFlags(filterConditions: MessageFlagsFilters = {}, options: MessageFlagsPaginationOptions = {}) { // Return a list of message flags return await this.get>(this.baseURL + '/moderation/flags/message', { payload: { filter_conditions: filterConditions, ...options }, }); } /** * queryChannels - Query channels * * @param {ChannelFilters} filterConditions object MongoDB style filters * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}. * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}] * @param {ChannelOptions} [options] Options object * @param {ChannelStateOptions} [stateOptions] State options object. These options will only be used for state management and won't be sent in the request. * - stateOptions.skipInitialization - Skips the initialization of the state for the channels matching the ids in the list. * * @return {Promise<{ channels: Array>}> } search channels response */ async queryChannels( filterConditions: ChannelFilters, sort: ChannelSort = [], options: ChannelOptions = {}, stateOptions: ChannelStateOptions = {}, ) { const defaultOptions: ChannelOptions = { state: true, watch: true, presence: false, }; // Make sure we wait for the connect promise if there is a pending one await this.wsPromise; if (!this._hasConnectionID()) { defaultOptions.watch = false; } // Return a list of channels const payload = { filter_conditions: filterConditions, sort: normalizeQuerySort(sort), ...defaultOptions, ...options, }; const data = await this.post>(this.baseURL + '/channels', payload); this.dispatchEvent({ type: 'channels.queried', queriedChannels: { channels: data.channels, isLatestMessageSet: true, }, }); return this.hydrateActiveChannels(data.channels, stateOptions); } async startCall(payload: any) { return this.post>(this.baseURL + '/signal', payload); } async queryFriends() { return await this.post(this.baseURL + '/contacts/list', { headers: { 'Content-Type': 'application/json', }, }); } hydrateActiveChannels( channelsFromApi: ChannelAPIResponse[] = [], stateOptions: ChannelStateOptions = {}, ) { const { skipInitialization, offlineMode = false } = stateOptions; for (const channelState of channelsFromApi) { this._addChannelConfig(channelState.channel); } const channels: Channel[] = []; for (const channelState of channelsFromApi) { const c = this.channel(channelState.channel.type, channelState.channel.id); c.data = channelState.channel; c.offlineMode = offlineMode; c.initialized = !offlineMode; if (skipInitialization === undefined) { c._initializeState(channelState, 'latest'); } else if (!skipInitialization.includes(channelState.channel.id)) { c.state.clearMessages(); c._initializeState(channelState, 'latest'); } channels.push(c); } return channels; } /** * search - Query messages * * @param {ChannelFilters} filterConditions MongoDB style filter conditions * @param {MessageFilters | string} query search query or object MongoDB style filters * @param {SearchOptions} [options] Option object, {user_id: 'tommaso'} * * @return {Promise>} search messages response */ async search( filterConditions: ChannelFilters, query: string | MessageFilters, options: SearchOptions = {}, ) { if (options.offset && options.next) { throw Error(`Cannot specify offset with next`); } const payload: SearchPayload = { filter_conditions: filterConditions, ...options, sort: options.sort ? normalizeQuerySort>(options.sort) : undefined, }; if (typeof query === 'string') { payload.query = query; } else if (typeof query === 'object') { payload.message_filter_conditions = query; } else { throw Error(`Invalid type ${typeof query} for query parameter`); } // Make sure we wait for the connect promise if there is a pending one await this.wsPromise; return await this.get>(this.baseURL + '/search', { payload }); } /** * setLocalDevice - Set the device info for the current client(device) that will be sent via WS connection automatically * * @param {BaseDeviceFields} device the device object * @param {string} device.id device id * @param {string} device.push_provider the push provider * */ setLocalDevice(device: BaseDeviceFields) { if ( (this.wsConnection?.isConnecting && this.wsPromise) || ((this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) && this._hasConnectionID()) ) { throw new Error('you can only set device before opening a websocket connection'); } this.options.device = device; } /** * addDevice - Adds a push device for a user. * * @param {string} id the device id * @param {PushProvider} push_provider the push provider * @param {string} [userID] the user id (defaults to current user) * @param {string} [push_provider_name] user provided push provider name for multi bundle support * */ async addDevice(id: string, push_provider: PushProvider, userID?: string, push_provider_name?: string) { return await this.post(this.baseURL + '/devices', { id, push_provider, ...(userID != null ? { user_id: userID } : {}), ...(push_provider_name != null ? { push_provider_name } : {}), }); } /** * getDevices - Returns the devices associated with a current user * * @param {string} [userID] User ID. Only works on serverside * * @return {Device[]} Array of devices */ async getDevices(userID?: string) { return await this.get[] }>( this.baseURL + '/devices', userID ? { user_id: userID } : {}, ); } /** * getUnreadCount - Returns unread counts for a single user * * @param {string} [userID] User ID. * * @return {} */ async getUnreadCount(userID?: string) { return await this.get(this.baseURL + '/unread', userID ? { user_id: userID } : {}); } /** * getUnreadCountBatch - Returns unread counts for multiple users at once. Only works server side. * * @param {string[]} [userIDs] List of user IDs to fetch unread counts for. * * @return {} */ async getUnreadCountBatch(userIDs: string[]) { return await this.post(this.baseURL + '/unread_batch', { user_ids: userIDs }); } /** * removeDevice - Removes the device with the given id. Clientside users can only delete their own devices * * @param {string} id The device id * @param {string} [userID] The user id. Only specify this for serverside requests * */ async removeDevice(id: string, userID?: string) { return await this.delete(this.baseURL + '/devices', { id, ...(userID ? { user_id: userID } : {}), }); } /** * getRateLimits - Returns the rate limits quota and usage for the current app, possibly filter for a specific platform and/or endpoints. * Only available server-side. * * @param {object} [params] The params for the call. If none of the params are set, all limits for all platforms are returned. * @returns {Promise} */ async getRateLimits(params?: { android?: boolean; endpoints?: EndpointName[]; ios?: boolean; serverSide?: boolean; web?: boolean; }) { const { serverSide, web, android, ios, endpoints } = params || {}; return this.get(this.baseURL + '/rate_limits', { server_side: serverSide, web, android, ios, endpoints: endpoints ? endpoints.join(',') : undefined, }); } _addChannelConfig({ cid, config }: ChannelResponse) { this.configs[cid] = config; } /** * channel - Returns a new channel with the given type, id and custom data * * If you want to create a unique conversation between 2 or more users; you can leave out the ID parameter and provide the list of members. * Make sure to await channel.create() or channel.watch() before accessing channel functions: * ie. channel = client.channel("messaging", {members: ["tommaso", "thierry"]}) * await channel.create() to assign an ID to channel * * @param {string} channelType The channel type * @param {string | ChannelData | null} [channelIDOrCustom] The channel ID, you can leave this out if you want to create a conversation channel * @param {object} [custom] Custom data to attach to the channel * * @return {channel} The channel object, initialize it using channel.watch() */ channel( channelType: string, channelID?: string | null, custom?: ChannelData, ): Channel; channel(channelType: string, custom?: ChannelData): Channel; channel( channelType: string, channelIDOrCustom?: string | ChannelData | null, custom: ChannelData = {} as ChannelData, ) { if (!this.userID && !this._isUsingServerAuth()) { throw Error('Call connectUser or connectAnonymousUser before creating a channel'); } if (~channelType.indexOf(':')) { throw Error(`Invalid channel group ${channelType}, can't contain the : character`); } // support channel("messaging", {options}) if (channelIDOrCustom && typeof channelIDOrCustom === 'object') { return this.getChannelByMembers(channelType, channelIDOrCustom); } // // support channel("messaging", undefined, {options}) if (!channelIDOrCustom && typeof custom === 'object' && custom.members?.length) { return this.getChannelByMembers(channelType, custom); } // support channel("messaging", null, {options}) // support channel("messaging", undefined, {options}) // support channel("messaging", "", {options}) if (!channelIDOrCustom) { return new Channel(this, channelType, undefined, custom); } return this.getChannelById(channelType, channelIDOrCustom, custom); } /** * It's a helper method for `client.channel()` method, used to create unique conversation or * channel based on member list instead of id. * * If the channel already exists in `activeChannels` list, then we simply return it, since that * means the same channel was already requested or created. * * Otherwise we create a new instance of Channel class and return it. * * @private * * @param {string} channelType The channel type * @param {object} [custom] Custom data to attach to the channel * * @return {channel} The channel object, initialize it using channel.watch() */ getChannelByMembers = (channelType: string, custom: ChannelData) => { // Check if the channel already exists. // Only allow 1 channel object per cid const membersStr = [...(custom.members || [])].sort().join(','); const tempCid = `${channelType}:!members-${membersStr}`; if (!membersStr) { throw Error('Please specify atleast one member when creating unique conversation'); } // channel could exist in `activeChannels` list with either one of the following two keys: // 1. cid - Which gets set on channel only after calling channel.query or channel.watch or channel.create // 2. Sorted membersStr - E.g., "messaging:amin,vishal" OR "messaging:amin,jaap,tom" // This is set when you create a channel, but haven't queried yet. After query, // we will replace it with `cid` for (const key in this.activeChannels) { const channel = this.activeChannels[key]; if (channel.disconnected) { continue; } if (key === tempCid) { return channel; } if (key.indexOf(`${channelType}:!members-`) === 0) { const membersStrInExistingChannel = Object.keys(channel.state.members).sort().join(','); if (membersStrInExistingChannel === membersStr) { return channel; } } } const channel = new Channel(this, channelType, undefined, custom); // For the time being set the key as membersStr, since we don't know the cid yet. // In channel.query, we will replace it with 'cid'. this.activeChannels[tempCid] = channel; return channel; }; /** * Its a helper method for `client.channel()` method, used to channel given the id of channel. * * If the channel already exists in `activeChannels` list, then we simply return it, since that * means the same channel was already requested or created. * * Otherwise we create a new instance of Channel class and return it. * * @private * * @param {string} channelType The channel type * @param {string} [channelID] The channel ID * @param {object} [custom] Custom data to attach to the channel * * @return {channel} The channel object, initialize it using channel.watch() */ getChannelById = (channelType: string, channelID: string, custom: ChannelData) => { // if (typeof channelID === 'string' && ~channelID.indexOf(':')) { // throw Error(`Invalid channel id ${channelID}, can't contain the : character`); // } // only allow 1 channel object per cid const cid = `${channelType}:${channelID}`; if (cid in this.activeChannels && !this.activeChannels[cid].disconnected) { const channel = this.activeChannels[cid]; if (Object.keys(custom).length > 0) { channel.data = custom; channel._data = custom; } return channel; } const channel = new Channel(this, channelType, channelID, custom); this.activeChannels[channel.cid] = channel; return channel; }; /** * partialUpdateUser - Update the given user object * * @param {PartialUserUpdate} partialUserObject which should contain id and any of "set" or "unset" params; * example: {id: "user1", set:{field: value}, unset:["field2"]} * * @return {Promise<{ users: { [key: string]: UserResponse } }>} list of updated users */ async partialUpdateUser(partialUserObject: PartialUserUpdate) { return await this.partialUpdateUsers([partialUserObject]); } /** * upsertUsers - Batch upsert the list of users * * @param {UserResponse[]} users list of users * * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ async upsertUsers(users: UserResponse[]) { const userMap: { [key: string]: UserResponse } = {}; for (const userObject of users) { if (!userObject.id) { throw Error('User ID is required when updating a user'); } userMap[userObject.id] = userObject; } return await this.post< APIResponse & { users: { [key: string]: UserResponse }; } >(this.baseURL + '/users', { users: userMap }); } /** * @deprecated Please use upsertUsers() function instead. * * updateUsers - Batch update the list of users * * @param {UserResponse[]} users list of users * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ updateUsers = this.upsertUsers; /** * upsertUser - Update or Create the given user object * * @param {UserResponse} userObject user object, the only required field is the user id. IE {id: "myuser"} is valid * * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ upsertUser(userObject: UserResponse) { return this.upsertUsers([userObject]); } /** * @deprecated Please use upsertUser() function instead. * * updateUser - Update or Create the given user object * * @param {UserResponse} userObject user object, the only required field is the user id. IE {id: "myuser"} is valid * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ updateUser = this.upsertUser; /** * partialUpdateUsers - Batch partial update of users * * @param {PartialUserUpdate[]} users list of partial update requests * * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ async partialUpdateUsers(users: PartialUserUpdate[]) { for (const userObject of users) { if (!userObject.id) { throw Error('User ID is required when updating a user'); } } return await this.patch< APIResponse & { users: { [key: string]: UserResponse }; } >(this.baseURL + '/users', { users }); } async deleteUser( userID: string, params?: { delete_conversation_channels?: boolean; hard_delete?: boolean; mark_messages_deleted?: boolean; }, ) { return await this.delete< APIResponse & { user: UserResponse } & { task_id?: string; } >(this.baseURL + `/users/${userID}`, params); } /** * restoreUsers - Restore soft deleted users * * @param {string[]} user_ids which users to restore * * @return {APIResponse} An API response */ async restoreUsers(user_ids: string[]) { return await this.post(this.baseURL + `/users/restore`, { user_ids, }); } /** * reactivateUser - Reactivate one user * * @param {string} userID which user to reactivate * @param {ReactivateUserOptions} [options] * * @return {UserResponse} Reactivated user */ async reactivateUser(userID: string, options?: ReactivateUserOptions) { return await this.post }>( this.baseURL + `/users/${userID}/reactivate`, { ...options }, ); } /** * reactivateUsers - Reactivate many users asynchronously * * @param {string[]} user_ids which users to reactivate * @param {ReactivateUsersOptions} [options] * * @return {TaskResponse} A task ID */ async reactivateUsers(user_ids: string[], options?: ReactivateUsersOptions) { return await this.post(this.baseURL + `/users/reactivate`, { user_ids, ...options }); } /** * deactivateUser - Deactivate one user * * @param {string} userID which user to deactivate * @param {DeactivateUsersOptions} [options] * * @return {UserResponse} Deactivated user */ async deactivateUser(userID: string, options?: DeactivateUsersOptions) { return await this.post }>( this.baseURL + `/users/${userID}/deactivate`, { ...options }, ); } /** * deactivateUsers - Deactivate many users asynchronously * * @param {string[]} user_ids which users to deactivate * @param {DeactivateUsersOptions} [options] * * @return {TaskResponse} A task ID */ async deactivateUsers(user_ids: string[], options?: DeactivateUsersOptions) { return await this.post(this.baseURL + `/users/deactivate`, { user_ids, ...options }); } async exportUser(userID: string, options?: Record) { return await this.get< APIResponse & { messages: MessageResponse[]; reactions: ReactionResponse[]; user: UserResponse; } >(this.baseURL + `/users/${userID}/export`, { ...options }); } /** banUser - bans a user from all channels * * @param {string} targetUserID * @param {BanUserOptions} [options] * @returns {Promise} */ async banUser(targetUserID: string, options?: BanUserOptions) { return await this.post(this.baseURL + '/moderation/ban', { target_user_id: targetUserID, ...options, }); } /** unbanUser - revoke global ban for a user * * @param {string} targetUserID * @param {UnBanUserOptions} [options] * @returns {Promise} */ async unbanUser(targetUserID: string, options?: UnBanUserOptions) { return await this.delete(this.baseURL + '/moderation/ban', { target_user_id: targetUserID, ...options, }); } /** shadowBan - shadow bans a user from all channels * * @param {string} targetUserID * @param {BanUserOptions} [options] * @returns {Promise} */ async shadowBan(targetUserID: string, options?: BanUserOptions) { return await this.banUser(targetUserID, { shadow: true, ...options, }); } /** removeShadowBan - revoke global shadow ban for a user * * @param {string} targetUserID * @param {UnBanUserOptions} [options] * @returns {Promise} */ async removeShadowBan(targetUserID: string, options?: UnBanUserOptions) { return await this.unbanUser(targetUserID, { shadow: true, ...options, }); } /** muteUser - mutes a user * * @param {string} targetID * @param {string} [userID] Only used with serverside auth * @param {MuteUserOptions} [options] * @returns {Promise>} */ async muteUser(targetID: string, userID?: string, options: MuteUserOptions = {}) { return await this.post>(this.baseURL + '/moderation/mute', { target_id: targetID, ...(userID ? { user_id: userID } : {}), ...options, }); } /** unmuteUser - unmutes a user * * @param {string} targetID * @param {string} [currentUserID] Only used with serverside auth * @returns {Promise} */ async unmuteUser(targetID: string, currentUserID?: string) { return await this.post(this.baseURL + '/moderation/unmute', { target_id: targetID, ...(currentUserID ? { user_id: currentUserID } : {}), }); } /** userMuteStatus - check if a user is muted or not, can be used after connectUser() is called * * @param {string} targetID * @returns {boolean} */ // userMuteStatus(targetID: string) { // if (!this.user || !this.wsPromise) { // throw new Error('Make sure to await connectUser() first.'); // } // for (let i = 0; i < this.mutedUsers.length; i += 1) { // if (this.mutedUsers[i].target.id === targetID) return true; // } // return false; // } /** * flagMessage - flag a message * @param {string} targetMessageID * @param {string} [options.user_id] currentUserID, only used with serverside auth * @returns {Promise} */ async flagMessage(targetMessageID: string, options: { user_id?: string } = {}) { return await this.post>(this.baseURL + '/moderation/flag', { target_message_id: targetMessageID, ...options, }); } /** * flagUser - flag a user * @param {string} targetID * @param {string} [options.user_id] currentUserID, only used with serverside auth * @returns {Promise} */ async flagUser(targetID: string, options: { user_id?: string } = {}) { return await this.post>(this.baseURL + '/moderation/flag', { target_user_id: targetID, ...options, }); } /** * unflagMessage - unflag a message * @param {string} targetMessageID * @param {string} [options.user_id] currentUserID, only used with serverside auth * @returns {Promise} */ async unflagMessage(targetMessageID: string, options: { user_id?: string } = {}) { return await this.post>(this.baseURL + '/moderation/unflag', { target_message_id: targetMessageID, ...options, }); } /** * unflagUser - unflag a user * @param {string} targetID * @param {string} [options.user_id] currentUserID, only used with serverside auth * @returns {Promise} */ async unflagUser(targetID: string, options: { user_id?: string } = {}) { return await this.post>(this.baseURL + '/moderation/unflag', { target_user_id: targetID, ...options, }); } /** * getCallToken - retrieves the auth token needed to join a call * * @param {string} callID * @param {object} options * @returns {Promise} */ async getCallToken(callID: string, options: { user_id?: string } = {}) { return await this.post(this.baseURL + `/calls/${callID}`, { ...options }); } /** * _queryFlags - Query flags. * * Note: Do not use this. * It is present for internal usage only. * This function can, and will, break and/or be removed at any point in time. * * @private * @param {FlagsFilters} filterConditions MongoDB style filter conditions * @param {FlagsPaginationOptions} options Option object, {limit: 10, offset:0} * * @return {Promise>} Flags Response */ async _queryFlags(filterConditions: FlagsFilters = {}, options: FlagsPaginationOptions = {}) { // Return a list of flags return await this.post>(this.baseURL + '/moderation/flags', { filter_conditions: filterConditions, ...options, }); } /** * _queryFlagReports - Query flag reports. * * Note: Do not use this. * It is present for internal usage only. * This function can, and will, break and/or be removed at any point in time. * * @private * @param {FlagReportsFilters} filterConditions MongoDB style filter conditions * @param {FlagReportsPaginationOptions} options Option object, {limit: 10, offset:0} * * @return {Promise>} Flag Reports Response */ async _queryFlagReports(filterConditions: FlagReportsFilters = {}, options: FlagReportsPaginationOptions = {}) { // Return a list of message flags return await this.post>(this.baseURL + '/moderation/reports', { filter_conditions: filterConditions, ...options, }); } /** * _reviewFlagReport - review flag report * * Note: Do not use this. * It is present for internal usage only. * This function can, and will, break and/or be removed at any point in time. * * @private * @param {string} [id] flag report to review * @param {string} [reviewResult] flag report review result * @param {string} [options.user_id] currentUserID, only used with serverside auth * @param {string} [options.review_details] custom information about review result * @returns {Promise>} */ async _reviewFlagReport(id: string, reviewResult: string, options: ReviewFlagReportOptions = {}) { return await this.patch>(this.baseURL + `/moderation/reports/${id}`, { review_result: reviewResult, ...options, }); } /** * unblockMessage - unblocks message blocked by automod * * * @param {string} targetMessageID * @param {string} [options.user_id] currentUserID, only used with serverside auth * @returns {Promise} */ async unblockMessage(targetMessageID: string, options: { user_id?: string } = {}) { return await this.post(this.baseURL + '/moderation/unblock_message', { target_message_id: targetMessageID, ...options, }); } // alias for backwards compatibility _unblockMessage = this.unblockMessage; /** * @deprecated use markChannelsRead instead * * markAllRead - marks all channels for this user as read * @param {MarkAllReadOptions} [data] * * @return {Promise} */ markAllRead = this.markChannelsRead; /** * markChannelsRead - marks channels read - * it accepts a map of cid:messageid pairs, if messageid is empty, the whole channel will be marked as read * * @param {MarkChannelsReadOptions } [data] * * @return {Promise} */ async markChannelsRead(data: MarkChannelsReadOptions = {}) { await this.post(this.baseURL + '/channels/read', { ...data }); } createCommand(data: CreateCommandOptions) { return this.post>(this.baseURL + '/commands', data); } getCommand(name: string) { return this.get>(this.baseURL + `/commands/${name}`); } updateCommand(name: string, data: UpdateCommandOptions) { return this.put>(this.baseURL + `/commands/${name}`, data); } deleteCommand(name: string) { return this.delete>(this.baseURL + `/commands/${name}`); } listCommands() { return this.get>(this.baseURL + `/commands`); } createChannelType(data: CreateChannelOptions) { const channelData = Object.assign({}, { commands: ['all'] }, data); return this.post>(this.baseURL + '/channeltypes', channelData); } getChannelType(channelType: string) { return this.get>(this.baseURL + `/channeltypes/${channelType}`); } updateChannelType(channelType: string, data: UpdateChannelOptions) { return this.put>(this.baseURL + `/channeltypes/${channelType}`, data); } deleteChannelType(channelType: string) { return this.delete(this.baseURL + `/channeltypes/${channelType}`); } listChannelTypes() { return this.get>(this.baseURL + `/channeltypes`); } /** * translateMessage - adds the translation to the message * * @param {string} messageId * @param {string} language * * @return {MessageResponse} Response that includes the message */ async translateMessage(messageId: string, language: string) { return await this.post>( this.baseURL + `/messages/${messageId}/translate`, { language }, ); } /** * _normalizeExpiration - transforms expiration value into ISO string * @param {undefined|null|number|string|Date} timeoutOrExpirationDate expiration date or timeout. Use number type to set timeout in seconds, string or Date to set exact expiration date */ _normalizeExpiration(timeoutOrExpirationDate?: null | number | string | Date) { let pinExpires: null | string = null; if (typeof timeoutOrExpirationDate === 'number') { const now = new Date(); now.setSeconds(now.getSeconds() + timeoutOrExpirationDate); pinExpires = now.toISOString(); } else if (isString(timeoutOrExpirationDate)) { pinExpires = timeoutOrExpirationDate; } else if (timeoutOrExpirationDate instanceof Date) { pinExpires = timeoutOrExpirationDate.toISOString(); } return pinExpires; } /** * _messageId - extracts string message id from either message object or message id * @param {string | { id: string }} messageOrMessageId message object or message id * @param {string} errorText error message to report in case of message id absence */ _validateAndGetMessageId(messageOrMessageId: string | { id: string }, errorText: string) { let messageId: string; if (typeof messageOrMessageId === 'string') { messageId = messageOrMessageId; } else { if (!messageOrMessageId.id) { throw Error(errorText); } messageId = messageOrMessageId.id; } return messageId; } /** * pinMessage - pins the message * @param {string | { id: string }} messageOrMessageId message object or message id * @param {undefined|null|number|string|Date} timeoutOrExpirationDate expiration date or timeout. Use number type to set timeout in seconds, string or Date to set exact expiration date * @param {undefined|string | { id: string }} [pinnedBy] who will appear as a user who pinned a message. Only for server-side use. Provide `undefined` when pinning message client-side * @param {undefined|number|string|Date} pinnedAt date when message should be pinned. It affects the order of pinned messages. Use negative number to set relative time in the past, string or Date to set exact date of pin */ pinMessage( messageOrMessageId: string | { id: string }, timeoutOrExpirationDate?: null | number | string | Date, pinnedBy?: string | { id: string }, pinnedAt?: number | string | Date, ) { const messageId = this._validateAndGetMessageId( messageOrMessageId, 'Please specify the message id when calling unpinMessage', ); return this.partialUpdateMessage( messageId, ({ set: { pinned: true, pin_expires: this._normalizeExpiration(timeoutOrExpirationDate), pinned_at: this._normalizeExpiration(pinnedAt), }, } as unknown) as PartialMessageUpdate, pinnedBy, ); } /** * unpinMessage - unpins the message that was previously pinned * @param {string | { id: string }} messageOrMessageId message object or message id * @param {string | { id: string }} [userId] */ unpinMessage(messageOrMessageId: string | { id: string }, userId?: string | { id: string }) { const messageId = this._validateAndGetMessageId( messageOrMessageId, 'Please specify the message id when calling unpinMessage', ); return this.partialUpdateMessage( messageId, ({ set: { pinned: false }, } as unknown) as PartialMessageUpdate, userId, ); } /** * updateMessage - Update the given message * * @param {Omit, 'mentioned_users'> & { mentioned_users?: string[] }} message object, id needs to be specified * @param {string | { id: string }} [userId] * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message * * @return {{ message: MessageResponse }} Response that includes the message */ async updateMessage( message: UpdatedMessage, userId?: string | { id: string }, options?: UpdateMessageOptions, ) { if (!message.id) { throw Error('Please specify the message id when calling updateMessage'); } const clonedMessage: Message = Object.assign({}, message); delete clonedMessage.id; const reservedMessageFields: Array = [ 'command', 'created_at', 'html', 'latest_reactions', 'own_reactions', 'quoted_message', 'reaction_counts', 'reply_count', 'type', 'updated_at', 'user', '__html', ]; reservedMessageFields.forEach(function (item) { if (clonedMessage[item] != null) { delete clonedMessage[item]; } }); if (userId != null) { if (isString(userId)) { clonedMessage.user_id = userId; } else { clonedMessage.user = { id: userId.id, } as UserResponse; } } /** * Server always expects mentioned_users to be array of string. We are adding extra check, just in case * SDK missed this conversion. */ if (Array.isArray(clonedMessage.mentioned_users) && !isString(clonedMessage.mentioned_users[0])) { clonedMessage.mentioned_users = clonedMessage.mentioned_users.map((mu) => ((mu as unknown) as UserResponse).id); } return await this.post>(this.baseURL + `/messages/${message.id}`, { message: clonedMessage, ...options, }); } /** * partialUpdateMessage - Update the given message id while retaining additional properties * * @param {string} id the message id * * @param {PartialUpdateMessage} partialMessageObject which should contain id and any of "set" or "unset" params; * example: {id: "user1", set:{text: "hi"}, unset:["color"]} * @param {string | { id: string }} [userId] * * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message * * @return {{ message: MessageResponse }} Response that includes the updated message */ async partialUpdateMessage( id: string, partialMessageObject: PartialMessageUpdate, userId?: string | { id: string }, options?: UpdateMessageOptions, ) { if (!id) { throw Error('Please specify the message id when calling partialUpdateMessage'); } let user = userId; if (userId != null && isString(userId)) { user = { id: userId }; } return await this.put>(this.baseURL + `/messages/${id}`, { ...partialMessageObject, ...options, user, }); } async deleteMessage(messageID: string, hardDelete?: boolean) { let params = {}; if (hardDelete) { params = { hard: true }; } return await this.delete }>( this.baseURL + `/messages/${messageID}`, params, ); } /** * undeleteMessage - Undelete a message * * undeletes a message that was previous soft deleted. Hard deleted messages * cannot be undeleted. This is only allowed to be called from server-side * clients. * * @param {string} messageID The id of the message to undelete * @param {string} userID The id of the user who undeleted the message * * @return {{ message: MessageResponse }} Response that includes the message */ async undeleteMessage(messageID: string, userID: string) { return await this.post }>( this.baseURL + `/messages/${messageID}/undelete`, { undeleted_by: userID }, ); } async getMessage(messageID: string, options?: GetMessageOptions) { return await this.get>( this.baseURL + `/messages/${encodeURIComponent(messageID)}`, { ...options }, ); } /** * queryThreads - returns the list of threads of current user. * * @param {QueryThreadsOptions} options Options object for pagination and limiting the participants and replies. * @param {number} options.limit Limits the number of threads to be returned. * @param {boolean} options.watch Subscribes the user to the channels of the threads. * @param {number} options.participant_limit Limits the number of participants returned per threads. * @param {number} options.reply_limit Limits the number of replies returned per threads. * * @returns {{ threads: Thread[], next: string }} Returns the list of threads and the next cursor. */ async queryThreads(options?: QueryThreadsOptions) { const opts = { limit: 10, participant_limit: 10, reply_limit: 3, watch: true, ...options, }; const res = await this.post>(this.baseURL + `/threads`, opts); return { threads: res.threads.map((thread) => new Thread(this, thread)), next: res.next, }; } /** * getThread - returns the thread of a message by its id. * * @param {string} messageId The message id * @param {GetThreadOptions} options Options object for pagination and limiting the participants and replies. * @param {boolean} options.watch Subscribes the user to the channel of the thread. * @param {number} options.participant_limit Limits the number of participants returned per threads. * @param {number} options.reply_limit Limits the number of replies returned per threads. * * @returns {Thread} Returns the thread. */ async getThread(messageId: string, options: GetThreadOptions = {}) { if (!messageId) { throw Error('Please specify the message id when calling partialUpdateThread'); } const opts = { participant_limit: 100, reply_limit: 3, watch: true, ...options, }; const res = await this.get>(this.baseURL + `/threads/${messageId}`, opts); return new Thread(this, res.thread); } /** * partialUpdateThread - updates the given thread * * @param {string} messageId The id of the thread message which needs to be updated. * @param {PartialThreadUpdate} partialThreadObject should contain "set" or "unset" params for any of the thread's non-reserved fields. * * @returns {GetThreadAPIResponse} Returns the updated thread. */ async partialUpdateThread(messageId: string, partialThreadObject: PartialThreadUpdate) { if (!messageId) { throw Error('Please specify the message id when calling partialUpdateThread'); } // check for reserved fields from ThreadResponse type within partialThreadObject's set and unset. // Throw error if any of the reserved field is found. const reservedThreadFields = [ 'created_at', 'id', 'last_message_at', 'type', 'updated_at', 'user', 'reply_count', 'participants', 'channel', ]; for (const key in { ...partialThreadObject.set, ...partialThreadObject.unset }) { if (reservedThreadFields.includes(key)) { throw Error( `You cannot set ${key} field on Thread object. ${key} is reserved for server-side use. Please omit ${key} from your set object.`, ); } } return await this.patch>( this.baseURL + `/threads/${messageId}`, partialThreadObject, ); } getUserAgent() { return ( this.userAgent || `ermis-js-sdk-javascript-client-${this.node ? 'node' : 'browser'}-${process.env.PKG_VERSION}` ); } setUserAgent(userAgent: string) { this.userAgent = userAgent; } /** * _isUsingServerAuth - Returns true if we're using server side auth */ _isUsingServerAuth = () => !!this.secret; _enrichAxiosOptions( options: AxiosRequestConfig & { config?: AxiosRequestConfig } = { params: {}, headers: {}, config: {}, }, ): AxiosRequestConfig { const token = this._getToken(); const authorization = token ? { Authorization: token } : undefined; let signal: AbortSignal | null = null; if (this.nextRequestAbortController !== null) { signal = this.nextRequestAbortController.signal; this.nextRequestAbortController = null; } if (!options.headers?.['x-client-request-id']) { options.headers = { ...options.headers, 'x-client-request-id': randomId(), }; } const { params: axiosRequestConfigParams, headers: axiosRequestConfigHeaders, ...axiosRequestConfigRest } = this.options.axiosRequestConfig || {}; return { params: { user_id: this.userID, connection_id: this._getConnectionID(), api_key: this.key, ...options.params, ...(axiosRequestConfigParams || {}), }, headers: { ...authorization, 'stream-auth-type': this.getAuthType(), 'X-Stream-Client': this.getUserAgent(), ...options.headers, ...(axiosRequestConfigHeaders || {}), }, ...(signal ? { signal } : {}), ...options.config, ...(axiosRequestConfigRest || {}), }; } _getToken() { if (!this.tokenManager || this.anonymous) return null; return this.tokenManager.getToken(); } _startCleaning() { const that = this; if (this.cleaningIntervalRef != null) { return; } this.cleaningIntervalRef = setInterval(() => { // call clean on the channel, used for calling the stop.typing event etc. for (const channel of Object.values(that.activeChannels)) { channel.clean(); } }, 500); } /** * encode ws url payload * @private * @returns json string */ _buildWSPayload = (client_request_id?: string) => { return JSON.stringify({ user_id: this.userID, user_details: this._user, device: this.options.device, client_request_id, }); }; /** * checks signature of a request * @param {string | Buffer} rawBody * @param {string} signature from HTTP header * @returns {boolean} */ verifyWebhook(requestBody: string | Buffer, xSignature: string) { return !!this.secret && CheckSignature(requestBody, this.secret, xSignature); } /** getPermission - gets the definition for a permission * * @param {string} name * @returns {Promise} */ getPermission(name: string) { return this.get(`${this.baseURL}/permissions/${name}`); } /** createPermission - creates a custom permission * * @param {CustomPermissionOptions} permissionData the permission data * @returns {Promise} */ createPermission(permissionData: CustomPermissionOptions) { return this.post(`${this.baseURL}/permissions`, { ...permissionData, }); } /** updatePermission - updates an existing custom permission * * @param {string} id * @param {Omit} permissionData the permission data * @returns {Promise} */ updatePermission(id: string, permissionData: Omit) { return this.put(`${this.baseURL}/permissions/${id}`, { ...permissionData, }); } /** deletePermission - deletes a custom permission * * @param {string} name * @returns {Promise} */ deletePermission(name: string) { return this.delete(`${this.baseURL}/permissions/${name}`); } /** listPermissions - returns the list of all permissions for this application * * @returns {Promise} */ listPermissions() { return this.get(`${this.baseURL}/permissions`); } /** createRole - creates a custom role * * @param {string} name the new role name * @returns {Promise} */ createRole(name: string) { return this.post(`${this.baseURL}/roles`, { name }); } /** listRoles - returns the list of all roles for this application * * @returns {Promise} */ listRoles() { return this.get(`${this.baseURL}/roles`); } /** deleteRole - deletes a custom role * * @param {string} name the role name * @returns {Promise} */ deleteRole(name: string) { return this.delete(`${this.baseURL}/roles/${name}`); } /** sync - returns all events that happened for a list of channels since last sync * @param {string[]} channel_cids list of channel CIDs * @param {string} last_sync_at last time the user was online and in sync. RFC3339 ie. "2020-05-06T15:05:01.207Z" * @param {SyncOptions} options See JSDoc in the type fields for more info * * @returns {Promise} */ sync(channel_cids: string[], last_sync_at: string, options: SyncOptions = {}) { return this.post(`${this.baseURL}/sync`, { channel_cids, last_sync_at, ...options, }); } /** * sendUserCustomEvent - Send a custom event to a user * * @param {string} targetUserID target user id * @param {UserCustomEvent} event for example {type: 'friendship-request'} * * @return {Promise} The Server Response */ async sendUserCustomEvent(targetUserID: string, event: UserCustomEvent) { return await this.post(`${this.baseURL}/users/${targetUserID}/event`, { event, }); } createBlockList(blockList: BlockList) { return this.post(`${this.baseURL}/blocklists`, blockList); } listBlockLists() { return this.get(`${this.baseURL}/blocklists`); } getBlockList(name: string) { return this.get(`${this.baseURL}/blocklists/${name}`); } updateBlockList(name: string, data: { words: string[] }) { return this.put(`${this.baseURL}/blocklists/${name}`, data); } deleteBlockList(name: string) { return this.delete(`${this.baseURL}/blocklists/${name}`); } exportChannels(request: Array, options: ExportChannelOptions = {}) { const payload = { channels: request, ...options }; return this.post(`${this.baseURL}/export_channels`, payload); } exportUsers(request: ExportUsersRequest) { return this.post(`${this.baseURL}/export/users`, request); } exportChannel(request: ExportChannelRequest, options?: ExportChannelOptions) { return this.exportChannels([request], options); } getExportChannelStatus(id: string) { return this.get(`${this.baseURL}/export_channels/${id}`); } campaign(idOrData: string | CampaignData, data?: CampaignData) { if (typeof idOrData === 'string') { return new Campaign(this, idOrData, data); } return new Campaign(this, null, idOrData); } segment(type: SegmentType, idOrData: string | SegmentData, data?: SegmentData) { if (typeof idOrData === 'string') { return new Segment(this, type, idOrData, data); } return new Segment(this, type, null, idOrData); } validateServerSideAuth() { if (!this.secret) { throw new Error( 'Campaigns is a server-side only feature. Please initialize the client with a secret to use this feature.', ); } } /** * createSegment - Creates a segment * * @private * @param {SegmentType} type Segment type * @param {string} id Segment ID (valid UUID) * @param {string} name Segment name (valid UUID) * @param {SegmentData} params Segment data * * @return {{segment: SegmentResponse} & APIResponse} The created Segment */ private async createSegment(type: SegmentType, id: string, name: string, data?: SegmentData) { this.validateServerSideAuth(); const body = { id, type, name, data, }; return this.post<{ segment: SegmentResponse }>(this.baseURL + `/segments`, body); } /** * createUserSegment - Creates a user segment * * @param {string} id Segment ID (valid UUID) * @param {string} name Segment name * @param {SegmentData} data Segment data * * @return {Segment} The created Segment */ async createUserSegment(id: string, name: string, data?: SegmentData) { this.validateServerSideAuth(); return this.createSegment('user', id, name, data); } /** * createChannelSegment - Creates a channel segment * * @param {string} id Segment ID (valid UUID) * @param {string} name Segment name * @param {SegmentData} data Segment data * * @return {Segment} The created Segment */ async createChannelSegment(id: string, name: string, data?: SegmentData) { this.validateServerSideAuth(); return this.createSegment('channel', id, name, data); } async getSegment(id: string) { this.validateServerSideAuth(); return this.get<{ segment: SegmentResponse } & APIResponse>(this.baseURL + `/segments/${id}`); } /** * updateSegment - Update a segment * * @param {string} id Segment ID * @param {Partial} data Data to update * * @return {Segment} Updated Segment */ async updateSegment(id: string, data: Partial) { this.validateServerSideAuth(); return this.put<{ segment: SegmentResponse }>(this.baseURL + `/segments/${id}`, data); } /** * addSegmentTargets - Add targets to a segment * * @param {string} id Segment ID * @param {string[]} targets Targets to add to the segment * * @return {APIResponse} API response */ async addSegmentTargets(id: string, targets: string[]) { this.validateServerSideAuth(); const body = { target_ids: targets }; return this.post(this.baseURL + `/segments/${id}/addtargets`, body); } async querySegmentTargets( id: string, filter: QuerySegmentTargetsFilter | null = {}, sort: SortParam[] | null | [] = [], options = {}, ) { this.validateServerSideAuth(); return this.post<{ targets: SegmentTargetsResponse[]; next?: string } & APIResponse>( this.baseURL + `/segments/${id}/targets/query`, { filter: filter || {}, sort: sort || [], ...options, }, ); } /** * removeSegmentTargets - Remove targets from a segment * * @param {string} id Segment ID * @param {string[]} targets Targets to add to the segment * * @return {APIResponse} API response */ async removeSegmentTargets(id: string, targets: string[]) { this.validateServerSideAuth(); const body = { target_ids: targets }; return this.post(this.baseURL + `/segments/${id}/deletetargets`, body); } /** * querySegments - Query Segments * * @param {filter} filter MongoDB style filter conditions * @param {QuerySegmentsOptions} options Options for sorting/paginating the results * * @return {Segment[]} Segments */ async querySegments(filter: {}, sort?: SortParam[], options: QuerySegmentsOptions = {}) { this.validateServerSideAuth(); return this.post< { segments: SegmentResponse[]; next?: string; } & APIResponse >(this.baseURL + `/segments/query`, { filter, sort, ...options, }); } /** * deleteSegment - Delete a Campaign Segment * * @param {string} id Segment ID * * @return {Promise} The Server Response */ async deleteSegment(id: string) { this.validateServerSideAuth(); return this.delete(this.baseURL + `/segments/${id}`); } /** * segmentTargetExists - Check if a target exists in a segment * * @param {string} segmentId Segment ID * @param {string} targetId Target ID * * @return {Promise} The Server Response */ async segmentTargetExists(segmentId: string, targetId: string) { this.validateServerSideAuth(); return this.get(this.baseURL + `/segments/${segmentId}/target/${targetId}`); } /** * createCampaign - Creates a Campaign * * @param {CampaignData} params Campaign data * * @return {Campaign} The Created Campaign */ async createCampaign(params: CampaignData) { this.validateServerSideAuth(); return this.post<{ campaign: CampaignResponse } & APIResponse>(this.baseURL + `/campaigns`, { ...params }); } async getCampaign(id: string) { this.validateServerSideAuth(); return this.get<{ campaign: CampaignResponse } & APIResponse>(this.baseURL + `/campaigns/${id}`); } async startCampaign(id: string, options?: { scheduledFor?: string; stopAt?: string }) { this.validateServerSideAuth(); return this.post<{ campaign: CampaignResponse } & APIResponse>(this.baseURL + `/campaigns/${id}/start`, { scheduled_for: options?.scheduledFor, stop_at: options?.stopAt, }); } /** * queryCampaigns - Query Campaigns * * * @return {Campaign[]} Campaigns */ async queryCampaigns(filter: CampaignFilters, sort?: CampaignSort, options?: CampaignQueryOptions) { this.validateServerSideAuth(); return await this.post< { campaigns: CampaignResponse[]; next?: string; } & APIResponse >(this.baseURL + `/campaigns/query`, { filter, sort, ...(options || {}), }); } /** * updateCampaign - Update a Campaign * * @param {string} id Campaign ID * @param {Partial} params Campaign data * * @return {Campaign} Updated Campaign */ async updateCampaign(id: string, params: Partial) { this.validateServerSideAuth(); return this.put<{ campaign: CampaignResponse }>(this.baseURL + `/campaigns/${id}`, params); } /** * deleteCampaign - Delete a Campaign * * @param {string} id Campaign ID * * @return {Promise} The Server Response */ async deleteCampaign(id: string) { this.validateServerSideAuth(); return this.delete(this.baseURL + `/campaigns/${id}`); } /** * stopCampaign - Stop a Campaign * * @param {string} id Campaign ID * * @return {Campaign} Stopped Campaign */ async stopCampaign(id: string) { this.validateServerSideAuth(); const { campaign } = await this.patch<{ campaign: CampaignResponse }>(this.baseURL + `/campaigns/${id}/stop`); return campaign; } /** * enrichURL - Get OpenGraph data of the given link * * @param {string} url link * @return {OGAttachment} OG Attachment */ async enrichURL(url: string) { return this.get(this.baseURL + `/og`, { url }); } /** * getTask - Gets status of a long running task * * @param {string} id Task ID * * @return {TaskStatus} The task status */ async getTask(id: string) { return this.get(`${this.baseURL}/tasks/${id}`); } /** * deleteChannels - Deletes a list of channel * * @param {string[]} cids Channel CIDs * @param {boolean} [options.hard_delete] Defines if the channel is hard deleted or not * * @return {DeleteChannelsResponse} Result of the soft deletion, if server-side, it holds the task ID as well */ async deleteChannels(cids: string[], options: { hard_delete?: boolean } = {}) { return await this.post(this.baseURL + `/channels/delete`, { cids, ...options, }); } /** * deleteUsers - Batch Delete Users * * @param {string[]} user_ids which users to delete * @param {DeleteUserOptions} options Configuration how to delete users * * @return {TaskResponse} A task ID */ async deleteUsers(user_ids: string[], options: DeleteUserOptions = {}) { if (typeof options.user !== 'undefined' && !['soft', 'hard', 'pruning'].includes(options.user)) { throw new Error('Invalid delete user options. user must be one of [soft hard pruning]'); } if (typeof options.conversations !== 'undefined' && !['soft', 'hard'].includes(options.conversations)) { throw new Error('Invalid delete user options. conversations must be one of [soft hard]'); } if (typeof options.messages !== 'undefined' && !['soft', 'hard', 'pruning'].includes(options.messages)) { throw new Error('Invalid delete user options. messages must be one of [soft hard pruning]'); } return await this.post(this.baseURL + `/users/delete`, { user_ids, ...options, }); } /** * _createImportURL - Create an Import upload url. * * Note: Do not use this. * It is present for internal usage only. * This function can, and will, break and/or be removed at any point in time. * * @private * @param {string} filename filename of uploaded data * @return {APIResponse & CreateImportResponse} An ImportTask */ async _createImportURL(filename: string) { return await this.post(this.baseURL + `/import_urls`, { filename, }); } /** * _createImport - Create an Import Task. * * Note: Do not use this. * It is present for internal usage only. * This function can, and will, break and/or be removed at any point in time. * * @private * @param {string} path path of uploaded data * @param {CreateImportOptions} options import options * @return {APIResponse & CreateImportResponse} An ImportTask */ async _createImport(path: string, options: CreateImportOptions = { mode: 'upsert' }) { return await this.post(this.baseURL + `/imports`, { path, ...options, }); } /** * _getImport - Get an Import Task. * * Note: Do not use this. * It is present for internal usage only. * This function can, and will, break and/or be removed at any point in time. * * @private * @param {string} id id of Import Task * * @return {APIResponse & GetImportResponse} An ImportTask */ async _getImport(id: string) { return await this.get(this.baseURL + `/imports/${id}`); } /** * _listImports - Lists Import Tasks. * * Note: Do not use this. * It is present for internal usage only. * This function can, and will, break and/or be removed at any point in time. * * @private * @param {ListImportsPaginationOptions} options pagination options * * @return {APIResponse & ListImportsResponse} An ImportTask */ async _listImports(options: ListImportsPaginationOptions) { return await this.get(this.baseURL + `/imports`, options); } /** * upsertPushProvider - Create or Update a push provider * * Note: Works only for v2 push version is enabled on app settings. * * @param {PushProviderConfig} configuration of the provider you want to create or update * * @return {APIResponse & PushProviderUpsertResponse} A push provider */ async upsertPushProvider(pushProvider: PushProviderConfig) { return await this.post(this.baseURL + `/push_providers`, { push_provider: pushProvider, }); } /** * deletePushProvider - Delete a push provider * * Note: Works only for v2 push version is enabled on app settings. * * @param {PushProviderID} type and foreign id of the push provider to be deleted * * @return {APIResponse} An API response */ async deletePushProvider({ type, name }: PushProviderID) { return await this.delete(this.baseURL + `/push_providers/${type}/${name}`); } /** * listPushProviders - Get all push providers in the app * * Note: Works only for v2 push version is enabled on app settings. * * @return {APIResponse & PushProviderListResponse} A push provider */ async listPushProviders() { return await this.get(this.baseURL + `/push_providers`); } /** * creates an abort controller that will be used by the next HTTP Request. */ createAbortControllerForNextRequest() { return (this.nextRequestAbortController = new AbortController()); } /** * commits a pending message, making it visible in the channel and for other users * @param id the message id * * @return {APIResponse & MessageResponse} The message */ async commitMessage(id: string) { return await this.post(this.baseURL + `/messages/${id}/commit`); } }