exchange contact with QRcode

master
Niko PLP 10 hours ago
parent 6fe3624ba0
commit a9ddf9ad32
  1. 15
      ng-app/src/apps/ContactEditor.svelte
  2. 107
      ng-app/src/apps/ProfileEditor.svelte
  3. 40
      ng-app/src/apps/ProfileQrCode.svelte
  4. 1
      ng-app/src/locales/en.json
  5. 45
      ng-net/src/types.rs
  6. 1
      ng-repo/src/errors.rs
  7. 63
      ng-verifier/src/inbox_processor.rs
  8. 117
      ng-verifier/src/request_processor.rs
  9. 6
      ng-verifier/src/verifier.rs

@ -30,6 +30,7 @@
check_has_camera,
toast_error,
display_error,
online
} from "../store";
import { onDestroy, onMount, tick } from "svelte";
@ -95,21 +96,25 @@
}
</script>
<div class="flex-col p-5" bind:this={container}>
<div class="flex-col p-5"bind:this={container}>
<h1 class="font-bold text-xl text-blue-700">Contact</h1>
{#if !has_camera && !has_name}
<Alert class="m-2" color="red">No camera available. You cannot import with QR-code</Alert>
<Alert class="m-2" color="red" style="word-break: break-word;" >No camera available. You cannot import with QR-code</Alert>
{/if}
{#if !has_name}
{#if !has_name && has_camera}
<Button
on:click={test}
on:click={open_scanner}
on:keypress={open_scanner}
disabled={!$online}
class="select-none ml-2 mt-2 mb-2 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-500/50 rounded-lg text-base p-2 text-center inline-flex items-center dark:focus:ring-primary-700/55"
>
<QrCode tabindex="-1" class="mr-2 focus:outline-none" />
Import with QR-code
</Button><br/>
{/if}
Name: {has_name || ""}<br/>
{#if has_name}
Name: {has_name}<br/>
{/if}
{#if has_email}
Email: {has_email}<br/>
{/if}

@ -23,66 +23,87 @@
import {
openModalCreate,
sparql_query,
active_session
active_session,
toast_error,
display_error,
toast_success,
} from "../store";
import {
Clipboard
CheckCircle,
ArrowLeft
} from "svelte-heros-v2";
export let commits;
let name = "";
let email = "";
let readonly = false;
$: valid = name.trim().length > 1 && email.trim().length > 6 && email.indexOf("@") >= 0 && email.indexOf("\"") < 0;
function contained(graph) {
let ret = [];
for (const g of graph) {
if (g.substring(57,90) === "http://www.w3.org/ns/ldp#contains") {
let nuri = g.substring(93,146);
let repo = nuri;
nuri = nuri + ":" + $cur_tab.store.overlay;
let hash = nuri.substring(9,16);
ret.push({nuri,hash,repo});
if (g.substring(57,91) === "http://www.w3.org/2006/vcard/ns#fn") {
name = g.substring(94, g.length-1);
readonly = true;
} else if (g.substring(57,97) === "http://www.w3.org/2006/vcard/ns#hasEmail") {
email = g.substring(100, g.length-1);
readonly = true;
}
}
ret.sort((a, b) => a.hash.localeCompare(b.hash));
return ret;
}
async function fetch_header(repo) {
$: if (commits) { contained(commits.graph) }
async function save() {
try {
let res = await ng.fetch_header($active_session.session_id, repo);
return res;
}catch(e){
console.error(e);
return {};
console.log($cur_tab.doc.nuri);
//TODO: more sanitation on the input here!
await ng.sparql_update($active_session.session_id, "PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>"
+"INSERT DATA { <> a vcard:Individual . <> vcard:fn \""+name.replace('"',"\\\"")+"\". <> vcard:hasEmail \""+email+"\" }", "did:ng:"+$cur_tab.doc.nuri );
toast_success("Your profile was edited successfully!");
set_view_or_edit(true);
} catch (e) {
toast_error(display_error(e));
}
}
const create = () => {
openModalCreate();
function cancel() {
set_view_or_edit(true);
}
const config = {
class: "mr-2 w-6 h-6 shrink-0 focus:outline-none"
}
</script>
<div class="flex-col p-5">
{#each contained(commits.graph) as doc}
{#await fetch_header(doc.repo)}
<div class="flex"> <Clipboard tabindex="-1" class="mr-2 w-6 h-6 shrink-0 focus:outline-none"/><div class="flex font-mono mb-3"> <a use:link href="/{doc.nuri}">{doc.hash}</a> </div> </div>
{:then header}
<div class="flex" title="{header.about || ''}"> {#if header.class}<DataClassIcon {config} dataClass={header.class}/>{:else}<Clipboard tabindex="-1" class="mr-2 w-6 h-6 shrink-0 focus:outline-none"/>{/if}<div class="flex font-mono mb-3"> <a use:link href="/{doc.nuri}">{header.title || doc.hash}</a> </div></div>
{/await}
{/each}
{#if commits.graph.length == 0 || contained(commits.graph).length == 0}
<p>{$t("doc.empty_container")}</p>
{#if $cur_tab_doc_can_edit}
<button
on:click={create}
on:keypress={create}
class="select-none ml-0 mt-2 mb-10 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-500/50 rounded-lg text-base p-2 text-center inline-flex items-center dark:focus:ring-primary-700/55"
>
<PlusCircle tabindex="-1" class="mr-2 focus:outline-none" />
{$t("doc.create")}
</button>
{/if}
{/if}
<h2>Editing your profile</h2>
<input
class="mt-5"
id="name"
placeholder="Enter your name"
bind:value={name}
disabled={readonly}
/>
<br/>
<input
class="mt-5"
id="name"
placeholder="Enter your email address"
bind:value={email}
disabled={readonly}
/>
<br/>
<Button
on:click={save}
disabled={!valid || readonly}
class="select-none mt-5 mb-2 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-500/50 rounded-lg text-base p-2 text-center inline-flex items-center dark:focus:ring-primary-700/55"
>
<CheckCircle tabindex="-1" class="mr-2 focus:outline-none" />
Save
</Button>
<button
on:click={cancel}
class="mt-5 mb-2 text-gray-500 dark:text-gray-400 focus:ring-4 focus:ring-primary-100/50 ounded-lg text-base p-2 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><ArrowLeft
tabindex="-1"
class="mr-2 focus:outline-none"
/>Cancel</button
>
</div>

@ -15,7 +15,7 @@
import { link, push } from "svelte-spa-router";
import { onDestroy, onMount, tick } from "svelte";
import { Button, Progressbar, Spinner, Alert } from "flowbite-svelte";
import{ PlusCircle, ArrowLeft } from "svelte-heros-v2";
import{ PlusCircle, ArrowLeft, PencilSquare } from "svelte-heros-v2";
import { t } from "svelte-i18n";
import {
@ -24,7 +24,8 @@
import {
openModalCreate,
sparql_query,
active_session
active_session,
display_error,
} from "../store";
@ -34,6 +35,7 @@
let generation_state: "before_start" | "loading" | "generated" =
"before_start";
let generated_qr: string | undefined = undefined;
let error = undefined;
async function scrollToTop() {
await tick();
@ -51,21 +53,41 @@
async function generate_qr_code() {
generation_state = "loading";
console.log(container.clientWidth);
generated_qr = await ng.get_qrcode_for_profile(
$active_session.session_id,
$cur_tab.store.store_type == "public", // are we public or protected?
Math.min(container.clientWidth, 800)
);
generation_state = "generated";
try {
generated_qr = await ng.get_qrcode_for_profile(
$active_session.session_id,
$cur_tab.store.store_type == "public", // are we public or protected?
Math.min(container.clientWidth, 800)
);
generation_state = "generated";
} catch (e) {
error = e;
}
}
function back_to_profile_viewer() {
set_viewer("n:g:z:profile");
}
function edit() {
set_editor("n:g:z:profile_editor");
set_view_or_edit(false);
}
</script>
<div class="flex-col" bind:this={container}>
{#if error}
<Alert class="m-2" color="red" style="word-break: break-word;">{display_error(error)}</Alert>
<button
on:click={edit}
on:keypress={edit}
class="select-none mx-6 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-500/50 rounded-lg text-base p-2 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><PencilSquare
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>Edit profile</button
>
{/if}
{#if generation_state == "generated"}
<div class="mx-auto">
{@html generated_qr}

@ -658,6 +658,7 @@
"SocialQueryAlreadyStarted": "Social Query already started",
"ContactAlreadyExists": "Contact already added to your account",
"ContactNotFound": "You don't have any contact. We cannot start the Social Query",
"InvalidProfile": "Your profile is incomplete. You should add a name before you can share your profile with others",
"no_wasm_on_old_safari": "Your Safari browser is too old (version before 14.1). As a result we cannot load Automerge, needed for this document. Please upgrade your macOS or iOS system",
"BrowserTooOld": "Your browser is too old. Please upgrade it, use another browser, or install our native app. If you are using jshelter or another javascript protection mechanism, please deactivate it as we need access to the WebWorker facility of your browser.",
"NoLocalStorage": "You have disabled local storage in your browser. Please allow the current website (and https://nextgraph.net website) to store data in this browser as otherwise we cannot proceed with Wallet creation. After allowing storage, please refresh the current page. You might need to all third-party cookies too."

@ -3763,6 +3763,35 @@ impl InboxPost {
Err(NgError::InvalidNuri)
}
pub fn new_contact_details(
from_profile_store_repo: StoreRepo,
from_inbox: PrivKey,
to_overlay: OverlayId,
to_inbox: PubKey,
to_broker: Option<Locator>,
with_readcap: bool,
name: String,
email: Option<String>
) -> Result<Self, NgError> {
let from_overlay = from_profile_store_repo.outer_overlay();
let content = InboxMsgContent::ContactDetails(ContactDetails{
profile: from_profile_store_repo,
read_cap: if with_readcap {unimplemented!();} else {None},
name,
email
});
return Ok(InboxPost::new(
to_overlay,
to_inbox,
Some((from_overlay,from_inbox)),
&content,
vec![],
to_broker
)?);
}
}
/// Request to publish an event in pubsub
@ -4186,6 +4215,20 @@ pub struct SocialQueryResponse {
pub content: SocialQueryResponseContent,
}
/// ContactDetails sent in reply to scanning a QRcode of a profile
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContactDetails {
/// Profile Nuri
pub profile: StoreRepo,
/// optional readcap on the profile, if user wants to share the content of profile
pub read_cap: Option<ReadCap>,
pub name: String,
pub email: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SocialQuery {
Request(SocialQueryRequest),
@ -4196,7 +4239,7 @@ pub enum SocialQuery {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum InboxMsgContent {
ContactDetails,
ContactDetails(ContactDetails),
DialogRequest,
Link,
Patch,

@ -396,6 +396,7 @@ pub enum VerifierError {
InvalidProfile,
ContactAlreadyExists,
InternalError,
InvalidInboxPost,
}
impl Error for VerifierError {}

@ -24,7 +24,7 @@ use crate::verifier::*;
impl Verifier {
async fn post_to_inbox(&self, post: InboxPost) -> Result<(), VerifierError> {
pub(crate) async fn post_to_inbox(&self, post: InboxPost) -> Result<(), VerifierError> {
match self.client_request::<_,()>(post).await
{
Err(e) => Err(VerifierError::InboxError(e.to_string())),
@ -79,17 +79,16 @@ impl Verifier {
Ok(())
}
fn get_privkey_of_inbox(&self, this_overlay: &OverlayId) -> Result<PrivKey, VerifierError> {
pub(crate) fn get_privkey_of_inbox(&self, this_overlay: &OverlayId) -> Result<PrivKey, VerifierError> {
let store = self.get_store_by_overlay_id(this_overlay)?;
let repo = self.repos.get(&store.id()).ok_or(NgError::RepoNotFound)?;
let from_inbox = repo.inbox.to_owned().ok_or(NgError::InboxNotFound)?;
Ok(from_inbox)
}
pub(crate) fn get_profile_replying_to(&self, forwarded_from_profile: &String) -> Result<
(OverlayId, PrivKey) ,NgError> {
fn get_profile_replying_to(&self, from_profile: &String) -> Result<(OverlayId, PrivKey) ,NgError> {
let from_profile_id = if forwarded_from_profile.starts_with("did:ng:b") {
let from_profile_id = if from_profile.starts_with("did:ng:b") {
self.config.protected_store_id.unwrap()
} else {
self.config.public_store_id.unwrap()
@ -571,12 +570,62 @@ impl Verifier {
}
SocialQueryResponseContent::QueryResult(_) | SocialQueryResponseContent::False | SocialQueryResponseContent::True => {
// not implemented yet
unimplemented!();
return Err(VerifierError::NotImplemented)
}
}
}
_ => unimplemented!()
InboxMsgContent::ContactDetails(details) => {
if msg.body.from_inbox.is_none() {
// TODO log error
// we do nothing as this is invalid msg. it must have a from.
return Err(VerifierError::InvalidInboxPost);
}
let inbox_nuri_string: String = NuriV0::inbox(&msg.body.from_inbox.unwrap());
let profile_nuri_string: String = NuriV0::from_store_repo_string(&details.profile);
let a_or_b = if details.profile.is_public() { "site" } else { "protected" };
// checking if this contact has already been added
match self.sparql_query(
&NuriV0::new_entire_user_site(),
format!("ASK {{ ?s <did:ng:x:ng#{a_or_b}_inbox> <{inbox_nuri_string}> . ?s <did:ng:x:ng#{a_or_b}> <{profile_nuri_string}> }}"), None).await?
{
QueryResults::Boolean(true) => {
return Err(VerifierError::ContactAlreadyExists);
}
_ => {}
}
let contact = self.doc_create_with_store_repo(
"Graph".to_string(), "social:contact".to_string(),
"store".to_string(), None // meaning in private store
).await?;
let contact_nuri = NuriV0::new_from_repo_graph(&contact)?;
let contact_id = contact_nuri.target.repo_id().clone();
let contact_nuri_string = NuriV0::repo_id(&contact_id);
let has_email = details.email.map_or("".to_string(), |email| format!("<> vcard:hasEmail \"{email}\"."));
// adding triples in contact doc
let sparql_update = format!(" PREFIX ng: <did:ng:x:ng#>
PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
INSERT DATA {{ <> ng:{a_or_b} <{profile_nuri_string}>.
<> ng:{a_or_b}_inbox <{inbox_nuri_string}>.
<> a vcard:Individual .
<> vcard:fn \"{}\".
{has_email} }}", details.name);
let ret = self
.process_sparql_update(&contact_nuri, &sparql_update, &Some(contact_nuri_string), vec![])
.await;
if let Err(e) = ret {
return Err(VerifierError::SparqlError(e));
}
self.update_header(&contact_nuri.target, Some(details.name), None).await?;
}
_ => return Err(VerifierError::NotImplemented)
}
Ok(())
}

@ -153,7 +153,7 @@ impl Verifier {
}
}
async fn update_header(&mut self, target: &NuriTargetV0, title: Option<String>, about: Option<String>) -> Result<(), VerifierError> {
pub(crate) async fn update_header(&mut self, target: &NuriTargetV0, title: Option<String>, about: Option<String>) -> Result<(), VerifierError> {
let (repo_id, branch_id, store_repo) = self.resolve_header_branch(target)?;
let graph_name = NuriV0::branch_repo_graph_name(
@ -713,18 +713,31 @@ impl Verifier {
Ok(nuri_result)
}
fn get_profile_for_inbox_post(&self, public: bool) -> Result<(StoreRepo, PrivKey),NgError> {
let from_profile_id = if !public {
self.config.protected_store_id.unwrap()
} else {
self.config.public_store_id.unwrap()
};
let repo = self.repos.get(&from_profile_id).ok_or(NgError::RepoNotFound)?;
let inbox = repo.inbox.to_owned().ok_or(NgError::InboxNotFound)?;
let store_repo = repo.store.get_store_repo();
Ok( (store_repo.clone(), inbox.clone()) )
}
async fn import_contact_from_qrcode(&mut self, repo_id: RepoId, contact: NgQRCodeProfileSharingV0) -> Result<(), VerifierError> {
let inbox_nuri_string: String = NuriV0::inbox(&contact.inbox);
let profile_nuri_string: String = NuriV0::from_store_repo_string(&contact.profile);
let a_or_b = if contact.profile.is_public() { "a" } else { "b" };
let a_or_b = if contact.profile.is_public() { "site" } else { "protected" };
// checking if this contact has already been added
match self.sparql_query(
&NuriV0::new_entire_user_site(),
format!("ASK {{ ?s <did:ng:x:ng#d> <{inbox_nuri_string}> . ?s <did:ng:x:ng#{}> <{profile_nuri_string}> }}",
a_or_b ), None).await?
format!("ASK {{ ?s <did:ng:x:ng#{a_or_b}_inbox> <{inbox_nuri_string}> . ?s <did:ng:x:ng#{a_or_b}> <{profile_nuri_string}> }}"), None).await?
{
QueryResults::Boolean(true) => {
return Err(VerifierError::ContactAlreadyExists);
@ -732,6 +745,37 @@ impl Verifier {
_ => {}
}
// getting the privkey of the inbox and ovelray because we will need it here below to send responses.
let (from_profile, from_inbox) = self.get_profile_for_inbox_post(contact.profile.is_public())?;
// get the name and optional email address of the profile we will respond with.
// if we don't have a name, we fail
let from_profile_nuri = NuriV0::repo_id(from_profile.repo_id());
let (name,email) = match self.sparql_query(
&NuriV0::from_store_repo(&from_profile),
format!("PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
SELECT ?name ?email WHERE {{ <> vcard:fn ?name . <> vcard:hasEmail ?email }}"), Some(from_profile_nuri)).await?
{
QueryResults::Solutions(mut sol) => {
let mut name = None;
let mut email = None;
if let Some(Ok(s)) = sol.next() {
if let Some(Term::Literal(l)) = s.get("name") {
name = Some(l.value().to_string());
}
if let Some(Term::Literal(l)) = s.get("email") {
email = Some(l.value().to_string());
}
}
if name.is_none() {
return Err(VerifierError::InvalidProfile)
}
(name.unwrap(),email)
}
_ => return Err(VerifierError::InvalidResponse),
};
let contact_doc_nuri_string = NuriV0::repo_id(&repo_id);
let contact_doc_nuri = NuriV0::new_repo_target_from_id(&repo_id);
let has_email = contact.email.map_or("".to_string(), |email| format!("<> vcard:hasEmail \"{email}\"."));
@ -739,7 +783,7 @@ impl Verifier {
let sparql_update = format!(" PREFIX ng: <did:ng:x:ng#>
PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
INSERT DATA {{ <> ng:{a_or_b} <{profile_nuri_string}>.
<> ng:d <{inbox_nuri_string}>.
<> ng:{a_or_b}_inbox <{inbox_nuri_string}>.
<> a vcard:Individual .
<> vcard:fn \"{}\".
{has_email} }}", contact.name);
@ -752,9 +796,61 @@ impl Verifier {
self.update_header(&contact_doc_nuri.target, Some(contact.name), None).await?;
self.post_to_inbox(InboxPost::new_contact_details(
from_profile,
from_inbox,
contact.profile.outer_overlay(),
contact.inbox,
None,
false,
name,
email,
)?).await?;
Ok(())
}
pub(crate) async fn search_for_contacts(&self, excluding_profile_id_nuri: Option<String>) -> Result<Vec<(String,String)>, VerifierError> {
let extra_conditions = if let Some(s) = excluding_profile_id_nuri {
format!("&& NOT EXISTS {{ ?c ng:site <{s}> }} && NOT EXISTS {{ ?c ng:protected <{s}> }}")
} else {
String::new()
};
let sparql = format!("PREFIX ng: <did:ng:x:ng#>
SELECT ?profile_id ?inbox_id WHERE
{{ ?c a <http://www.w3.org/2006/vcard/ns#Individual> .
OPTIONAL {{ ?c ng:site ?profile_id . ?c ng:site_inbox ?inbox_id }}
OPTIONAL {{ ?c ng:protected ?profile_id . ?c ng:protected_inbox ?inbox_id }}
FILTER ( bound(?profile_id) {extra_conditions} )
}}");
log_info!("{sparql}");
let sols = match self.sparql_query(
&NuriV0::new_entire_user_site(),
sparql, None).await?
{
QueryResults::Solutions(sols) => { sols }
_ => return Err(VerifierError::SparqlError(NgError::InvalidResponse.to_string())),
};
let mut res = vec![];
for sol in sols {
match sol {
Err(e) => return Err(VerifierError::SparqlError(e.to_string())),
Ok(s) => {
if let Some(Term::NamedNode(profile_id)) = s.get("profile_id") {
let profile_nuri = profile_id.as_string();
if let Some(Term::NamedNode(inbox_id)) = s.get("inbox_id") {
let inbox_nuri = inbox_id.as_string();
res.push((profile_nuri.clone(), inbox_nuri.clone()));
}
}
}
}
}
Ok(res)
}
pub(crate) async fn process(
&mut self,
command: &AppRequestCommandV0,
@ -790,12 +886,13 @@ impl Verifier {
return Err(NgError::NotConnected);
}
// TODO: search for contacts (all stores, one store, a sparql query, etc..)
// searching for contacts (all stores, one store, a sparql query, etc..)
// (profile_nuri, inbox_nuri)
let contacts = if contacts_string.as_str() == "did:ng:d:c" {
let mut res = vec![];
res.push(("did:ng:a:rjoQTS4LMBDcuh8CEjmTYrgALeApBg2cgKqyPEuQDUgA".to_string(),"did:ng:d:KMFdOcGjdFBQgA9QNEDWcgEErQ1isbvDe7d_xndNOUMA".to_string()));
res
self.search_for_contacts(None).await?
// let mut res = vec![];
// res.push(("did:ng:a:rjoQTS4LMBDcuh8CEjmTYrgALeApBg2cgKqyPEuQDUgA".to_string(),"did:ng:d:KMFdOcGjdFBQgA9QNEDWcgEErQ1isbvDe7d_xndNOUMA".to_string()));
// res
} else {
return Ok(AppResponse::error(NgError::NotImplemented.to_string()));
};

@ -1585,7 +1585,7 @@ impl Verifier {
<> vcard:hasEmail ?email .
}}");
//log_info!("{sparql}");
let (mut name, mut email) = match self.sparql_query(
let (name, email) = match self.sparql_query(
&NuriV0::new_repo_target_from_id(profile_id),
sparql, Some(NuriV0::repo_id(profile_id))).await?
{
@ -1616,9 +1616,7 @@ impl Verifier {
_ => return Err(VerifierError::SparqlError(NgError::InvalidResponse.to_string())),
};
if name.is_none() {
//return Err(VerifierError::InvalidProfile);
name = Some("no name".to_string());
email = Some("fake@email.com".to_string());
return Err(VerifierError::InvalidProfile);
}
let profile_sharing = NgQRCode::ProfileSharingV0(NgQRCodeProfileSharingV0 {
inbox,

Loading…
Cancel
Save