@towns-protocol/sdk
Version:
For more details, visit the following resources:
193 lines • 8.68 kB
JavaScript
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