+
Contact
{#if !has_camera && !has_name}
-
No camera available. You cannot import with QR-code
+
No camera available. You cannot import with QR-code
{/if}
- {#if !has_name}
+ {#if !has_name && has_camera}
{/if}
- Name: {has_name || ""}
+ {#if has_name}
+ Name: {has_name}
+ {/if}
{#if has_email}
Email: {has_email}
{/if}
diff --git a/ng-app/src/apps/ProfileEditor.svelte b/ng-app/src/apps/ProfileEditor.svelte
index 7c62d603..8c1a524d 100644
--- a/ng-app/src/apps/ProfileEditor.svelte
+++ b/ng-app/src/apps/ProfileEditor.svelte
@@ -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:
"
+ +"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"
- }
-
- {#each contained(commits.graph) as doc}
- {#await fetch_header(doc.repo)}
-
- {:then header}
-
{#if header.class}
{:else}{/if}
- {/await}
- {/each}
- {#if commits.graph.length == 0 || contained(commits.graph).length == 0}
-
{$t("doc.empty_container")}
- {#if $cur_tab_doc_can_edit}
-
- {/if}
- {/if}
+
Editing your profile
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ng-app/src/apps/ProfileQrCode.svelte b/ng-app/src/apps/ProfileQrCode.svelte
index 91cefa3b..cde28132 100644
--- a/ng-app/src/apps/ProfileQrCode.svelte
+++ b/ng-app/src/apps/ProfileQrCode.svelte
@@ -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);
+ }
+ {#if error}
+
{display_error(error)}
+
+ {/if}
{#if generation_state == "generated"}
{@html generated_qr}
diff --git a/ng-app/src/locales/en.json b/ng-app/src/locales/en.json
index a54cbf91..469d011c 100644
--- a/ng-app/src/locales/en.json
+++ b/ng-app/src/locales/en.json
@@ -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."
diff --git a/ng-net/src/types.rs b/ng-net/src/types.rs
index 76aeb88e..bc12e729 100644
--- a/ng-net/src/types.rs
+++ b/ng-net/src/types.rs
@@ -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
,
+ with_readcap: bool,
+ name: String,
+ email: Option
+ ) -> Result {
+
+ 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,
+
+ pub name: String,
+
+ pub email: Option,
+}
+
#[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,
diff --git a/ng-repo/src/errors.rs b/ng-repo/src/errors.rs
index bdd9be66..a1b286f2 100644
--- a/ng-repo/src/errors.rs
+++ b/ng-repo/src/errors.rs
@@ -396,6 +396,7 @@ pub enum VerifierError {
InvalidProfile,
ContactAlreadyExists,
InternalError,
+ InvalidInboxPost,
}
impl Error for VerifierError {}
diff --git a/ng-verifier/src/inbox_processor.rs b/ng-verifier/src/inbox_processor.rs
index 675bfb93..c8d4dc02 100644
--- a/ng-verifier/src/inbox_processor.rs
+++ b/ng-verifier/src/inbox_processor.rs
@@ -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 {
+ pub(crate) fn get_privkey_of_inbox(&self, this_overlay: &OverlayId) -> Result {
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 <{inbox_nuri_string}> . ?s <{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:
+ PREFIX vcard:
+ 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(())
}
diff --git a/ng-verifier/src/request_processor.rs b/ng-verifier/src/request_processor.rs
index b06ce5b2..0847c05f 100644
--- a/ng-verifier/src/request_processor.rs
+++ b/ng-verifier/src/request_processor.rs
@@ -153,7 +153,7 @@ impl Verifier {
}
}
- async fn update_header(&mut self, target: &NuriTargetV0, title: Option, about: Option) -> Result<(), VerifierError> {
+ pub(crate) async fn update_header(&mut self, target: &NuriTargetV0, title: Option, about: Option) -> 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 <{inbox_nuri_string}> . ?s <{profile_nuri_string}> }}",
- a_or_b ), None).await?
+ format!("ASK {{ ?s <{inbox_nuri_string}> . ?s <{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:
+ 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:
PREFIX vcard:
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) -> Result, 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:
+ SELECT ?profile_id ?inbox_id WHERE
+ {{ ?c a .
+ 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()));
};
diff --git a/ng-verifier/src/verifier.rs b/ng-verifier/src/verifier.rs
index 67e9d14d..d93b71f1 100644
--- a/ng-verifier/src/verifier.rs
+++ b/ng-verifier/src/verifier.rs
@@ -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,