Skip to content

Commit

Permalink
Bugfixes. TODO: remove log, etc., DRY sorting in identity, fix chat-m…
Browse files Browse the repository at this point in the history
…essage and chat-scrolling tests
  • Loading branch information
corrideat committed Dec 21, 2023
1 parent 4e2546e commit ef99263
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 452 deletions.
23 changes: 21 additions & 2 deletions frontend/controller/actions/chatroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export default (sbp('sbp/selectors/register', {
name: 'group-csk',
purpose: ['sig'],
ringLevel: 2,
permissions: [GIMessage.OP_ACTION_ENCRYPTED],
permissions: [GIMessage.OP_ATOMIC, GIMessage.OP_KEY_DEL, GIMessage.OP_ACTION_ENCRYPTED],
allowedActions: ['gi.contracts/chatroom/leave'],
foreignKey: params.options.groupKeys[0].foreignKey,
meta: params.options.groupKeys[0].meta,
Expand All @@ -119,7 +119,7 @@ export default (sbp('sbp/selectors/register', {
name: 'group-cek',
purpose: ['enc'],
ringLevel: 2,
permissions: [GIMessage.OP_ACTION_ENCRYPTED],
permissions: [GIMessage.OP_ATOMIC, GIMessage.OP_KEY_ADD, GIMessage.OP_KEY_DEL, GIMessage.OP_ACTION_ENCRYPTED],
allowedActions: ['gi.contracts/chatroom/join', 'gi.contracts/chatroom/leave'],
foreignKey: params.options.groupKeys[1].foreignKey,
meta: params.options.groupKeys[1].meta,
Expand Down Expand Up @@ -259,6 +259,25 @@ export default (sbp('sbp/selectors/register', {
const hooks = {
...params?.hooks,
preSendCheck (msg, state) {
const x = document.createElement('pre')
x.innerText = JSON.stringify({
c: params.contractID,
m: params.data.member,
s: state.users?.[params.data.member] ?? '---',
sx: state.users,
db: sbp('state/vuex/state').contracts[params.contractID]
}, undefined, 2)
Object.assign(x.style, {
position: 'absolute',
color: '#fff',
background: '#000',
padding: '2px',
border: '1px solid red',
top: '0',
left: '0',
pointerEvents: 'none'
})
document.body?.appendChild(x)
console.error('cL', params.contractID, params.data.member, state.users?.[params.data.member])
// Avoid sending a duplicate action if the person isn't a
// chatroom member
Expand Down
26 changes: 22 additions & 4 deletions frontend/controller/actions/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,15 +281,20 @@ export default (sbp('sbp/selectors/register', {
return deriveKeyFromPassword(CURVE25519XSALSA20POLY1305, password, salt + stateEncryptionKeyId)
}))

const contractIDs = []
const contractIDs = Object.create(null)
// login can be called when no settings are saved (e.g. from Signup.vue)
if (state) {
// The retrieved local data might need to be completed in case it was originally saved
// under an older version of the app where fewer/other Vuex modules were implemented.
sbp('state/vuex/postUpgradeVerification', state)
sbp('state/vuex/replace', state)
sbp('chelonia/pubsub/update') // resubscribe to contracts since we replaced the state
contractIDs.push(...Object.keys(state.contracts))
Object.entries(state.contracts).forEach(([id, { type }]) => {
if (!contractIDs[type]) {
contractIDs[type] = []
}
contractIDs[type].push(id)
})
}

await sbp('gi.db/settings/save', SETTING_CURRENT_USER, identityContractID)
Expand Down Expand Up @@ -334,7 +339,19 @@ export default (sbp('sbp/selectors/register', {

throw new Error('Unable to sync identity contract')
}).then(() => {
return sbp('chelonia/contract/sync', contractIDs, { force: true })
// $FlowFixMe[incompatible-call]
return Promise.all(Object.entries(contractIDs).sort(([a], [b]) => {
if (a === b) return 0
if (a === 'gi.contracts/identity') return -1
if (b === 'gi.contracts/identity') return 1
if (a === 'gi.contracts/group') return -1
if (b === 'gi.contracts/group') return 1
if (a === 'gi.contracts/chatroom') return -1
if (b === 'gi.contracts/chatroom') return 1
return 0
}).map(([, ids]) => {
return sbp('okTurtles.eventQueue/queueEvent', `login:${identityContractID ?? '(null)'}`, ['chelonia/contract/sync', ids, { force: true }])
}))
.catch((err) => {
console.error('Error during contract sync upon login (syncing all contractIDs)', err)
})
Expand All @@ -347,7 +364,8 @@ export default (sbp('sbp/selectors/register', {

// contract sync might've triggered an async call to /remove, so
// wait before proceeding
await sbp('chelonia/contract/wait', Array.from(new Set([...groupIds, ...contractIDs])))
// $FlowFixMe[incompatible-call]
await sbp('chelonia/contract/wait', Array.from(new Set([...groupIds, ...Object.values(contractIDs).flat()])))

// Call 'gi.actions/group/join' on all groups which may need re-joining
await Promise.allSettled(
Expand Down
44 changes: 29 additions & 15 deletions frontend/model/contracts/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -1066,8 +1066,9 @@ sbp('chelonia/defineContract', {
// They MUST NOT call 'commit'!
// They should only coordinate the actions of outside contracts.
// Otherwise `latestContractState` and `handleEvent` will not produce same state!
sideEffect ({ meta, contractID }, { state }) {
sideEffect ({ meta, contractID, innerSigningContractID }, { state }) {
const { loggedIn } = sbp('state/vuex/state')
console.error('@@@AT inviteAccept for', { contractID, innerSigningContractID, meta })

sbp('chelonia/queueInvocation', contractID, async () => {
const rootState = sbp('state/vuex/state')
Expand All @@ -1082,21 +1083,22 @@ sbp('chelonia/defineContract', {
const { profiles = {} } = state

if (profiles[meta.username].status !== PROFILE_STATUS.ACTIVE) {
console.error('@@@RETURNING AS PROFILE_STATUS.ACTIVE IS FALSE', { contractID, innerSigningContractID, meta })
return
}

const userID = loggedIn.identityContractID

// TODO: per #257 this will ,have to be encompassed in a recoverable transaction
// however per #610 that might be handled in handleEvent (?), or per #356 might not be needed
if (meta.username === loggedIn.username) {
if (innerSigningContractID === userID) {
// we're the person who just accepted the group invite

const userID = loggedIn.identityContractID

// Add the group's CSK to our identity contract so that we can receive
// DMs.
await sbp('gi.actions/identity/addJoinDirectMessageKey', userID, contractID, 'csk')

const generalChatRoomId = state.generalChatRoomId
console.error('@@@WILL JOIN GC', { contractID, innerSigningContractID, meta, generalChatRoomId })
if (generalChatRoomId) {
// Join the general chatroom
if (state.chatRooms[generalChatRoomId]?.users?.[loggedIn.username]?.status !== PROFILE_STATUS.ACTIVE) {
Expand All @@ -1120,6 +1122,8 @@ sbp('chelonia/defineContract', {
alert(L("Couldn't join the #{chatroomName} in the group. An error occurred: #{error}.", { chatroomName: CHATROOM_GENERAL_NAME, error: e?.message || e }))
}, 0)
})
} else {
console.error('@@@ALREADY JOINED JC', { contractID, innerSigningContractID, meta, generalChatRoomId })
}
} else {
// setTimeout to avoid blocking the main thread
Expand Down Expand Up @@ -1387,7 +1391,14 @@ sbp('chelonia/defineContract', {
// Abort the joining action if it's been initiated. This way, calling /remove on the leave action will work
if (sbp('okTurtles.data/get', `JOINING_CHATROOM-${data.chatRoomID}-${data.member}`)) {
sbp('okTurtles.data/delete', `JOINING_CHATROOM-${data.chatRoomID}-${data.member}`)
sbp('chelonia/contract/remove', data.chatRoomID).catch((e) => {
sbp('chelonia/contract/remove', data.chatRoomID).then(() => {
const rootState = sbp('state/vuex/state')
if (rootState.currentChatRoomIDs[contractID] === data.chatRoomID) {
sbp('state/vuex/commit', 'setCurrentChatRoomId', {
groupId: contractID
})
}
}).catch((e) => {
console.error(`[gi.contracts/group/leaveChatRoom/sideEffect] Error calling remove for ${contractID} on chatroom ${data.chatRoomID}`, e)
})
}
Expand Down Expand Up @@ -1424,7 +1435,7 @@ sbp('chelonia/defineContract', {
// If we added someone to the chatroom (including ourselves), we issue
// the relevant action to the chatroom contract
if (innerSigningContractID === rootState.loggedIn.identityContractID) {
sbp('chelonia/queueInvocation', contractID, () => sbp('gi.contracts/group/joinGroupChatrooms', contractID, data.chatRoomID, username, rootState.loggedIn.username)).catch((e) => {
sbp('chelonia/queueInvocation', contractID, () => sbp('gi.contracts/group/joinGroupChatrooms', contractID, data.chatRoomID, username)).catch((e) => {
console.error(`[gi.contracts/group/joinChatRoom/sideEffect] Error adding member to group chatroom for ${contractID}`, { e, data })
})
} else if (username === rootState.loggedIn.username) {
Expand Down Expand Up @@ -1748,20 +1759,20 @@ sbp('chelonia/defineContract', {
})
}
},
'gi.contracts/group/joinGroupChatrooms': async function (contractID, chatRoomId, member, loggedInUsername) {
'gi.contracts/group/joinGroupChatrooms': async function (contractID, chatRoomId, member) {
const rootState = sbp('state/vuex/state')
const state = rootState[contractID]
const username = rootState.loggedIn.username

if (loggedInUsername !== username || state?.profiles?.[username]?.status !== PROFILE_STATUS.ACTIVE || state?.chatRooms?.[chatRoomId]?.users[member]?.status !== PROFILE_STATUS.ACTIVE) {
if (state?.profiles?.[username]?.status !== PROFILE_STATUS.ACTIVE || state.chatRooms?.[chatRoomId]?.users[member]?.status !== PROFILE_STATUS.ACTIVE) {
return
}

try {
await sbp('chelonia/contract/sync', chatRoomId, { deferredRemove: true })

if (!sbp('chelonia/contract/hasKeysToPerformOperation', chatRoomId, 'gi.contracts/chatroom/join')) {
return
throw new Error(`Missing keys to join chatroom ${chatRoomId}`)
}

// Using the group's CEK allows for everyone to have an overview of the
Expand All @@ -1772,14 +1783,17 @@ sbp('chelonia/defineContract', {
await sbp('gi.actions/chatroom/join', {
contractID: chatRoomId,
data: { username: member },
encryptionKeyId
encryptionKeyId,
...username === member && {
hooks: {
onprocessed: () => {
sbp('state/vuex/commit', 'setCurrentChatRoomId', { groupId: contractID, chatRoomId })
}
}
}
}).catch(e => {
console.error(`Unable to join ${member} to chatroom ${chatRoomId} for group ${contractID}`, e)
})

if (username === member) {
sbp('state/vuex/commit', 'setCurrentChatRoomId', { groupId: contractID, chatRoomId })
}
} finally {
await sbp('chelonia/contract/remove', chatRoomId, { removeIfPending: true })
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/model/contracts/manifests.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifests": {
"gi.contracts/chatroom": "21XWnNHH2e67b1HMpFupf1EmSWH6ATaEN8yDL73d7nHKyduJru",
"gi.contracts/group": "21XWnNU6B4j7vVX2pYDtDmCTb6U7S3C9q4ijfZkberWfn7dMcT",
"gi.contracts/group": "21XWnNVMvDKnsaAr4Sqo328irkQHF3rSigpLdtxcRpUN8HCgG8",
"gi.contracts/identity": "21XWnNK49ogfNcAQfanvYPyEbGHRyekriVhAegxAvnWgZyLL8b"
}
}
6 changes: 3 additions & 3 deletions frontend/views/containers/chatroom/ChatMain.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

emoticons

template(v-for='x in latestEvents')
div
pre(style='overflow-wrap: anywhere; white-space: pre-wrap; font-size: 0.8em') {{ JSON.stringify({...GIMessage.deserializeHEAD(x).head, version: void 0, manifest: void 0 }) }}
div(style='height: 0; overflow: visible')
div(v-for='x in latestEvents')
pre(style='background: #000; color: #fff; overflow-wrap: anywhere; white-space: pre-wrap; font-size: 0.9em') {{ JSON.stringify({...GIMessage.deserializeHEAD(x).head, version: void 0, manifest: void 0 }) }}

.c-body
.c-body-conversation(
Expand Down
4 changes: 2 additions & 2 deletions frontend/views/containers/chatroom/ChatMembersAllModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ import ProfileCard from '@components/ProfileCard.vue'
import ButtonSubmit from '@components/ButtonSubmit.vue'
import DMMixin from './DMMixin.js'
import GroupMembersTooltipPending from '@containers/dashboard/GroupMembersTooltipPending.vue'
import { CHATROOM_PRIVACY_LEVEL } from '@model/contracts/shared/constants.js'
import { CHATROOM_PRIVACY_LEVEL, PROFILE_STATUS } from '@model/contracts/shared/constants.js'
import { uniq } from '@model/contracts/shared/giLodash.js'
import { filterByKeyword } from '@view-utils/filters.js'
Expand Down Expand Up @@ -207,7 +207,7 @@ export default ({
return this.isJoined
? this.chatRoomUsersInSort
: this.groupMembersSorted
.filter(member => this.getGroupChatRooms[this.currentChatRoomId].users.includes(member.username))
.filter(member => this.getGroupChatRooms[this.currentChatRoomId].users[member.username]?.status === PROFILE_STATUS.ACTIVE)
.map(member => ({ username: member.username, displayName: member.displayName }))
}
},
Expand Down
89 changes: 86 additions & 3 deletions shared/domains/chelonia/internals.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export default (sbp('sbp/selectors/register', {
// We always call recreateEvent because we may have received new events
// in the web socket
if (!isFirstMessage) {
return recreateEvent(entry, state)
return recreateEvent(entry, state, rootState.contracts[contractID])
}

return entry
Expand Down Expand Up @@ -401,7 +401,7 @@ export default (sbp('sbp/selectors/register', {
// TODO: The [pubsub] code seems to miss events that happened between
// a call to sync and the subscription time. This is a temporary measure
// to handle this until [pubsub] is updated.
if (entry.height() === lastAttemptedHeight) {
if (!entry.isFirstMessage() && entry.height() === lastAttemptedHeight) {
await sbp('chelonia/contract/sync', contractID, { force: true })
}
} else {
Expand Down Expand Up @@ -487,7 +487,14 @@ export default (sbp('sbp/selectors/register', {
if (!state._vm) config.reactiveSet(state, '_vm', Object.create(null))
const opFns: { [GIOpType]: (any) => void } = {
[GIMessage.OP_ATOMIC] (v: GIOpAtomic) {
v.forEach((u) => opFns[u[0]](u[1]))
v.forEach((u) => {
if (u[0] === GIMessage.OP_ATOMIC) throw new Error('Cannot nest OP_ATOMIC')
if (!validateKeyPermissions(state, signingKeyId, u[0], u[1])) {
throw new Error('Inside OP_ATOMIC: no matching signing key was defined')
}
console.error('@@@OP_ATOMIC', u, state)
opFns[u[0]](u[1])
})
},
[GIMessage.OP_CONTRACT] (v: GIOpContract) {
state._vm.type = v.type
Expand Down Expand Up @@ -1451,6 +1458,23 @@ export default (sbp('sbp/selectors/register', {
try {
await handleEvent.processMutation.call(this, message, contractStateCopy, internalSideEffectStack)
} catch (e) {
if (document && state.contracts[contractID]?.type === 'gi.contracts/chatroom') {
const x = document.createElement('pre')
x.innerText = JSON.stringify({ c: contractID, cs: state.contracts[contractID], s: state[contractID].users, en: e?.name, d: e?.message }, undefined, 2)
Object.assign(x.style, {
position: 'absolute',
color: '#00f',
background: '#fff',
padding: '2px',
border: '1px solid red',
top: '0',
right: '0',
pointerEvents: 'none',
zIndex: '9999'
})
document.body?.appendChild(x)
}

if (e?.name === 'ChelErrorDecryptionKeyNotFound') {
console.warn(`[chelonia] WARN '${e.name}' in processMutation for ${message.description()}: ${e.message}`, e, message.serialize())
} else {
Expand Down Expand Up @@ -1488,11 +1512,70 @@ export default (sbp('sbp/selectors/register', {
// an inconsistent state if a sudden failure happens while this code
// is executing. In particular, everything in between should be synchronous.
try {
const state = sbp(this.config.stateSelector)
if (document && state.contracts[contractID]?.type === 'gi.contracts/chatroom') {
const x = document.createElement('pre')
x.innerText = JSON.stringify({ c: contractID, cs: state.contracts[contractID], s: state[contractID].users }, undefined, 2)
Object.assign(x.style, {
position: 'absolute',
color: '#0f0',
background: '#000',
padding: '2px',
border: '1px solid red',
top: '0',
right: '0',
pointerEvents: 'none',
zIndex: '9999'
})
document.body?.appendChild(x)
}

handleEvent.applyProccessResult.call(this, { message, state, contractState: contractStateCopy, processingErrored, postHandleEvent })
} catch (e) {
const x = document.createElement('pre')
x.innerText = JSON.stringify({
m: JSON.parse(JSON.parse(rawMessage).head),
s: e.stack,
e: e.message,
c: contractID,
db: sbp('state/vuex/state').contracts[contractID]
}, undefined, 2)
Object.assign(x.style, {
position: 'absolute',
color: '#000',
background: '#fff',
padding: '2px',
border: '1px solid red',
top: '0',
right: '0',
pointerEvents: 'none',
zIndex: '9999'
})
document.body?.appendChild(x)

console.error(`[chelonia] ERROR '${e.name}' for ${message.description()} marking the event as processed: ${e.message}`, e, { message: message.serialize() })
}
} catch (e) {
const x = document.createElement('pre')
x.innerText = JSON.stringify({
m: rawMessage,
s: e.stack,
e: e.message,
c: contractID,
db: sbp('state/vuex/state').contracts[contractID]
}, undefined, 2)
Object.assign(x.style, {
position: 'absolute',
color: '#fff',
background: '#000',
padding: '2px',
border: '1px solid red',
top: '0',
right: '0',
pointerEvents: 'none'
})
document.body?.appendChild(x)

console.error(`[chelonia] ERROR in handleEvent: ${e.message || e}`, e)
try {
handleEventError?.(e, message)
Expand Down
Loading

0 comments on commit ef99263

Please sign in to comment.