const e = require("keet-identity-key"),
i = require("tiny-buffer-map"),
t = require("b4a"),
{ MemberSchema: s } = require("@holepunchto/keet-core-schemas"),
{
MemberCapabilities: a,
MemberRoles: r,
getCapabilitiesForRole: n,
GENERIC_EVENTS: c,
FEATURES: m,
VALIDATION: o,
} = require("@holepunchto/keet-core-api"),
{
UpdateMemberOperationV1: l,
UpdateMemberOperationV2: y,
RemoveMemberOperation: b,
ChangeMemberRoleOperation: u,
MuteMemberOperation: v,
UnmuteMemberOperation: d,
} = s.Operations,
p = 0,
h = t.alloc(0)
function M(i, t, s) {
return e.verify(i.proof, t, { expectedIdentity: i.memberKey, receipt: s }) || null
}
function f(e, i, t, { latestReceipt: s } = {}) {
return {
key: e,
hasAvatar: !!i?.hasAvatar || !!t?.hasAvatar,
displayName: i?.displayName || t?.displayName,
latestReceipt: t?.latestReceipt || s,
devices: w(i, t),
capabilities: g(i, t),
}
}
function w(e, s) {
if (!e && !s) return []
if (!s) return e.devices ? e.devices : [e.key]
if (!e) return s.devices ? s.devices : [s.key]
const a = new i()
if (e.devices) {
for (const i of e.devices) a.set(i, h)
for (const e of s.devices) a.set(e, h)
} else {
for (const e of s.devices) a.set(e, h)
a.set(e.key, h)
}
const r = [...a.keys()]
return (r.sort(t.compare), r)
}
function g(e, i) {
return e || i ? (e ? (i ? e.capabilities | i.capabilities : e.capabilities) : i.capabilities) : 0
}
function K(e) {
return 0 !== (e & a.CAN_MODERATE)
}
function E(e, i) {
return !e || -1 === e.devices.findIndex((e) => t.equals(e, i))
}
function D(e) {
if (!e.inactive) return !1
if (e.inactive.length !== e.devices.length) return !1
for (let i = 0; i < e.inactive.length; i++) if (!t.equals(e.inactive[i], e.devices[i])) return !1
return !0
}
function _(e, i) {
if (e.length !== i.length) return !1
for (let s = 0; s < e.length; s++) if (!t.equals(e[s], i[s])) return !1
return !0
}
module.exports = class {
static Operations = new Set([l.TYPE, y.TYPE, b.TYPE, u.TYPE, v.TYPE, d.TYPE])
constructor(e) {
;((this.view = e),
(this.chat = null),
(this.devices = null),
(this.config = null),
(this.members = null),
(this.avatars = null),
(this.pendingMembers = null),
(this.inactiveMembers = null),
(this.mutedMembers = null),
(this.membersKeysByMemberName = null),
(this.membersByDeviceKey = null),
(this.inactiveMembersByDeviceKey = null),
(this.capabilitiesByDeviceKey = null),
(this.moderatorsByMemberKey = null),
(this.membersCount = null))
}
initialize() {
;((this.chat = this.view.chat),
(this.devices = this.view.devices),
(this.config = this.view.config),
(this.members = this.view.collections.members),
(this.avatars = this.view.collections.avatars),
(this.pendingMembers = this.view.collections.pendingMembers),
(this.inactiveMembers = this.view.collections.inactiveMembers),
(this.mutedMembers = this.view.collections.mutedMembers),
(this.memberKeysByMemberName = this.view.indexes.memberKeysByMemberName),
(this.membersByDeviceKey = this.view.indexes.membersByDeviceKey),
(this.inactiveMembersByDeviceKey = this.view.indexes.inactiveMembersByDeviceKey),
(this.capabilitiesByDeviceKey = this.view.indexes.capabilitiesByDeviceKey),
(this.moderatorsByMemberKey = this.view.indexes.moderatorsByMemberKey),
(this.membersCount = this.view.indexes.membersCount))
}
_migratePendingMember(e, i, s) {
if (e.attestation)
return s && t.equals(s.memberKey, e.attestation.memberKey)
? f(s.memberKey, e, i, { latestReceipt: e.attestation.receipt })
: null
if (s) {
if (!s.proof) return null
const t = M(s, e.key, i?.latestReceipt)
return f(t ? s.memberKey : e.key, e, t ? i : null, { latestReceipt: t ? t.receipt : null })
}
return f(e.key, e, null)
}
_migrateExistingMember(e, i, s) {
if (!s.proof || t.equals(e.key, s.memberKey)) return null
const a = M(s, e.key, i?.latestReceipt)
return a ? f(s.memberKey, e, i, { latestReceipt: a.receipt }) : null
}
async _maybeReactivateMember(e) {
const i = await this.getInactiveMemberForDeviceKey(e, { full: !0 })
if (i) {
const s = i.devices.findIndex((i) => t.equals(i, e))
;-1 !== s &&
(i.devices.splice(s, 1),
i.devices.length > 0
? await this.inactiveMembers.update(i.key, i, { upsert: !0 })
: await this.inactiveMembers.delete(i.key))
}
}
async _applyUpdateMemberOperation(e, { id: i, timestamp: t }) {
const { displayName: s, avatar: a, identity: r } = e.value,
[n, l, y, b, u] = await Promise.all([
this.getPendingMemberForDeviceKey(i.key),
this.getMemberForDeviceKey(i.key, { full: !0 }),
r ? this.getMember(r.memberKey) : Promise.resolve(),
this.view.internal.getCodeVersion(),
this.view.isFeatureSupported(m.DISPLAY_NAME_VALIDATION),
])
let v = l,
d = s
if (
(u &&
s &&
(s.length < o.MIN_USERNAME_LENGTH || s.length > o.MAX_USERNAME_LENGTH) &&
(d = v.displayName),
n)
) {
if (
(b >= 2 && (await this._maybeReactivateMember(i.key)),
(v = this._migratePendingMember(n, y, r)),
!v)
)
return
await this.pendingMembers.delete(i.key)
} else if (r && E(y, i.key)) {
if (((v = this._migrateExistingMember(l, y, r)), !v)) return
await this.members.delete(l.key)
}
a && (await this.avatars.update(v.key, a, { upsert: !0 }))
const p = { ...v, timestamp: t, displayName: d || v.displayName, hasAvatar: !!a || v.hasAvatar }
if ((await this.members.update(p.key, p, { upsert: !0 }), n && !y)) {
const [e, s] = await Promise.all([
this.config.isBroadcastRoom(),
this.config.isDisableMembershipEvents(),
])
if (!e && !s) {
const e = { id: i, timestamp: t, event: { type: c.MEMBER_JOINED } }
await this.chat.addMessage(e)
}
}
}
async _applyRemoveMemberOperation(e, { id: t, timestamp: s }) {
const { memberKey: r } = e.value,
[n, o, l, y] = await Promise.all([
this.isModerator(t.key),
this.getMemberForDeviceKey(t.key),
this.getMember(r),
this.view.isFeatureSupported(m.REMOVE_MEMBER),
])
if (!o || !l || !y) return
if (!n) return
const b = 0 !== (o.capabilities & a.CAN_INDEX)
if (0 !== (l.capabilities & a.CAN_INDEX) && !b) return
const u = new i()
for (const e of l.inactive || []) u.set(e, h)
let v = !1
for await (const e of l.devices) {
if (u.get(e)) continue
const i = await this.devices.maybeDeleteDevice(e)
;(i && (await this.removeMemberForDeviceKey({ key: e }, s, { sendLeftEvent: !1 })), (v = i))
}
v &&
(await this.chat.addMessage({
id: t,
timestamp: s,
event: { type: c.MEMBER_REMOVED, value: { memberKey: r } },
}))
}
async _applyChangeMemberRoleOperation(e, { id: i, timestamp: t }) {
const { memberKey: s, role: c } = e.value
if (![r.MODERATOR, r.PEER].includes(c)) return
const [o, l, y, b, u] = await Promise.all([
this.isModerator(i.key),
this.getMemberForDeviceKey(i.key),
this.moderatorsByMemberKey.get(s),
this.getMember(s),
this.view.isFeatureSupported(m.PERMISSION_DOWNGRADE),
])
if (!l || !b || !u) return
const v = !!y
if (!o) return
const d = 0 !== (l.capabilities & a.CAN_INDEX),
p = 0 !== (b.capabilities & a.CAN_INDEX)
if (p && !d) return
if (!p && v && r.PEER !== c) return
if (!p && !v && c !== r.MODERATOR) return
const h = n(c),
M = await Promise.all(b.devices.map((e) => this.devices.getDevice(e)))
if (!M.some((e) => 0 !== (e.capabilities & a.CAN_INDEX) && !this.view.base.removeable(e.key))) {
for await (const e of M) {
const { key: i } = e
;(0 !== (e.capabilities & a.CAN_INDEX) &&
this.view.base.removeable(i) &&
(await this.view.base.addWriter(i, { indexer: !1 })),
await this.devices.updateDevice(i, { ...e, capabilities: h }))
}
await this.members.update(b.key, { ...b, timestamp: t, capabilities: h }, { upsert: !0 })
}
}
async _applyMuteMemberOperation(e, { id: i, timestamp: t }) {
const { memberKey: s } = e.value,
[a, r] = await Promise.all([
this.view.isFeatureSupported(m.MUTE_MEMBER),
this.isModerator(i.key),
])
a && r && (await this.mutedMembers.update(s, h, { upsert: !0 }))
}
async _applyUnmuteMemberOperation(e, { id: i, timestamp: t }) {
const { memberKey: s } = e.value,
[a, r] = await Promise.all([
this.view.isFeatureSupported(m.MUTE_MEMBER),
this.isModerator(i.key),
])
a && r && (await this.mutedMembers.get(s)) && (await this.mutedMembers.delete(s))
}
async _indexMembersCount(e, i) {
if (e && i) return
const t = await this.membersCount.get(0)
let s = t ? t.value : 0
;(e && (s -= 1), i && (s += 1), await this.membersCount.insert(0, s))
}
async _indexMemberKeysByMemberName(e, i, t) {
;(e && i && e.displayName === i.displayName) ||
(e &&
e.displayName &&
(await this.memberKeysByMemberName.delete([e.displayName.toLowerCase(), t])),
i &&
i.displayName &&
(await this.memberKeysByMemberName.insert([i.displayName.toLowerCase(), t])))
}
async _indexMembersByDeviceKey(e, i) {
if (e) for (const i of e.devices) await this.membersByDeviceKey.delete(i)
if (i) for (const e of i.devices) await this.membersByDeviceKey.insert(e, i)
}
async _indexInactiveMembersByDeviceKey(e, i) {
if (e) for (const i of e.devices) await this.inactiveMembersByDeviceKey.delete(i)
if (i) for (const e of i.devices) await this.inactiveMembersByDeviceKey.insert(e, i)
}
async _indexCapabilitiesByDeviceKey(e, i) {
if (!(i && e && e.capabilities === i.capabilities && _(e.devices, i.devices))) {
if (e) for (const i of e.devices) await this.capabilitiesByDeviceKey.delete(i)
if (i) for (const e of i.devices) await this.capabilitiesByDeviceKey.insert(e, i.capabilities)
}
}
async _indexModeratorsByMemberKey(e, i) {
;(e && i && K(e.capabilities) && K(i.capabilities)) ||
(e && K(e.capabilities) && (await this.moderatorsByMemberKey.delete(e.key)),
i && K(i.capabilities) && (await this.moderatorsByMemberKey.insert(i.key, h)))
}
async addPendingMember(e) {
;((await this.view.internal.getCodeVersion()) < 2 && (await this._maybeReactivateMember(e.key)),
await this.pendingMembers.update(
e.key,
{ invitation: e.invitation, capabilities: e.capabilities, attestation: e.attestation },
{ upsert: !0 },
))
}
async updateExistingMember(e) {
const i = await this.getMemberForDeviceKey(e.key, { full: !0 })
if (i)
return this.members.update(
i.key,
{ ...i, capabilities: i.capabilities | e.capabilities },
{ upsert: !0 },
)
}
async getPendingMemberForDeviceKey(e) {
const i = await this.pendingMembers.get(e)
return i ? { key: i._id, ...i.value } : null
}
async getMemberForDeviceKey(e, { full: i = !1 } = {}) {
const t = await this.membersByDeviceKey.get(e)
if (!t) return null
const s = t.value
return i ? this.getMember(s.key) : { ...s, deviceKey: e }
}
async getInactiveMemberForDeviceKey(e, { full: i = !1 } = {}) {
const t = await this.inactiveMembersByDeviceKey.get(e)
if (!t) return null
const s = t.value
return i ? this.getInactiveMember(s.key) : { ...s, deviceKey: e }
}
async removeMemberForDeviceKey(e, i, { sendLeftEvent: s = !0 } = {}) {
const a = e.key,
[r, n, m] = await Promise.all([
this.getMemberForDeviceKey(a, { full: !0 }),
this.getInactiveMemberForDeviceKey(a, { full: !0 }),
this.getPendingMemberForDeviceKey(a),
])
if (n || !r) return
if (
(m && (await this.pendingMembers.delete(a)),
r.inactive
? -1 === r.inactive.findIndex((e) => t.equals(e, a)) && r.inactive.push(a)
: (r.inactive = [a]),
r.inactive.sort(t.compare),
await this.members.update(r.key, r, { upsert: !0 }),
!D(r))
)
return
;(await this.members.delete(r.key), await this.inactiveMembers.update(r.key, r, { upsert: !0 }))
const [o, l, y] = await Promise.all([
this.config.isBroadcastRoom(),
this.view.internal.getCodeVersion(),
this.config.isDisableMembershipEvents(),
])
if ((l < 2 || !o) && s && !y) {
const t = { id: e, timestamp: i, event: { type: c.MEMBER_LEFT } }
await this.chat.addMessage(t)
}
}
async getMember(e) {
const i = await this.members.get(e)
return i?.value
}
async getInactiveMember(e) {
const i = await this.inactiveMembers.get(e)
return i?.value
}
async isDeviceMuted(e) {
const [i, t] = await Promise.all([
this.getMemberForDeviceKey(e),
this.getInactiveMemberForDeviceKey(e),
])
return !(!i && !t) && this.isMemberMuted(i?.key || t?.key)
}
async isMemberMuted(e) {
return !!(await this.mutedMembers.get(e))
}
async isModerator(e) {
if (t.equals(e, this.view.base.key)) return !0
const i = await this.capabilitiesByDeviceKey.get(e)
return K(i?.value)
}
autobaseApply(e, i) {
switch (e.type) {
case l.TYPE:
case y.TYPE:
return this._applyUpdateMemberOperation(e, i)
case b.TYPE:
return this._applyRemoveMemberOperation(e, i)
case u.TYPE:
return this._applyChangeMemberRoleOperation(e, i)
case v.TYPE:
return this._applyMuteMemberOperation(e, i)
case d.TYPE:
return this._applyUnmuteMemberOperation(e, i)
}
}
async autobaseIndexMembers({ prev: e, next: i, id: t }) {
;(await this._indexMemberKeysByMemberName(e, i, t),
await this._indexMembersByDeviceKey(e, i, t),
await this._indexCapabilitiesByDeviceKey(e, i, t),
await this._indexModeratorsByMemberKey(e, i, t),
await this._indexMembersCount(e, i))
}
async autobaseIndexInactiveMembers({ prev: e, next: i, id: t }) {
await this._indexInactiveMembersByDeviceKey(e, i, t)
}
}