UNPKG

@towns-protocol/sdk

Version:

For more details, visit the following resources:

308 lines 12.6 kB
import { DmChannelSettingValue, GdmChannelSettingValue, SpaceChannelSettingValue, StartAuthenticationResponseSchema, FinishAuthenticationResponseSchema, GetSettingsResponseSchema, DmChannelSettingSchema, GdmChannelSettingSchema, SpaceSettingSchema, SpaceChannelSettingSchema, } from '@towns-protocol/proto'; import { create, fromBinary, toBinary, toJson } from '@bufbuild/protobuf'; import { bin_fromBase64, bin_toBase64, dlogger } from '@towns-protocol/dlog'; import { cloneDeep } from 'lodash-es'; import { Observable } from './observable/observable'; import { makeNotificationRpcClient } from './makeNotificationRpcClient'; import { NotificationService } from './notificationService'; import { spaceIdFromChannelId, streamIdAsBytes, streamIdAsString, userIdFromAddress } from './id'; const logger = dlogger('csb:notifications'); class InMemoryNotificationStore { store = {}; getItem(schema, key) { const data = this.store[`${schema.typeName}-${key}`]; return data ? fromBinary(schema, bin_fromBase64(data)) : undefined; } setItem(schema, key, value) { this.store[`${schema.typeName}-${key}`] = bin_toBase64(toBinary(schema, value)); } } export class NotificationsClient { signerContext; url; store; opts; data; _client; startResponseKey; finishResponseKey; settingsKey; getClientPromise; getSettingsPromise; constructor(signerContext, url, store = new InMemoryNotificationStore(), opts = undefined) { this.signerContext = signerContext; this.url = url; this.store = store; this.opts = opts; this.startResponseKey = `startResponse`; this.finishResponseKey = `finishResponse`; this.settingsKey = `settings`; this.data = new Observable({ fetchedAtMs: undefined, settings: this.getLocalSettings(), error: undefined, }); } get userId() { return userIdFromAddress(this.signerContext.creatorAddress); } getLocalStartResponse() { return this.store.getItem(StartAuthenticationResponseSchema, this.startResponseKey); } setLocalStartResponse(response) { this.store.setItem(StartAuthenticationResponseSchema, this.startResponseKey, response); } getLocalFinishResponse() { return this.store.getItem(FinishAuthenticationResponseSchema, this.finishResponseKey); } setLocalFinishResponse(response) { this.store.setItem(FinishAuthenticationResponseSchema, this.finishResponseKey, response); } getLocalSettings() { return this.store.getItem(GetSettingsResponseSchema, this.settingsKey); } setLocalSettings(settings) { this.store.setItem(GetSettingsResponseSchema, this.settingsKey, settings); } updateLocalSettings(fn) { if (!this.data.value.settings) { throw new Error('TNS PUSH: settings has not been fetched'); } const newSettings = cloneDeep(this.data.value.settings); fn(newSettings); this.setLocalSettings(newSettings); this.data.setValue({ ...this.data.value, settings: newSettings }); } async getClient() { if (this.getClientPromise) { return this.getClientPromise; } try { this.getClientPromise = this._getClient(); const result = await this.getClientPromise; this.getClientPromise = undefined; return result; } catch (error) { this.data.setValue({ ...this.data.value, error: error }); this.getClientPromise = undefined; return undefined; } } async _getClient() { const startResponse = this.getLocalStartResponse(); const finishResponse = this.getLocalFinishResponse(); // if we have a valid start response and finish response, and the start response is still valid, // we can re-create the client from the local storage // and cache it locally to leverage http2 connection pooling if (startResponse && finishResponse && startResponse.expiration && startResponse.expiration.seconds > Date.now() / 1000) { if (this._client) { // if everything is still valid, and we have the client, return the client return this._client; } try { const client = makeNotificationRpcClient(this.url, finishResponse.sessionToken, this.opts); this._client = client; return client; } catch (error) { logger.error('TNS PUSH: error authenticating from local storage, will try from scratch', error); } } // if we don't have a valid client, or if the client has expired,we need to authenticate from scratch const service = await NotificationService.authenticate(this.signerContext, this.url, this.opts); this.setLocalStartResponse(service.startResponse); this.setLocalFinishResponse(service.finishResponse); return service.notificationRpcClient; } async withClient(fn) { const client = await this.getClient(); if (client) { try { return await fn(client); } catch (error) { this.data.setValue({ ...this.data.value, error: error }); throw error; } } // either an error is already logged or the initialize threw an error return undefined; } async getSettings() { if (this.getSettingsPromise) { return this.getSettingsPromise; } this.getSettingsPromise = this._getSettings(); return this.getSettingsPromise; } async _getSettings() { return this.withClient(async (client) => { try { const response = await client.getSettings({}); this.setLocalSettings(response); this.data.setValue({ fetchedAtMs: Date.now(), settings: response, error: undefined, }); logger.log('TNS PUSH: fetched settings', toJson(GetSettingsResponseSchema, response)); this.getSettingsPromise = undefined; return response; } catch (error) { this.data.setValue({ ...this.data.value, error: error, }); throw error; } }); } async subscribeWebPush(subscription) { return this.withClient(async (client) => { logger.log('TNS PUSH: subscribing to web push'); return client.subscribeWebPush({ subscription }); }); } async unsubscribeWebPush(subscription) { return this.withClient(async (client) => { logger.log('TNS PUSH: unsubscribing to web push'); return client.unsubscribeWebPush({ subscription }); }); } async setDmGlobalSetting(value) { return this.withClient(async (client) => { await client.setDmGdmSettings({ dmGlobal: value, gdmGlobal: this.data.value.settings?.gdmGlobal ?? GdmChannelSettingValue.GDM_UNSPECIFIED, }); this.updateLocalSettings((settings) => { settings.dmGlobal = value; }); }); } async setGdmGlobalSetting(value) { return this.withClient(async (client) => { await client.setDmGdmSettings({ dmGlobal: this.data.value.settings?.dmGlobal ?? DmChannelSettingValue.DM_UNSPECIFIED, gdmGlobal: value, }); this.updateLocalSettings((settings) => { settings.gdmGlobal = value; }); }); } async setDmChannelSetting(channelId, value) { return this.withClient(async (client) => { await client.setDmChannelSetting({ dmChannelId: streamIdAsBytes(channelId), value, }); this.updateLocalSettings((settings) => { settings.dmChannels = settings.dmChannels.filter((c) => streamIdAsString(c.channelId) !== channelId); settings.dmChannels.push(create(DmChannelSettingSchema, { channelId: streamIdAsBytes(channelId), value, })); }); }); } async setGdmChannelSetting(channelId, value) { return this.withClient(async (client) => { await client.setGdmChannelSetting({ gdmChannelId: streamIdAsBytes(channelId), value, }); this.updateLocalSettings((settings) => { settings.gdmChannels = settings.gdmChannels.filter((c) => streamIdAsString(c.channelId) !== channelId); settings.gdmChannels.push(create(GdmChannelSettingSchema, { channelId: streamIdAsBytes(channelId), value, })); }); }); } async setSpaceSetting(spaceId, value) { return this.withClient(async (client) => { await client.setSpaceSettings({ spaceId: streamIdAsBytes(spaceId), value }); this.updateLocalSettings((settings) => { const spaceIndex = settings.space.findIndex((s) => streamIdAsString(s.spaceId) === spaceId); if (spaceIndex != -1) { settings.space[spaceIndex].value = value; } else { settings.space.push(create(SpaceSettingSchema, { spaceId: streamIdAsBytes(spaceId), value, channels: [], })); } }); }); } async setChannelSetting(channelId, value) { const spaceId = spaceIdFromChannelId(channelId); return this.withClient(async (client) => { await client.setSpaceChannelSettings({ spaceId: streamIdAsBytes(spaceId), channelId: streamIdAsBytes(channelId), value, }); this.updateLocalSettings((settings) => { let spaceIndex = settings.space.findIndex((s) => streamIdAsString(s.spaceId) === spaceId); if (spaceIndex == -1) { spaceIndex = settings.space.length; settings.space.push(create(SpaceSettingSchema, { spaceId: streamIdAsBytes(spaceId), value: SpaceChannelSettingValue.SPACE_CHANNEL_SETTING_UNSPECIFIED, channels: [], })); } const channelIndex = settings.space[spaceIndex].channels.findIndex((c) => streamIdAsString(c.channelId) === channelId); if (channelIndex == -1) { settings.space[spaceIndex].channels.push(create(SpaceChannelSettingSchema, { channelId: streamIdAsBytes(channelId), value, })); } else { settings.space[spaceIndex].channels[channelIndex].value = value; } }); }); } } export function getMutedChannelIds(settings) { if (!settings) { return undefined; } const ids = new Set(); for (const spaceSetting of settings.space) { if (spaceSetting.value === SpaceChannelSettingValue.SPACE_CHANNEL_SETTING_NO_MESSAGES_AND_MUTE) { ids.add(streamIdAsString(spaceSetting.spaceId)); } for (const channelSetting of spaceSetting.channels) { if (channelSetting.value === SpaceChannelSettingValue.SPACE_CHANNEL_SETTING_NO_MESSAGES_AND_MUTE) { ids.add(streamIdAsString(channelSetting.channelId)); } } } for (const dmSetting of settings.dmChannels) { if (dmSetting.value === DmChannelSettingValue.DM_MESSAGES_NO_AND_MUTE) { ids.add(streamIdAsString(dmSetting.channelId)); } } for (const gdmSetting of settings.gdmChannels) { if (gdmSetting.value === GdmChannelSettingValue.GDM_MESSAGES_NO_AND_MUTE) { ids.add(streamIdAsString(gdmSetting.channelId)); } } return ids; } //# sourceMappingURL=notificationsClient.js.map