@river-build/sdk
Version:
For more details, visit the following resources:
186 lines • 8.35 kB
JavaScript
import { ChannelOp, Err, ChunkedMediaSchema, } from '@river-build/proto';
import { StreamStateView_AbstractContent } from './streamStateView_AbstractContent';
import { check, throwWithCode } from '@river-build/dlog';
import { logNever } from './check';
import { contractAddressFromSpaceId, isDefaultChannelId, streamIdAsString } from './id';
import { fromBinary } from '@bufbuild/protobuf';
import { decryptDerivedAESGCM } from './crypto_utils';
export class StreamStateView_Space extends StreamStateView_AbstractContent {
streamId;
spaceChannelsMetadata = new Map();
spaceImage;
encryptedSpaceImage;
decryptionInProgress;
constructor(streamId) {
super();
this.streamId = streamId;
}
applySnapshot(eventHash, _snapshot, content, _cleartexts, _encryptionEmitter) {
// loop over content.channels, update space channels metadata
for (const payload of content.channels) {
this.addSpacePayload_Channel(eventHash, payload, payload.updatedAtEventNum, undefined);
}
if (content.spaceImage?.data) {
this.encryptedSpaceImage = { data: content.spaceImage.data, eventId: 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(event.hashStr, 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);
const channel = this.spaceChannelsMetadata.get(channelId);
if (!channel) {
throwWithCode(`Channel not found: ${channelId}`, Err.STREAM_BAD_EVENT);
}
this.spaceChannelsMetadata.set(channelId, {
...channel,
isAutojoin: autojoin,
});
stateEmitter?.emit('spaceChannelAutojoinUpdated', this.streamId, channelId, autojoin);
}
addSpacePayload_UpdateChannelHideUserJoinLeaveEvents(payload, stateEmitter) {
const { channelId: channelIdBytes, hideUserJoinLeaveEvents } = payload;
const channelId = streamIdAsString(channelIdBytes);
const channel = this.spaceChannelsMetadata.get(channelId);
if (!channel) {
throwWithCode(`Channel not found: ${channelId}`, Err.STREAM_BAD_EVENT);
}
this.spaceChannelsMetadata.set(channelId, {
...channel,
hideUserJoinLeaveEvents,
});
stateEmitter?.emit('spaceChannelHideUserJoinLeaveEventsUpdated', this.streamId, channelId, hideUserJoinLeaveEvents);
}
addSpacePayload_Channel(_eventHash, 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.spaceChannelsMetadata.set(channelId, {
isDefault,
updatedAtEventNum,
isAutojoin,
hideUserJoinLeaveEvents,
});
stateEmitter?.emit('spaceChannelCreated', this.streamId, channelId);
break;
}
case ChannelOp.CO_DELETED:
if (this.spaceChannelsMetadata.delete(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.get(channelId);
const isDefault = isDefaultChannelId(channelId);
const isAutojoin = payload.settings?.autojoin ?? channel?.isAutojoin ?? isDefault;
const hideUserJoinLeaveEvents = payload.settings?.hideUserJoinLeaveEvents ?? channel?.isAutojoin ?? false;
this.spaceChannelsMetadata.set(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