UNPKG

@towns-protocol/sdk

Version:

For more details, visit the following resources:

193 lines 8.68 kB
import { ChannelOp, Err, ChunkedMediaSchema, } from '@towns-protocol/proto'; import { StreamStateView_AbstractContent } from './streamStateView_AbstractContent'; import { check, throwWithCode } from '@towns-protocol/dlog'; import { isDefined, logNever } from './check'; import { contractAddressFromSpaceId, isDefaultChannelId, streamIdAsString } from './id'; import { fromBinary } from '@bufbuild/protobuf'; import { decryptDerivedAESGCM } from '@towns-protocol/sdk-crypto'; import { bytesToHex } from 'ethereum-cryptography/utils'; export class StreamStateView_Space extends StreamStateView_AbstractContent { spacesView; streamId; get spaceChannelsMetadata() { return this.spaceStreamModel.channelsMetadata; } spaceImage; encryptedSpaceImage; decryptionInProgress; get spaceStreamModel() { return this.spacesView.get(this.streamId); } constructor(streamId, spacesView) { super(); this.spacesView = spacesView; this.streamId = streamId; } applySnapshot(_snapshot, content, _cleartexts, _encryptionEmitter) { // loop over content.channels, update space channels metadata for (const payload of content.channels) { this.addSpacePayload_Channel(payload, payload.updatedAtEventNum, undefined); } if (content.spaceImage?.data) { this.encryptedSpaceImage = { data: content.spaceImage.data, eventId: bytesToHex(content.spaceImage.eventHash), }; } } onConfirmedEvent(_event, _emitter) { // pass } prependEvent(event, _cleartext, _encryptionEmitter, _stateEmitter) { check(event.remoteEvent.event.payload.case === 'spacePayload'); const payload = event.remoteEvent.event.payload.value; switch (payload.content.case) { case 'inception': break; case 'channel': // nothing to do, channel data was conveyed in the snapshot break; case 'updateChannelAutojoin': // likewise, this data was conveyed in the snapshot break; case 'updateChannelHideUserJoinLeaveEvents': // likewise, this data was conveyed in the snapshot break; case 'spaceImage': // nothing to do, spaceImage is set in the snapshot break; case undefined: break; default: logNever(payload.content); } } appendEvent(event, _cleartext, _encryptionEmitter, stateEmitter) { check(event.remoteEvent.event.payload.case === 'spacePayload'); const payload = event.remoteEvent.event.payload.value; switch (payload.content.case) { case 'inception': break; case 'channel': this.addSpacePayload_Channel(payload.content.value, event.eventNum, stateEmitter); break; case 'updateChannelAutojoin': this.addSpacePayload_UpdateChannelAutojoin(payload.content.value, stateEmitter); break; case 'updateChannelHideUserJoinLeaveEvents': this.addSpacePayload_UpdateChannelHideUserJoinLeaveEvents(payload.content.value, stateEmitter); break; case 'spaceImage': this.encryptedSpaceImage = { data: payload.content.value, eventId: event.hashStr }; stateEmitter?.emit('spaceImageUpdated', this.streamId); break; case undefined: break; default: logNever(payload.content); } } async getSpaceImage() { // if we have an encrypted space image, decrypt it if (this.encryptedSpaceImage) { const encryptedData = this.encryptedSpaceImage?.data; this.encryptedSpaceImage = undefined; this.decryptionInProgress = { promise: this.decryptSpaceImage(encryptedData), encryptedData, }; return this.decryptionInProgress.promise; } // if there isn't an updated encrypted space image, but a decryption is // in progress, return the promise if (this.decryptionInProgress) { return this.decryptionInProgress.promise; } // always return the decrypted space image return this.spaceImage; } async decryptSpaceImage(encryptedData) { try { const spaceAddress = contractAddressFromSpaceId(this.streamId); const context = spaceAddress.toLowerCase(); const plaintext = await decryptDerivedAESGCM(context, encryptedData); const decryptedImage = fromBinary(ChunkedMediaSchema, plaintext); if (encryptedData === this.decryptionInProgress?.encryptedData) { this.spaceImage = decryptedImage; } return decryptedImage; } finally { if (encryptedData === this.decryptionInProgress?.encryptedData) { this.decryptionInProgress = undefined; } } } addSpacePayload_UpdateChannelAutojoin(payload, stateEmitter) { const { channelId: channelIdBytes, autojoin } = payload; const channelId = streamIdAsString(channelIdBytes); this.spacesView.updateChannelMetadata(this.streamId, channelId, { isAutojoin: autojoin }); stateEmitter?.emit('spaceChannelAutojoinUpdated', this.streamId, channelId, autojoin); } addSpacePayload_UpdateChannelHideUserJoinLeaveEvents(payload, stateEmitter) { const { channelId: channelIdBytes, hideUserJoinLeaveEvents } = payload; const channelId = streamIdAsString(channelIdBytes); this.spacesView.updateChannelMetadata(this.streamId, channelId, { hideUserJoinLeaveEvents, }); stateEmitter?.emit('spaceChannelHideUserJoinLeaveEventsUpdated', this.streamId, channelId, hideUserJoinLeaveEvents); } addSpacePayload_Channel(payload, updatedAtEventNum, stateEmitter) { const { op, channelId: channelIdBytes } = payload; const channelId = streamIdAsString(channelIdBytes); switch (op) { case ChannelOp.CO_CREATED: { const isDefault = isDefaultChannelId(channelId); const isAutojoin = payload.settings?.autojoin ?? isDefault; const hideUserJoinLeaveEvents = payload.settings?.hideUserJoinLeaveEvents ?? false; this.spacesView.updateChannelMetadata(this.streamId, channelId, { isDefault, updatedAtEventNum, isAutojoin, hideUserJoinLeaveEvents, }); stateEmitter?.emit('spaceChannelCreated', this.streamId, channelId); break; } case ChannelOp.CO_DELETED: if (this.spacesView.delete(this.streamId, channelId)) { stateEmitter?.emit('spaceChannelDeleted', this.streamId, channelId); } break; case ChannelOp.CO_UPDATED: { // first take settings from payload, then from local channel, then defaults const channel = this.spaceChannelsMetadata[channelId]; const isDefault = isDefaultChannelId(channelId); const isAutojoin = isDefined(payload.settings?.autojoin) ? payload.settings.autojoin : isDefined(channel?.isAutojoin) ? channel.isAutojoin : isDefault; const hideUserJoinLeaveEvents = isDefined(payload.settings?.hideUserJoinLeaveEvents) ? payload.settings.hideUserJoinLeaveEvents : isDefined(channel?.hideUserJoinLeaveEvents) ? channel.hideUserJoinLeaveEvents : false; this.spacesView.updateChannelMetadata(this.streamId, channelId, { isDefault, updatedAtEventNum, isAutojoin, hideUserJoinLeaveEvents, }); stateEmitter?.emit('spaceChannelUpdated', this.streamId, channelId, updatedAtEventNum); break; } default: throwWithCode(`Unknown channel ${op}`, Err.STREAM_BAD_EVENT); } } onDecryptedContent(_eventId, _content, _stateEmitter) { // pass } } //# sourceMappingURL=streamStateView_Space.js.map