async def _do_delete_invitation( conn, organization_id: OrganizationID, greeter: UserID, token: InvitationToken, on: DateTime, reason: InvitationDeletedReason, ): row = await conn.fetchrow( *_q_delete_invitation_info( organization_id=organization_id.str, greeter=greeter.str, token=token.uuid ) ) if not row: raise InvitationNotFoundError(token) row_id, deleted_on = row if deleted_on: raise InvitationAlreadyDeletedError(token) await conn.execute(*_q_delete_invitation(row_id=row_id, on=on, reason=reason.value)) await send_signal( conn, BackendEvent.INVITE_STATUS_CHANGED, organization_id=organization_id, greeter=greeter, token=token, status=InvitationStatus.DELETED, )
async def _do_delete_invitation( conn, organization_id: OrganizationID, greeter: UserID, token: UUID, on: Pendulum, reason: InvitationDeletedReason, ): row = await conn.fetchrow( *_q_delete_invitation_info(organization_id=organization_id, greeter=greeter, token=token) ) if not row: raise InvitationNotFoundError(token) row_id, deleted_on = row if deleted_on: raise InvitationAlreadyDeletedError(token) await conn.execute(*_q_delete_invitation(row_id=row_id, on=on, reason=reason.value)) await send_signal( conn, "invite.status_changed", organization_id=organization_id, greeter=greeter, token=token, status_str=InvitationStatus.DELETED.value, )
def _get_invitation_and_conduit( self, organization_id: OrganizationID, token: UUID, expected_greeter: Optional[UserID] = None, ) -> Tuple[Invitation, Conduit]: org = self._organizations[organization_id] invitation = org.invitations.get(token) if not invitation or (expected_greeter and invitation.greeter_user_id != expected_greeter): raise InvitationNotFoundError(token) if token in org.deleted_invitations: raise InvitationAlreadyDeletedError(token) return invitation, org.conduits[token]
async def info(self, organization_id: OrganizationID, token: InvitationToken) -> Invitation: async with self.dbh.pool.acquire() as conn: row = await conn.fetchrow( *_q_info_invitation(organization_id=organization_id.str, token=token.uuid) ) if not row: raise InvitationNotFoundError(token) ( type, greeter, greeter_human_handle_email, greeter_human_handle_label, claimer_email, created_on, deleted_on, deleted_reason, ) = row if deleted_on: raise InvitationAlreadyDeletedError(token) greeter_human_handle = None if greeter_human_handle_email: greeter_human_handle = HumanHandle( email=greeter_human_handle_email, label=greeter_human_handle_label ) if type == InvitationType.USER.value: return UserInvitation( greeter_user_id=UserID(greeter), greeter_human_handle=greeter_human_handle, claimer_email=claimer_email, token=token, created_on=created_on, status=InvitationStatus.READY, ) else: # Device return DeviceInvitation( greeter_user_id=UserID(greeter), greeter_human_handle=greeter_human_handle, token=token, created_on=created_on, status=InvitationStatus.READY, )
async def _conduit_listen(conn, ctx: ConduitListenCtx) -> Optional[bytes]: async with conn.transaction(): if ctx.is_greeter: row = await conn.fetchrow( *_q_conduit_greeter_info( organization_id=ctx.organization_id.str, greeter_user_id=ctx.greeter.str, # type: ignore[union-attr] token=ctx.token.uuid, ) ) else: row = await conn.fetchrow( *_q_conduit_claimer_info( organization_id=ctx.organization_id.str, token=ctx.token.uuid ) ) if not row: raise InvitationNotFoundError() row_id = row["_id"] curr_conduit_state = ConduitState(row["conduit_state"]) if row["deleted_on"]: raise InvitationAlreadyDeletedError() if ctx.is_greeter: curr_our_payload = row["conduit_greeter_payload"] curr_peer_payload = row["conduit_claimer_payload"] else: curr_our_payload = row["conduit_claimer_payload"] curr_peer_payload = row["conduit_greeter_payload"] if ctx.peer_payload is None: # We are waiting for the peer to provite it payload # Only peer payload should be allowed to change if curr_conduit_state != ctx.state or curr_our_payload != ctx.payload: raise InvitationInvalidStateError() if curr_peer_payload is not None: # Our peer has provided it payload (hence it knows # about our payload too), we can update the conduit # to the next state await conn.execute( *_q_conduit_update( row_id=row_id, conduit_state=NEXT_CONDUIT_STATE[ctx.state].value, conduit_greeter_payload=None, conduit_claimer_payload=None, ) ) await send_signal( conn, BackendEvent.INVITE_CONDUIT_UPDATED, organization_id=ctx.organization_id, token=ctx.token, ) return curr_peer_payload else: # We were waiting for the peer to take into account the # payload we provided. This would be done once the conduit # has switched to it next state. if curr_conduit_state == NEXT_CONDUIT_STATE[ctx.state] and curr_our_payload is None: return ctx.peer_payload elif ( curr_conduit_state != ctx.state or curr_our_payload != ctx.payload or curr_peer_payload != ctx.peer_payload ): # Something unexpected has changed in our back... raise InvitationInvalidStateError() # Peer hasn't answered yet, we should wait and retry later... return None
async def _conduit_talk( conn, organization_id: OrganizationID, greeter: Optional[UserID], token: InvitationToken, state: ConduitState, payload: bytes, ) -> ConduitListenCtx: is_greeter = greeter is not None async with conn.transaction(): # On top of retrieving the invitation row, this query lock the row # in the database for the duration of the transaction. # Hence concurrent request will be on hold until the end of the transaction. if is_greeter: row = await conn.fetchrow( *_q_conduit_greeter_info( organization_id=organization_id.str, greeter_user_id=greeter.str, # type: ignore[union-attr] token=token.uuid, ) ) else: row = await conn.fetchrow( *_q_conduit_claimer_info(organization_id=organization_id.str, token=token.uuid) ) if not row: raise InvitationNotFoundError(token) row_id = row["_id"] curr_conduit_state = ConduitState(row["conduit_state"]) curr_greeter_payload = row["conduit_greeter_payload"] curr_claimer_payload = row["conduit_claimer_payload"] if is_greeter: curr_our_payload = curr_greeter_payload curr_peer_payload = curr_claimer_payload else: curr_our_payload = curr_claimer_payload curr_peer_payload = curr_greeter_payload if row["deleted_on"]: raise InvitationAlreadyDeletedError(token) if curr_conduit_state != state or curr_our_payload is not None: # We are out of sync with the conduit: # - the conduit state has changed in our back (maybe reseted by the peer) # - we want to reset the conduit # - we have already provided a payload for the current conduit state (most # likely because a retry of a command that failed due to connection outage) if state == ConduitState.STATE_1_WAIT_PEERS: # We wait to reset the conduit curr_conduit_state = state curr_claimer_payload = None curr_greeter_payload = None curr_our_payload = None curr_peer_payload = None else: raise InvitationInvalidStateError() # Now update the conduit with our payload and send a signal if # the peer is already waiting for us. if is_greeter: curr_greeter_payload = payload else: curr_claimer_payload = payload await conn.execute( *_q_conduit_update( row_id=row_id, conduit_state=curr_conduit_state.value, conduit_greeter_payload=curr_greeter_payload, conduit_claimer_payload=curr_claimer_payload, ) ) # Note that in case of conduit reset, this signal will lure the peer into # thinking we have answered so he will wakeup and take into account the reset await send_signal( conn, BackendEvent.INVITE_CONDUIT_UPDATED, organization_id=organization_id, token=token ) return ConduitListenCtx( organization_id=organization_id, greeter=greeter, token=token, state=state, payload=payload, peer_payload=curr_peer_payload, )