@river-build/sdk
Version:
For more details, visit the following resources:
152 lines • 6.18 kB
JavaScript
import { usernameChecksum } from './utils';
import { dlog } from '@river-build/dlog';
const textDecoder = new TextDecoder();
export class MemberMetadata_Usernames {
log = dlog('csb:streams:usernames');
streamId;
plaintextUsernames = new Map();
userIdToEventId = new Map();
confirmedUserIds = new Set();
usernameEvents = new Map();
checksums = new Set();
constructor(streamId) {
this.streamId = streamId;
}
setLocalUsername(userId, username, emitter) {
this.plaintextUsernames.set(userId, username);
emitter?.emit('streamPendingUsernameUpdated', this.streamId, userId);
}
resetLocalUsername(userId, emitter) {
this.plaintextUsernames.delete(userId);
emitter?.emit('streamPendingUsernameUpdated', this.streamId, userId);
}
addEncryptedData(eventId, encryptedData, userId, pending = true, cleartext, encryptionEmitter, stateEmitter) {
if (!encryptedData.checksum) {
this.log('no checksum in encrypted data');
return;
}
if (!this.usernameAvailable(encryptedData.checksum)) {
this.log(`username not available for checksum ${encryptedData.checksum}`);
return;
}
this.removeUsernameEventForUserId(userId);
this.addUsernameEventForUserId(userId, eventId, encryptedData, pending);
if (cleartext) {
this.plaintextUsernames.set(userId, typeof cleartext === 'string' ? cleartext : textDecoder.decode(cleartext));
}
else {
// Clear the plaintext username for this user on name change
this.plaintextUsernames.delete(userId);
encryptionEmitter?.emit('newEncryptedContent', this.streamId, eventId, {
kind: 'text',
content: encryptedData,
});
}
if (!pending) {
this.confirmedUserIds.add(userId);
}
this.emitUsernameUpdated(eventId, stateEmitter);
}
onConfirmEvent(eventId, emitter) {
const event = this.usernameEvents.get(eventId);
if (!event) {
return;
}
this.usernameEvents.set(eventId, { ...event, pending: false });
this.confirmedUserIds.add(event.userId);
// if we don't have the plaintext username, no need to emit an event
if (this.plaintextUsernames.has(event.userId)) {
this.log(`'streamUsernameUpdated' for userId ${event.userId}`);
this.emitUsernameUpdated(eventId, emitter);
}
}
onDecryptedContent(eventId, content, emitter) {
const event = this.usernameEvents.get(eventId);
if (!event) {
return;
}
const checksum = event.encryptedData.checksum;
if (!checksum) {
return;
}
// If the checksum doesn't match, we don't want to update the username
const calculatedChecksum = usernameChecksum(content, this.streamId);
if (checksum !== calculatedChecksum) {
this.log(`checksum mismatch for userId: ${event.userId}, username: ${content}`);
return;
}
this.log(`setting username ${content} for user ${event.userId}`);
this.plaintextUsernames.set(event.userId, content);
this.emitUsernameUpdated(eventId, emitter);
}
cleartextUsernameAvailable(username) {
const checksum = usernameChecksum(username, this.streamId);
return this.usernameAvailable(checksum);
}
usernameAvailable(checksum) {
return !this.checksums.has(checksum);
}
emitUsernameUpdated(eventId, emitter) {
const event = this.usernameEvents.get(eventId);
if (!event) {
return;
}
// no information to emit — we haven't decrypted the username yet
if (!this.plaintextUsernames.has(event.userId)) {
return;
}
// depending on confirmation status, emit different events
emitter?.emit(event.pending ? 'streamPendingUsernameUpdated' : 'streamUsernameUpdated', this.streamId, event.userId);
}
removeUsernameEventForUserId(userId) {
// remove any traces of old events for this user
// we do this because unused usernames should be freed up for other users to use
const eventId = this.userIdToEventId.get(userId);
if (!eventId) {
this.log(`no existing username event for user ${userId}`);
return;
}
const event = this.usernameEvents.get(eventId);
if (!event) {
this.log(`no existing username event for user ${userId} — this is a programmer error`);
return;
}
this.checksums.delete(event.encryptedData.checksum ?? '');
this.usernameEvents.delete(eventId);
this.log(`deleted old username event for user ${userId}`);
}
addUsernameEventForUserId(userId, eventId, encryptedData, pending) {
if (!encryptedData.checksum) {
this.log('no checksum in encrypted data');
return;
}
// add to the userId -> eventId mapping for fast lookup later
this.userIdToEventId.set(userId, eventId);
// Set the checksum. This user has now claimed this checksum
// and no other users are able to use a username with the same checksum
this.checksums.add(encryptedData.checksum);
this.usernameEvents.set(eventId, {
userId,
encryptedData: encryptedData,
pending: pending,
});
}
info(userId) {
const name = this.plaintextUsernames.get(userId) ?? '';
const eventId = this.userIdToEventId.get(userId);
if (!eventId) {
return {
username: name,
usernameConfirmed: false,
usernameEncrypted: false,
};
}
const encrypted = this.usernameEvents.has(eventId) && !this.plaintextUsernames.has(userId);
return {
username: name,
usernameConfirmed: this.confirmedUserIds.has(userId),
usernameEncrypted: encrypted,
};
}
}
//# sourceMappingURL=memberMetadata_Usernames.js.map