const e = require("ready-resource"),
t = require("safety-catch"),
s = require("b4a"),
i = require("./loader"),
{ KeetContract: a } = require("../contract"),
{
contract: {
hyperschema: { getEnum: o },
},
} = require("../../../schema/spec"),
{
verifyIdentityProofs: n,
generateRoomCapabilities: r,
generateIdentityPairingName: l,
} = require("../invitations"),
{ RoomStatus: h } = require("../contract/identity/private"),
{ KeetErrorV2: c } = require("../../errors"),
d = o("@keet/room-type"),
u = "insert",
m = 16
function f(e, t) {
return s.equals(e.oplog.key, t.oplog.key) && e.oplog.seq === t.oplog.seq
}
module.exports = class extends e {
constructor(e, t, s, i) {
;(super(),
(this.keet = e),
(this.swarm = e.swarm),
(this.local = e.local),
(this.view = t),
(this.base = s),
(this.changes = i),
(this.isCurrentPrivateProfile = !1),
(this.currentPrivateProfileInfo = null),
(this.currentIdentityInfo = null),
(this.currentWriterInfo = null),
(this.currentRoomInfo = null),
(this.roomInfo = null),
(this.updated = !1),
(this.updates = {
cache: null,
tasks: null,
contacts: null,
dispatches: null,
shouldUpgrade: !1,
profileDispatches: null,
unreadKeys: !1,
activeCall: null,
activeCallDevice: null,
activeCallSynced: !1,
sharedRooms: null,
sharedPreferences: null,
pendingInvitations: null,
startIdentitySwap: null,
attemptIdentitySwap: null,
finishIdentitySwap: null,
logout: !1,
}),
(this.events = []),
(this._loader = null),
(this._running = null))
}
_open() {
return ((this._loader = new i(this.local, this.view, this.base)), this._loader.ready())
}
async _close() {
this._loader && (await this._loader.close())
}
async _syncDigest(e) {
if ((e || (e = await this._loader.loadDigest()), e)) {
if (!this.keet.isValidBuildType(e)) throw c.INVALID_BUILD_TYPE()
;(this.events.push({ type: "v2:room-digest-changed", value: e }), (this.roomInfo.digest = e))
}
}
async _syncConfig() {
const e = await this._loader.loadConfig()
e &&
(this.events.push({ type: "v2:room-config-changed", value: e }), (this.roomInfo.config = e))
}
async _syncRoomCapabilities(e) {
;(e || (e = await this._loader.loadCapabilities()),
e &&
((this.roomInfo.seed && s.equals(this.roomInfo.seed, e.seed)) ||
(this.events.push({ type: "v2:room-capabilities-changed", value: e }),
(this.roomInfo.seed = e.seed),
(this.roomInfo.capabilities = r(this.base.key, e.seed)))))
}
async _syncSelf() {
const [e, t, s] = await Promise.all([
this._loader.loadDigest(),
this._loader.loadSelfDevice(),
this._loader.loadSelfMember(),
])
if (
((this.roomInfo.selfDevice = t),
(this.roomInfo.selfMember = s),
!e || e.settings.roomType !== d.DirectMessage)
)
return
const [i, a] = await Promise.all([this._loader.loadConfig(), this._loader.loadDMMembers()])
;((this.roomInfo.config = i), (this.roomInfo.dm = a))
}
async _syncSelfMember(e) {
if (
(this.events.push({ type: "v2:room-member-changed", value: e }),
!e || s.equals(e.key, this.currentIdentityInfo.publicKey))
)
return this._syncSelf()
}
async _syncDevice(e) {
if (
(this.events.push({ type: "v2:room-device-changed", value: e }),
!e || s.equals(e.key, this.base.local.key))
)
return this._syncSelf()
}
_setLastMessage(e) {
;(this.events.push({ type: "v2:room-last-message-changed", value: e }),
(this.roomInfo.lastMessage = e.message),
(this.roomInfo.lastMember = e.member),
(this.roomInfo.lastMessageReactions = e.reactions),
(this.updates.cache = { member: e.member, otherMembers: e.otherMembers, device: e.device }))
const t = this.currentRoomInfo?.lastMessage
;(t && f(t, e.message)) || (this.updates.chat = e.message)
}
async _syncLastMessage() {
const e = await this._loader.loadLastMessage()
e && this._setLastMessage(e)
}
async _syncMessage(e) {
if (!e) return
const [t, s] = await Promise.all([
this._loader.loadPrimaryThread(),
this._loader.loadMessage(e.id),
])
;(0 === e.id.thread && e.id.index === t.count - 1 && this._setLastMessage(s),
this.events.push({ type: "v2:room-chat-changed", value: s }))
}
async _syncReactions(e) {
const t = await this._loader.input.get("@keet/chat", { id: e.id })
this.events.push({ type: "v2:room-reactions-changed", value: { id: t.oplog, reactions: e } })
}
_syncJoinRequests(e) {
this.events.push({ type: "v2:room-join-requests-changed", value: e })
}
async _syncReadLengths() {
const e = await this._loader.loadReadLengths()
e?.length &&
(this.events.push({ type: "v2:read-lengths-changed", value: e }),
(this.roomInfo.readLengths = e))
}
async _syncActiveCall() {
;((this.updates.activeCallSynced = !0), (this.roomInfo.activeCall = null))
const e = await this._loader.loadActiveCall()
if (e) {
const t = e.callKey,
s = this.base.local.key,
i = await this._loader.loadActiveCallDevice(t, s),
a = i ? i.deviceKey : null
;((this.updates.activeCallDevice = i),
(this.roomInfo.activeCall = { callKey: t, selfDeviceKey: a }))
}
this.updates.activeCall = e
}
async _syncPreferences() {
const e = await this._loader.loadPreferences()
e && (this.roomInfo.preferences = e.status)
}
async _syncStats(e) {
;(e || (e = await this._loader.loadPrimaryThread()),
e &&
(this.events.push({ type: "v2:room-stats-changed", value: e }),
(this.roomInfo.stats = e.stats)))
}
async _syncRoomIdentity(e) {
;(e || (e = await this._loader.loadIdentity()),
e &&
(this.events.push({ type: "v2:room-identity-changed", value: e }),
(this.roomInfo.identity = e)))
}
async _syncUnreadAnchors() {
this.updates.unreadKeys = !0
}
async _syncMember(e) {
const i = [this._syncSelfMember(e)]
this.roomInfo.dm?.length && i.push(this._syncConfig().catch(t))
const { lastMember: a } = this.roomInfo
;(a && s.equals(a.key, e.key) && i.push(this._syncLastMessage().catch(t)), await Promise.all(i))
}
async _incrementalRoomInfoLoad(e, s) {
const i = [],
a = ["@keet/calls", "@keet/call-devices", "@keet/call-members"]
for await (const { type: o, collection: n, value: r } of this._loader.input.changes({
gte: e,
lt: s,
})) {
if (!a.includes(n) && o !== u) continue
let e = null
switch (n) {
case "@keet/system-digest":
e = this._syncDigest(r)
break
case "@keet/room-config":
e = this._syncConfig()
break
case "@keet/room-capabilities":
e = this._syncRoomCapabilities(r)
break
case "@keet/members":
e = this._syncMember(r)
break
case "@keet/devices":
e = this._syncDevice(r)
break
case "@keet/chat":
e = this._syncMessage(r)
break
case "@keet/reaction-digests":
e = this._syncReactions(r)
break
case "@keet/threads":
e = this._syncStats(r)
break
case "@keet-private-id/identity":
e = this._syncRoomIdentity(r)
break
case "@keet/join-requests":
e = this._syncJoinRequests(r)
break
case "@keet-private-id/unreads-v2":
case "@keet-private-id/unreads":
e = this._syncUnreadAnchors()
break
case "@keet/read-lengths":
e = this._syncReadLengths()
break
case "@keet/calls":
case "@keet/call-devices":
case "@keet/call-members":
e = this._syncActiveCall()
break
default:
continue
}
e && (e.catch(t), i.push(e))
}
return (await Promise.all(i), !!this.roomInfo.digest)
}
async _fullRoomInfoLoad() {
return (
this.events.push({ type: "v2:room-fast-forward" }),
(this.roomInfo = {
roomKey: this.base.key,
discoveryKey: this.base.discoveryKey,
encryptionKey: this.base.encryptionKey,
digest: null,
stats: null,
fork: 0,
length: 0,
timestamps: { creation: 0, latestActivity: 0, latestChatMessage: 0 },
status: null,
mailbox: null,
selfDevice: null,
selfMember: null,
config: null,
capabilities: null,
identity: null,
lastMessage: null,
lastMember: null,
lastMessageReactions: null,
dm: null,
activeCall: null,
preferences: null,
}),
await Promise.all([
this._syncDigest(null),
this._syncRoomCapabilities(null),
this._syncDevice(null),
this._syncMember(null),
this._syncStats(null),
this._syncRoomIdentity(null),
this._syncUnreadAnchors(),
this._syncConfig(),
this._syncReadLengths(),
this._syncLastMessage(),
this._syncActiveCall(),
this._syncPreferences(),
]),
!!this.roomInfo.digest
)
}
async _loadPrivateProfileChanges() {
if (!0 === this.keet.debugDisableSynching) return
let e = null
if (this.isCurrentPrivateProfile && this.roomInfo.selfDevice) {
const t = this.changes.get(a.DB_VIEW_NAME)
;((this._loader.shared = this.currentRoomInfo?.selfDevice ? t.shared : 1),
(e = await Promise.all([
this._loader.loadContactChanges(),
this._loader.loadSharedRoomChanges(),
this._loader.loadPendingInvitations(),
])))
}
e &&
((this.updates.contacts = e[0]),
(this.updates.sharedRooms = e[1]),
(this.updates.pendingInvitations = e[2]))
}
async _loadLocalInfo() {
const [e, t, i] = await Promise.all([
this._loader.loadLocalRoomInfo(),
this._loader.loadLocalIdentityInfo(),
this._loader.loadLocalPrivateProfileInfo(),
])
;((this.currentRoomInfo = e),
(this.roomInfo = { ...this.currentRoomInfo }),
(this.currentIdentityInfo = t),
(this.currentPrivateProfileInfo = i),
(this.isCurrentPrivateProfile = !!t && s.equals(t.privateProfileKey, this.base.key)))
const a = this.currentIdentityInfo?.swapV2
return (
!a ||
!s.equals(a.privateProfileKey, this.base.key) ||
((this.updates.attemptIdentitySwap = a), !0)
)
}
async _load() {
let [e, t] = await Promise.all([
this._loadLocalInfo(),
this.base.system.get(this.base.local.key),
])
if (this.closing || !e) return !1
if (this.updates.attemptIdentitySwap) {
const e = this.updates.attemptIdentitySwap,
t = e.writerKey,
[i, a, o] = await Promise.all([
this._loader.input.get("@keet-private-id/device-identities", { deviceKey: t }),
this._loader.input.get("@keet/devices", { key: t }),
this.local.get("@local/rooms", { roomKey: this.base.key }),
])
if (!i || !a || !n(e, i)) return !1
const r = i.deviceProof,
h = l(e.publicKey),
c = await this.keet.store.createKeyPair(h)
if (!s.equals(c.publicKey, e.devicePublicKey)) return !1
;((this.isCurrentPrivateProfile = !0),
(this.currentPrivateProfileInfo = o),
(this.currentIdentityInfo = this.currentIdentityInfo.swapV2),
(this.updates.finishIdentitySwap = {
publicKey: this.updates.attemptIdentitySwap.publicKey,
privateProfileKey: this.base.key,
deviceKeyPair: c,
deviceProof: r,
status: { isConfirmed: !0 },
}))
}
this.currentWriterInfo = t
let i = null
if (this.changes && this.currentRoomInfo) {
const e = this.changes.get("_system")
if (e.to - e.shared < m) {
const e = this.changes.get("keet/v3/db")
i = { from: e.shared, to: e.to }
}
}
if (
((e = !1),
(e = i ? await this._incrementalRoomInfoLoad(i.from, i.to) : await this._fullRoomInfoLoad()),
!e)
)
return !1
;(this.events.push({ type: "v2:room-changed" }),
(this.roomInfo.length = this._loader.core.length),
(this.roomInfo.fork = this._loader.core.fork),
this.isCurrentPrivateProfile && (await this._loadPrivateProfileChanges()))
const a = !!this.roomInfo.selfMember?.status.isRemoved,
o = !!this.roomInfo.selfMember?.status.isActive,
r = !!this.roomInfo.selfDevice?.status.isActive,
h = a || (!r && !!this.currentWriterInfo),
c = this.base.writable && r && o,
d = !!this.roomInfo.identity
return (
(this.roomInfo.status = { isWritable: c, isReadable: !0, isPrivateProfile: d, isRemoved: h }),
(this.roomInfo.timestamps.latestSync = Date.now()),
(this.roomInfo.timestamps.creation = this.roomInfo.digest.creationTime),
this.roomInfo.lastMessage &&
(this.roomInfo.timestamps.latestChatMessage = this.roomInfo.lastMessage.timestamp),
!0
)
}
_shouldUpgrade() {
return (
(this.roomInfo.selfMember.status.isModerator &&
!(this.roomInfo.digest.abi.version >= this.keet.abi.autoupgrade)) ||
null
)
}
_generateMemberUpdate() {
const e = this.currentPrivateProfileInfo.selfMember,
t = this.roomInfo.selfMember,
s = t.profile.timestamp > 0,
i = e.profile.timestamp <= t.profile.timestamp
if (!e?.profile || (s && i)) return null
const a = { identity: this.currentIdentityInfo, privateProfile: this.currentPrivateProfileInfo }
return { type: "@keet/update-member", op: { member: this.keet.internalGetMemberUpdate(a) } }
}
_generatePrivateProfileDispatches() {
;((this.updates.profileDispatches = []),
this.updates.profileDispatches.push({
type: "@keet-private-id/update-room-status",
op: { roomKey: this.roomInfo.roomKey, key: h.IsActive, status: { isActive: !1 } },
}))
}
_generateDispatches() {
if (
((this.updates.dispatches = []),
(this.updates.shouldUpgrade = this._shouldUpgrade()),
!this.currentPrivateProfileInfo?.selfMember)
)
return
const e = this._generateMemberUpdate()
e && this.updates.dispatches.push(e)
}
_generateTasks() {
if (this.updates.sharedRooms) {
;((this.updates.tasks = []), (this.updates.sharedPreferences = []))
for (const e of this.updates.sharedRooms) {
const t = e.mailbox
if (e.identitySwapV2) {
;((this.updates.startIdentitySwap = {
...e.identitySwapV2,
privateProfileKey: e.roomKey,
}),
(this.updates.tasks = [this.keet.tasks.joinRoomKeys(t, { keys: e })]))
break
}
;(e.status.isActive
? this.updates.tasks.push(this.keet.tasks.joinRoomKeys(t, { keys: e }))
: this.updates.tasks.push(this.keet.tasks.leaveRoom(t, { roomKey: e.roomKey })),
this.updates.sharedPreferences.push({ roomKey: e.roomKey, status: e.status }))
}
if (!this.updates.startIdentitySwap && this.updates.pendingInvitations)
for (const e of this.updates.pendingInvitations)
this.updates.tasks.push(this.keet.tasks.joinRoomInvitation(e.mailbox, { invitation: e }))
}
}
async _maybeUpdateOpenRooms(e) {
try {
const t = this.currentRoomInfo?.selfMember?.profile.timestamp || 0
if (!this.roomInfo?.selfMember || t >= this.roomInfo.selfMember.profile.timestamp) return
const i = {
profile: this.roomInfo.selfMember.profile,
attestation: {
memberKey: this.roomInfo.selfMember.key,
proof: this.roomInfo.selfMember.proofs.profileAttestation,
},
},
a = []
for (const t of this.keet.rooms.values())
s.equals(t.key, e) || a.push(t.dispatch("@keet/update-member", { member: i }))
await Promise.allSettled(a)
} catch (e) {
this.keet.onerror(e)
}
}
async _run() {
if ((this.opened || (await this.ready()), !(await this._load()) || this.closing)) return !1
;(this.roomInfo.status.isWritable && this._generateDispatches(),
this.isCurrentPrivateProfile &&
(this.roomInfo.selfDevice?.status.isActive
? (this._generateTasks(), this._maybeUpdateOpenRooms(this.base.key))
: (this.changes.logout = !0)))
const e = this.roomInfo.selfMember,
t = this.currentRoomInfo?.selfMember
return (
e?.status.isRemoved && !t?.status.isRemoved && this._generatePrivateProfileDispatches(),
!0
)
}
run() {
return (this._running || ((this._running = this._run()), this._running.catch(t)), this._running)
}
}