add contact with QRCode

Niko PLP 2 days ago
parent ef93afe4cb
commit 6fe3624ba0
  1. 3
      Cargo.lock
  2. 2
      helpers/app-auth/src/App.svelte
  3. 2
      helpers/net-auth/src/App.svelte
  4. 3
      nextgraph/src/local_broker.rs
  5. 5
      ng-app/index-web.html
  6. 2
      ng-app/src/App.svelte
  7. 116
      ng-app/src/apps/ContactEditor.svelte
  8. 88
      ng-app/src/apps/ProfileEditor.svelte
  9. 84
      ng-app/src/apps/ProfileQrCode.svelte
  10. 88
      ng-app/src/apps/ProfileView.svelte
  11. 42
      ng-app/src/apps/SocialQueryDemo.svelte
  12. 17
      ng-app/src/classes.ts
  13. 2
      ng-app/src/lib/Login.svelte
  14. 2
      ng-app/src/lib/icons/DataClassIcon.svelte
  15. 7
      ng-app/src/lib/icons/ZeraIcon.svelte
  16. 5
      ng-app/src/locales/en.json
  17. 2
      ng-app/src/routes/WalletInfo.svelte
  18. 4
      ng-app/src/routes/WalletLoginQr.svelte
  19. 4
      ng-app/src/store.ts
  20. 7
      ng-app/src/tab.ts
  21. 40
      ng-app/src/zeras.ts
  22. 1
      ng-net/Cargo.toml
  23. 22
      ng-net/src/app_protocol.rs
  24. 45
      ng-net/src/types.rs
  25. 5
      ng-oxigraph/src/oxigraph/storage/mod.rs
  26. 6
      ng-repo/src/errors.rs
  27. 8
      ng-repo/src/store.rs
  28. 7
      ng-repo/src/types.rs
  29. 64
      ng-sdk-js/src/lib.rs
  30. 2
      ng-verifier/Cargo.toml
  31. 1
      ng-verifier/src/commits/transaction.rs
  32. 8
      ng-verifier/src/inbox_processor.rs
  33. 244
      ng-verifier/src/request_processor.rs
  34. 82
      ng-verifier/src/verifier.rs
  35. 33
      ng-wallet/src/types.rs

3
Cargo.lock generated

@ -3376,6 +3376,7 @@ dependencies = [
"unique_id", "unique_id",
"url", "url",
"web-time", "web-time",
"zeroize",
] ]
[[package]] [[package]]
@ -3532,6 +3533,7 @@ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
"automerge", "automerge",
"base64-url",
"either", "either",
"futures", "futures",
"getrandom 0.2.10", "getrandom 0.2.10",
@ -3540,6 +3542,7 @@ dependencies = [
"ng-oxigraph", "ng-oxigraph",
"ng-repo", "ng-repo",
"ng-storage-rocksdb", "ng-storage-rocksdb",
"qrcode",
"rand 0.7.3", "rand 0.7.3",
"sbbf-rs-safe", "sbbf-rs-safe",
"serde", "serde",

@ -56,7 +56,7 @@
routes.set("/user", User); routes.set("/user", User);
routes.set("/wallet", WalletInfo); routes.set("/wallet", WalletInfo);
routes.set("/user/accounts", AccountInfo); routes.set("/user/accounts", AccountInfo);
routes.set("/wallet/scanqr", ScanQRWeb); routes.set("/scanqr", ScanQRWeb);
routes.set("*", NotFound); routes.set("*", NotFound);
let unsubscribe = () => {}; let unsubscribe = () => {};

@ -48,7 +48,7 @@
// routes.set("/user/registered", UserRegistered); // routes.set("/user/registered", UserRegistered);
// routes.set("/wallet", WalletInfo); // routes.set("/wallet", WalletInfo);
// routes.set("/user/accounts", AccountInfo); // routes.set("/user/accounts", AccountInfo);
// routes.set("/wallet/scanqr", ScanQRWeb); // routes.set("/scanqr", ScanQRWeb);
// routes.set("/install", Install); // routes.set("/install", Install);
routes.set("*", NotFound); routes.set("*", NotFound);

@ -1683,8 +1683,9 @@ pub fn wallet_to_wallet_recovery(
Wallet::V0(v0) => { Wallet::V0(v0) => {
let mut content = v0.content.clone(); let mut content = v0.content.clone();
content.security_img = vec![]; content.security_img = vec![];
content.security_txt = String::new();
NgQRCodeWalletRecoveryV0 { NgQRCodeWalletRecoveryV0 {
wallet: content, wallet: serde_bare::to_vec(&content).unwrap(),
pazzle, pazzle,
mnemonic, mnemonic,
pin, pin,

@ -101,8 +101,9 @@
<div class="noshow" id="app-loading">&nbsp;&nbsp;&nbsp;Loading ...</div> <div class="noshow" id="app-loading">&nbsp;&nbsp;&nbsp;Loading ...</div>
<div id="error-no-wasm" class="error-no-wasm-hidden"> <div id="error-no-wasm" class="error-no-wasm-hidden">
Your browser is too old and does not support NextGraph. <br/>Please upgrade to a newer version of this browser,<br/> try with another browser,<br/> <br/>or <a href="https://nextgraph.org/download">install our native apps on <br/> Your browser is too old or is miss-configured. <br/>Please upgrade to a newer version of this browser,<br/> try with another browser,<br/> <br/>or <a href="https://nextgraph.org/download">install our native apps on <br/>
Linux, macOS, Windows desktops and laptops,<br/> and iOS, Android mobiles.</a><br/><br/>If you are using jshelter or another javascript protection mechanism, please deactivate it as we need access to the WebWorker facility of your browser. Linux, macOS, Windows desktops and laptops,<br/> and iOS, Android mobiles.</a><br/><br/>If you are using jshelter or another javascript protection mechanism,
please deactivate it as we need access to the WebWorker, JIT and WASM features of your browser. If those features are disabled, please enable them for this website.
</div> </div>
<noscript style="display:grid;"> <noscript style="display:grid;">
NextGraph cannot load as Javascript is deactivated.<br/> NextGraph cannot load as Javascript is deactivated.<br/>

@ -59,7 +59,7 @@
routes.set("/user/registered", UserRegistered); routes.set("/user/registered", UserRegistered);
routes.set("/wallet", WalletInfo); routes.set("/wallet", WalletInfo);
routes.set("/user/accounts", AccountInfo); routes.set("/user/accounts", AccountInfo);
routes.set("/wallet/scanqr", ScanQR); routes.set("/scanqr", ScanQR);
if (import.meta.env.NG_APP_WEB) routes.set("/install", Install); if (import.meta.env.NG_APP_WEB) routes.set("/install", Install);
routes.set("/shared", Shared); routes.set("/shared", Shared);
routes.set("/site", Site); routes.set("/site", Site);

@ -0,0 +1,116 @@
<!--
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<script lang="ts">
import ng from "../api";
import { link } from "svelte-spa-router";
import { Button, Progressbar, Spinner, Alert } from "flowbite-svelte";
import{ PlusCircle } from "svelte-heros-v2";
import { push } from "svelte-spa-router";
import{ QrCode } from "svelte-heros-v2";
import { t } from "svelte-i18n";
import {
in_memory_discrete, open_viewer, set_viewer, set_editor, set_view_or_edit, cur_tab_branch_class, cur_tab_doc_can_edit, cur_tab
} from "../tab";
import DataClassIcon from "../lib/icons/DataClassIcon.svelte";
import {
openModalCreate,
sparql_query,
active_session,
scanned_qr_code,
check_has_camera,
toast_error,
display_error,
} from "../store";
import { onDestroy, onMount, tick } from "svelte";
export let commits;
const open_scanner = () => {
push("#/scanqr");
};
let container: HTMLElement;
let has_camera = false;
let has_name = undefined;
let has_email = undefined;
async function scrollToTop() {
await tick();
if (container) container.scrollIntoView();
}
onMount(async () => {
if (!$active_session) {
push("#/");
return;
}
has_camera = await check_has_camera();
console.log("has_camera",has_camera)
if ($scanned_qr_code) {
on_qr_scanned($scanned_qr_code);
scanned_qr_code.set("");
}
await scrollToTop();
});
$: if (commits) { contained(commits.graph) }
async function on_qr_scanned(text: string) {
try {
console.log("got QR",text, "did:ng:"+$cur_tab.doc.nuri);
await ng.import_contact_from_qrcode($active_session.session_id, "did:ng:"+$cur_tab.doc.nuri, text);
} catch (e) {
console.error(e)
toast_error(display_error(e));
}
}
function contained(graph) {
let ret = [];
for (const g of graph) {
if (g.substring(57,91) === "http://www.w3.org/2006/vcard/ns#fn") {
has_name = g.substring(94, g.length-1);
} else if (g.substring(57,97) === "http://www.w3.org/2006/vcard/ns#hasEmail") {
has_email = g.substring(100, g.length-1);
}
}
ret.sort((a, b) => a.hash.localeCompare(b.hash));
return ret;
}
async function test() {
if (!has_camera) {
await on_qr_scanned("AgBPtkD9jg11uDj7FTK0VqWb_aVxYvoyjFyIWs5VwCOICwAAABsxv_FXViA-5LUMNjARLJCiS3nOc7WYdoVQYgWn2ukcB25vIG5hbWUBDmZha2VAZW1haWwuY29t");
}
}
</script>
<div class="flex-col p-5" bind:this={container}>
{#if !has_camera && !has_name}
<Alert class="m-2" color="red">No camera available. You cannot import with QR-code</Alert>
{/if}
{#if !has_name}
<Button
on:click={test}
on:keypress={open_scanner}
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_email}
Email: {has_email}<br/>
{/if}
</div>

@ -0,0 +1,88 @@
<!--
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<script lang="ts">
import ng from "../api";
import { link } from "svelte-spa-router";
import { Button, Progressbar, Spinner, Alert } from "flowbite-svelte";
import{ PlusCircle } from "svelte-heros-v2";
import { t } from "svelte-i18n";
import {
in_memory_discrete, open_viewer, set_viewer, set_editor, set_view_or_edit, cur_tab_branch_class, cur_tab_doc_can_edit, cur_tab
} from "../tab";
import DataClassIcon from "../lib/icons/DataClassIcon.svelte";
import {
openModalCreate,
sparql_query,
active_session
} from "../store";
import {
Clipboard
} from "svelte-heros-v2";
export let commits;
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});
}
}
ret.sort((a, b) => a.hash.localeCompare(b.hash));
return ret;
}
async function fetch_header(repo) {
try {
let res = await ng.fetch_header($active_session.session_id, repo);
return res;
}catch(e){
console.error(e);
return {};
}
}
const create = () => {
openModalCreate();
}
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}
</div>

@ -0,0 +1,84 @@
<!--
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<script lang="ts">
import ng from "../api";
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 { t } from "svelte-i18n";
import {
in_memory_discrete, open_viewer, set_viewer, set_editor, set_view_or_edit, cur_tab_branch_class, cur_tab_doc_can_edit, cur_tab
} from "../tab";
import {
openModalCreate,
sparql_query,
active_session
} from "../store";
export let commits;
let container: HTMLElement;
let generation_state: "before_start" | "loading" | "generated" =
"before_start";
let generated_qr: string | undefined = undefined;
async function scrollToTop() {
await tick();
container.scrollIntoView();
}
onMount(async () => {
if (!$active_session) {
push("#/");
return;
}
await scrollToTop();
await generate_qr_code();
});
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";
}
function back_to_profile_viewer() {
set_viewer("n:g:z:profile");
}
</script>
<div class="flex-col" bind:this={container}>
{#if generation_state == "generated"}
<div class="mx-auto">
{@html generated_qr}
</div>
<button
on:click={back_to_profile_viewer}
on:keypress={back_to_profile_viewer}
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"
><ArrowLeft
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"
/>{$t("buttons.back")}</button
>
{/if}
</div>

@ -0,0 +1,88 @@
<!--
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<script lang="ts">
import ng from "../api";
import { link } from "svelte-spa-router";
import { Button, Progressbar, Spinner, Alert } from "flowbite-svelte";
import{ PlusCircle } from "svelte-heros-v2";
import { t } from "svelte-i18n";
import {
in_memory_discrete, open_viewer, set_viewer, set_editor, set_view_or_edit, cur_tab_branch_class, cur_tab_doc_can_edit, cur_tab
} from "../tab";
import DataClassIcon from "../lib/icons/DataClassIcon.svelte";
import {
openModalCreate,
sparql_query,
active_session
} from "../store";
import {
Clipboard
} from "svelte-heros-v2";
export let commits;
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});
}
}
ret.sort((a, b) => a.hash.localeCompare(b.hash));
return ret;
}
async function fetch_header(repo) {
try {
let res = await ng.fetch_header($active_session.session_id, repo);
return res;
}catch(e){
console.error(e);
return {};
}
}
const create = () => {
openModalCreate();
}
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}
</div>

@ -15,7 +15,9 @@
sparql_update, sparql_update,
toast_error, toast_error,
toast_success, toast_success,
active_session active_session,
display_error,
online
} from "../store"; } from "../store";
import ng from "../api"; import ng from "../api";
import { import {
@ -42,19 +44,25 @@
const openQuery = async () => { const openQuery = async () => {
await sparql_update("INSERT DATA { <> <did:ng:x:ng#social_query_sparql> \"CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }\".}"); //TODO : return now if already processing (when LDO for svelte is ready)
let commit_id = commits.heads[0]; // and even disable the button in that case
let commit_key = commits.head_keys[0]; try {
let session = $active_session; await sparql_update("INSERT DATA { <> <did:ng:x:ng#social_query_sparql> \"CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }\".}");
if (!session) return; let commit_id = commits.heads[0];
let request_nuri = "did:ng:"+$cur_tab.doc.nuri+":c:"+commit_id+":k:"+commit_key; let commit_key = commits.head_keys[0];
await ng.social_query_start( let session = $active_session;
session.session_id, if (!session) return;
"did:ng:a", let request_nuri = "did:ng:"+$cur_tab.doc.nuri+":c:"+commit_id+":k:"+commit_key;
request_nuri, await ng.social_query_start(
"did:ng:d:c", session.session_id,
2, "did:ng:a",
); request_nuri,
"did:ng:d:c",
2,
);
} catch (e) {
toast_error(display_error(e));
}
} }
onMount(()=>{ onMount(()=>{
@ -62,7 +70,6 @@
}); });
const info = () => { const info = () => {
} }
</script> </script>
@ -78,14 +85,15 @@
> >
info info
</button> </button>
<button <Button
on:click={openQuery} on:click={openQuery}
on:keypress={openQuery} on:keypress={openQuery}
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" 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"
> >
<Lifebuoy tabindex="-1" class="mr-2 focus:outline-none" /> <Lifebuoy tabindex="-1" class="mr-2 focus:outline-none" />
Start query Start query
</button> </Button>
{#if source} {#if source}
<Highlight {language} code={source} class="mb-10" let:highlighted > <Highlight {language} code={source} class="mb-10" let:highlighted >

@ -728,9 +728,12 @@ export const official_classes = {
"ng:crdt": "Graph", "ng:crdt": "Graph",
"ng:n": "Contact", "ng:n": "Contact",
"ng:a": "Contact: an Individual, Organization or Group", "ng:a": "Contact: an Individual, Organization or Group",
"ng:o": "n:g:z:contact_editor",
"implemented": true,
"ng:x": { "ng:x": {
"vcard":true, "vcard":true,
"foaf": true, "foaf": true,
"schema": true,
}, },
"ng:include": ["data:graph"], "ng:include": ["data:graph"],
"ng:compat": ["foaf:Person","foaf:Agent","vcard:Individual", "vcard:Organization", "vcard:Group", "file:iana:text:vcard", "file:iana:application:vcard+json", "file:iana:application:vcard+xml" ], "ng:compat": ["foaf:Person","foaf:Agent","vcard:Individual", "vcard:Organization", "vcard:Group", "file:iana:text:vcard", "file:iana:application:vcard+json", "file:iana:application:vcard+xml" ],
@ -784,6 +787,20 @@ export const official_classes = {
"ng:n": "Live", "ng:n": "Live",
"ng:a": "A live session of video or audio, with optional chat", "ng:a": "A live session of video or audio, with optional chat",
}, },
"social:profile": {
"ng:crdt": "Graph",
"ng:n": "Social Profile",
"ng:a": "The profile your share to others",
"ng:o": "n:g:z:profile",
"ng:w": "n:g:z:profile_editor",
"ng:x": {
"rdf": true,
"rdfs": true,
"ldp": true,
},
"ng:include": "data:container",
"implemented": true
},
"social:profile:skills:programming": { "social:profile:skills:programming": {
"ng:crdt": "Graph", "ng:crdt": "Graph",
"ng:n": "Skills Profile", "ng:n": "Skills Profile",

@ -430,7 +430,7 @@
<Alert color="orange" class=""> <Alert color="orange" class="">
Access to local storage is denied. <br/>You won't be able to save your wallet in this browser.<br/> Access to local storage is denied. <br/>You won't be able to save your wallet in this browser.<br/>
If you wanted to save it, please allow storing local data<br/> for the websites {location.origin} <br/> If you wanted to save it, please allow storing local data<br/> for the websites {location.origin} <br/>
and https://nextgraph.net and then reload the page. and https://nextgraph.net and then reload the page. <br/> You might need to all third-party cookies too.
</Alert> </Alert>
</div> </div>
{:else} {:else}

@ -44,6 +44,7 @@
CursorArrowRays, CursorArrowRays,
Megaphone, Megaphone,
User, User,
UserCircle,
Clock, Clock,
CalendarDays, CalendarDays,
Calendar, Calendar,
@ -132,6 +133,7 @@
"social:channel": Megaphone, "social:channel": Megaphone,
"social:stream": Bolt, "social:stream": Bolt,
"social:contact": User, "social:contact": User,
"social:profile": UserCircle,
"social:event": Clock, "social:event": Clock,
"social:calendar": CalendarDays, "social:calendar": CalendarDays,
"social:scheduler": Calendar, "social:scheduler": Calendar,

@ -43,7 +43,7 @@
Ticket, Ticket,
CursorArrowRays, CursorArrowRays,
Megaphone, Megaphone,
User, UserCircle,
Clock, Clock,
CalendarDays, CalendarDays,
Calendar, Calendar,
@ -73,6 +73,8 @@
Square3Stack3d, Square3Stack3d,
QueueList, QueueList,
Lifebuoy, Lifebuoy,
QrCode,
User,
} from "svelte-heros-v2"; } from "svelte-heros-v2";
import JsonIcon from "./JsonIcon.svelte"; import JsonIcon from "./JsonIcon.svelte";
import JsonLdIcon from "./JsonLdIcon.svelte"; import JsonLdIcon from "./JsonLdIcon.svelte";
@ -115,6 +117,9 @@
grid: Squares2x2, grid: Squares2x2,
view: Eye, view: Eye,
social_query: Lifebuoy, social_query: Lifebuoy,
profile: UserCircle,
qrcode: QrCode,
contact: User,
}; };
const prefix_mapping = {}; const prefix_mapping = {};

@ -655,9 +655,12 @@
"WsError": "WebSocket error", "WsError": "WebSocket error",
"cannot_load_this_file": "Cannot load this file", "cannot_load_this_file": "Cannot load this file",
"graph_not_found": "Graph not found", "graph_not_found": "Graph not found",
"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",
"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", "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.", "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." "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."
}, },
"connectivity": { "connectivity": {
"stopped": "Stopped", "stopped": "Stopped",

@ -117,7 +117,7 @@
} }
async function open_scanner() { async function open_scanner() {
push("#/wallet/scanqr"); push("#/scanqr");
} }
async function generate_text_code() { async function generate_text_code() {

@ -24,7 +24,7 @@
import { wallet_from_import, scanned_qr_code, display_error, check_has_camera } from "../store"; import { wallet_from_import, scanned_qr_code, display_error, check_has_camera } from "../store";
import ng from "../api"; import ng from "../api";
// <a href="/wallet/scanqr" use:link> // <a href="/scanqr" use:link>
let top: HTMLElement; let top: HTMLElement;
@ -44,7 +44,7 @@
let rendezvous_code; let rendezvous_code;
const open_scanner = () => { const open_scanner = () => {
push("#/wallet/scanqr"); push("#/scanqr");
}; };

@ -220,9 +220,11 @@ export const check_has_camera = async () => {
else { else {
try { try {
const devices = await navigator.mediaDevices.enumerateDevices(); const devices = await navigator.mediaDevices.enumerateDevices();
console.log(devices);
has_camera = has_camera =
devices.filter((device) => device.kind === "videoinput").length > 0; devices.filter((device) => device.kind === "videoinput").length > 0;
} catch { } catch(e) {
console.log(e);
has_camera = false; has_camera = false;
} }
} }

@ -129,6 +129,13 @@ const class_to_viewers_editors = (class_name: string) => {
if (!has_discrete) { if (!has_discrete) {
if (class_def["ng:o"]) graph_viewers.push(class_def["ng:o"]); if (class_def["ng:o"]) graph_viewers.push(class_def["ng:o"]);
if (class_def["ng:w"]) graph_editors.push(class_def["ng:w"]); if (class_def["ng:w"]) graph_editors.push(class_def["ng:w"]);
for (const additional_v of find_viewers_for_class(class_name)){
if (!graph_viewers.includes(additional_v)) graph_viewers.push(additional_v);
}
for (const additional_e of find_editors_for_class(class_name)){
if (!graph_editors.includes(additional_e)) graph_editors.push(additional_e);
}
} }
for (const additional_g_v of find_viewers_for_class("data:graph")){ for (const additional_g_v of find_viewers_for_class("data:graph")){
if (!graph_viewers.includes(additional_g_v)) graph_viewers.push(additional_g_v); if (!graph_viewers.includes(additional_g_v)) graph_viewers.push(additional_g_v);

@ -519,6 +519,46 @@ export const official_apps = {
"ng:o": ["data:collection","data:container"], "ng:o": ["data:collection","data:container"],
"ng:w": ["data:collection","data:container"], "ng:w": ["data:collection","data:container"],
}, },
"n:g:z:profile": {
"ng:n": "Profile",
"ng:a": "See the profile of a user of group",
"ng:c": "app",
"ng:u": "profile",//favicon. can be a did:ng:j
"ng:g": "n:g:z:profile",
"ng:b": "ProfileView",
implemented: true,
"ng:o": ["social:profile"],
},
"n:g:z:profile_qrcode": {
"ng:n": "QR Code",
"ng:a": "Share your profile with others via QR Code",
"ng:c": "app",
"ng:u": "qrcode",//favicon. can be a did:ng:j
"ng:g": "n:g:z:profile_qrcode",
"ng:b": "ProfileQrCode",
implemented: true,
"ng:o": ["social:profile"],
},
"n:g:z:profile_editor": {
"ng:n": "Profile Editor",
"ng:a": "Edit your profile",
"ng:c": "app",
"ng:u": "profile",//favicon. can be a did:ng:j
"ng:g": "n:g:z:profile_editor",
"ng:b": "ProfileEditor",
implemented: true,
"ng:w": ["social:profile"],
},
"n:g:z:contact_editor": {
"ng:n": "Contact",
"ng:a": "Add, View, and Edit a Contact",
"ng:c": "app",
"ng:u": "contact",//favicon. can be a did:ng:j
"ng:g": "n:g:z:contact_editor",
"ng:b": "ContactEditor",
implemented: true,
"ng:o": ["social:contact"],
},
"n:g:z:grid": { "n:g:z:grid": {
"ng:n": "Grid", "ng:n": "Grid",
"ng:a": "See the content of document as a grid", "ng:a": "See the content of document as a grid",

@ -38,6 +38,7 @@ regex = "1.8.4"
base64-url = "2.0.0" base64-url = "2.0.0"
web-time = "0.2.0" web-time = "0.2.0"
time = "0.3.41" time = "0.3.41"
zeroize = { version = "1.7.0", features = ["zeroize_derive"] }
ng-repo = { path = "../ng-repo", version = "0.1.1-alpha.2" } ng-repo = { path = "../ng-repo", version = "0.1.1-alpha.2" }
reqwest = { version = "0.11.18", features = ["json","native-tls-vendored"] } reqwest = { version = "0.11.18", features = ["json","native-tls-vendored"] }

@ -555,6 +555,15 @@ impl NuriV0 {
Err(NgError::InvalidNuri) Err(NgError::InvalidNuri)
} }
pub fn new_from_repo_nuri(from: &String) -> Result<Self, NgError> {
let repo_id = Self::from_repo_nuri_to_id(from)?;
let mut n = Self::new_empty();
n.target = NuriTargetV0::Repo(repo_id);
return Ok(n);
}
pub fn new_from_commit(from: &String) -> Result<Self, NgError> { pub fn new_from_commit(from: &String) -> Result<Self, NgError> {
let c = RE_COMMIT.captures(&from); let c = RE_COMMIT.captures(&from);
@ -695,6 +704,8 @@ pub enum AppRequestCommandV0 {
InboxPost, InboxPost,
SocialQueryStart, SocialQueryStart,
SocialQueryCancel, SocialQueryCancel,
QrCodeProfile,
QrCodeProfileImport,
} }
impl AppRequestCommandV0 { impl AppRequestCommandV0 {
@ -734,6 +745,12 @@ impl AppRequestCommandV0 {
pub fn new_header() -> Self { pub fn new_header() -> Self {
AppRequestCommandV0::Header AppRequestCommandV0::Header
} }
pub fn new_qrcode_for_profile() -> Self {
AppRequestCommandV0::QrCodeProfile
}
pub fn new_qrcode_profile_import() -> Self {
AppRequestCommandV0::QrCodeProfileImport
}
pub fn new_fetch_header() -> Self { pub fn new_fetch_header() -> Self {
AppRequestCommandV0::Fetch(AppFetchContentV0::Header) AppRequestCommandV0::Fetch(AppFetchContentV0::Header)
} }
@ -1015,6 +1032,8 @@ pub enum AppRequestPayloadV0 {
}, },
//RemoveFile //RemoveFile
//Invoke(InvokeArguments), //Invoke(InvokeArguments),
QrCodeProfile(u32),
QrCodeProfileImport(String),
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -1281,4 +1300,7 @@ impl AppResponse {
pub fn commits(commits: Vec<String>) -> Self { pub fn commits(commits: Vec<String>) -> Self {
AppResponse::V0(AppResponseV0::Commits(commits)) AppResponse::V0(AppResponseV0::Commits(commits))
} }
pub fn text(text: String) -> Self {
AppResponse::V0(AppResponseV0::Text(text))
}
} }

@ -21,6 +21,7 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use web_time::SystemTime; use web_time::SystemTime;
use zeroize::{Zeroize, ZeroizeOnDrop};
use ng_repo::errors::*; use ng_repo::errors::*;
use ng_repo::log::*; use ng_repo::log::*;
@ -5175,6 +5176,50 @@ pub enum NgLink {
V0(NgLinkV0), V0(NgLinkV0),
} }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NgQRCodeWalletTransferV0 {
pub broker: BrokerServerV0,
pub rendezvous: SymKey, // Rendez-vous ID
pub secret_key: SymKey,
pub is_rendezvous: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NgQRCodeProfileSharingV0 {
pub inbox: PubKey,
pub profile: StoreRepo,
pub name: String,
pub email: Option<String>,
}
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct NgQRCodeWalletRecoveryV0 {
#[zeroize(skip)]
#[serde(with = "serde_bytes")]
pub wallet: Vec<u8>, // a serialized WalletContentV0, //of which security_img and security_text are emptied
pub pazzle: Vec<u8>,
pub mnemonic: [u16; 12],
pub pin: [u8; 4],
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum NgQRCode {
WalletTransferV0(NgQRCodeWalletTransferV0),
WalletRecoveryV0(NgQRCodeWalletRecoveryV0),
ProfileSharingV0(NgQRCodeProfileSharingV0),
}
impl NgQRCode {
pub fn from_code(code: String) -> Result<Self, NgError> {
let decoded = base64_url::decode(&code).map_err(|_| NgError::SerializationError)?;
Ok(serde_bare::from_slice(&decoded)?)
}
pub fn to_code(&self) -> String {
let ser = serde_bare::to_vec(self).unwrap();
base64_url::encode(&ser)
}
}
// TODO: PermaLinks and InboxPost (and ExtRequests) // TODO: PermaLinks and InboxPost (and ExtRequests)
#[cfg(test)] #[cfg(test)]

@ -628,7 +628,6 @@ impl NgCommitQuadIterator {
self.current_is_added = false; self.current_is_added = false;
self.current_add_is_removed = None; self.current_add_is_removed = None;
self.current = None; self.current = None;
ret ret
} }
@ -689,6 +688,7 @@ impl NgCommitQuadIterator {
return Err(false); return Err(false);
} }
} else { } else {
self.iter.next();
return Err(false); return Err(false);
} }
} }
@ -707,6 +707,9 @@ impl Iterator for NgCommitQuadIterator {
if let Ok(found) = res { if let Ok(found) = res {
return found; return found;
} }
// if res.err().unwrap() {
// return None;
// }
} }
} }
} }

@ -95,6 +95,7 @@ pub enum NgError {
BrokerNotFound, BrokerNotFound,
SparqlError(String), SparqlError(String),
ContactNotFound, ContactNotFound,
SocialQueryAlreadyStarted,
} }
impl Error for NgError {} impl Error for NgError {}
@ -191,6 +192,7 @@ impl From<VerifierError> for NgError {
VerifierError::StoreNotFound => NgError::StoreNotFound, VerifierError::StoreNotFound => NgError::StoreNotFound,
VerifierError::BranchNotFound => NgError::BranchNotFound, VerifierError::BranchNotFound => NgError::BranchNotFound,
VerifierError::SparqlError(s) => NgError::SparqlError(s), VerifierError::SparqlError(s) => NgError::SparqlError(s),
VerifierError::InternalError => NgError::InternalError,
_ => NgError::VerifierError(e), _ => NgError::VerifierError(e),
} }
} }
@ -390,6 +392,10 @@ pub enum VerifierError {
InvalidResponse, InvalidResponse,
SparqlError(String), SparqlError(String),
InboxError(String), InboxError(String),
QrCode(String),
InvalidProfile,
ContactAlreadyExists,
InternalError,
} }
impl Error for VerifierError {} impl Error for VerifierError {}

@ -293,7 +293,13 @@ impl Store {
) -> Result<(Repo, Vec<(Commit, Vec<Digest>)>), NgError> { ) -> Result<(Repo, Vec<(Commit, Vec<Digest>)>), NgError> {
let is_store = branch_crdt.is_none(); let is_store = branch_crdt.is_none();
if is_store { if is_store {
branch_crdt = Some(BranchCrdt::Graph("data:container".to_string())); branch_crdt = Some(BranchCrdt::Graph(
if is_private_store {
"data:container"
} else {
"social:profile"
}.to_string()
));
} }
let mut events = Vec::with_capacity(9); let mut events = Vec::with_capacity(9);

@ -888,6 +888,13 @@ impl StoreRepo {
} }
} }
pub fn is_public(&self) -> bool {
match self {
Self::V0(StoreRepoV0::PublicStore(_)) => true,
_ => false,
}
}
// pub fn overlay_id_for_storage_purpose( // pub fn overlay_id_for_storage_purpose(
// &self, // &self,
// store_overlay_branch_readcap_secret: Option<ReadCapSecret>, // store_overlay_branch_readcap_secret: Option<ReadCapSecret>,

@ -1053,6 +1053,70 @@ pub async fn wallet_import(
Ok(serde_wasm_bindgen::to_value(&client).unwrap()) Ok(serde_wasm_bindgen::to_value(&client).unwrap())
} }
#[wasm_bindgen]
pub async fn import_contact_from_qrcode(
session_id: JsValue,
doc_nuri: String,
qrcode: String,
) -> Result<(), String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
let mut request = AppRequest::new(
AppRequestCommandV0::QrCodeProfileImport,
NuriV0::new_from_repo_nuri(&doc_nuri).map_err(|e| e.to_string())?,
Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfileImport(qrcode))),
);
request.set_session_id(session_id);
let response = nextgraph::local_broker::app_request(request)
.await
.map_err(|e: NgError| e.to_string())?;
match response {
AppResponse::V0(AppResponseV0::Ok) => Ok(()),
AppResponse::V0(AppResponseV0::Error(e)) => Err(e),
_ => Err("invalid response".to_string()),
}
}
#[wasm_bindgen]
pub async fn get_qrcode_for_profile(
session_id: JsValue,
public: bool,
size: JsValue,
) -> Result<String, String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
let size: u32 = serde_wasm_bindgen::from_value::<u32>(size)
.map_err(|_| "Deserialization error of size".to_string())?;
let nuri = if public {
NuriV0::new_public_store_target()
} else {
NuriV0::new_protected_store_target()
};
let mut request = AppRequest::new(
AppRequestCommandV0::QrCodeProfile,
nuri,
Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfile(size))),
);
request.set_session_id(session_id);
let response = nextgraph::local_broker::app_request(request)
.await
.map_err(|e: NgError| e.to_string())?;
match response {
AppResponse::V0(AppResponseV0::Text(qrcode)) => Ok(qrcode),
AppResponse::V0(AppResponseV0::Error(e)) => Err(e),
_ => Err("invalid response".to_string()),
}
}
#[cfg(wasmpack_target = "nodejs")] #[cfg(wasmpack_target = "nodejs")]
#[wasm_bindgen(module = "/js/node.js")] #[wasm_bindgen(module = "/js/node.js")]
extern "C" { extern "C" {

@ -29,9 +29,11 @@ either = "1.8.1"
futures = "0.3.24" futures = "0.3.24"
lazy_static = "1.4.0" lazy_static = "1.4.0"
async-trait = "0.1.64" async-trait = "0.1.64"
base64-url = "2.0.0"
async-std = { version = "1.12.0", features = [ "attributes", "unstable" ] } async-std = { version = "1.12.0", features = [ "attributes", "unstable" ] }
automerge = "0.5.11" automerge = "0.5.11"
yrs = "0.19.2" yrs = "0.19.2"
qrcode = { version = "0.14.1", default-features = false, features = ["svg"] }
sbbf-rs-safe = "0.3.2" sbbf-rs-safe = "0.3.2"
ng-repo = { path = "../ng-repo", version = "0.1.1-alpha.2" } ng-repo = { path = "../ng-repo", version = "0.1.1-alpha.2" }
ng-net = { path = "../ng-net", version = "0.1.1-alpha.2" } ng-net = { path = "../ng-net", version = "0.1.1-alpha.2" }

@ -551,7 +551,6 @@ impl Verifier {
let commit_name = let commit_name =
NuriV0::commit_graph_name(&update.commit_id, &update.overlay_id); NuriV0::commit_graph_name(&update.commit_id, &update.overlay_id);
let commit_encoded = numeric_encoder::StrHash::new(&commit_name); let commit_encoded = numeric_encoder::StrHash::new(&commit_name);
let cv_graphname = NamedNode::new_unchecked(commit_name); let cv_graphname = NamedNode::new_unchecked(commit_name);
let cv_graphname_ref = GraphNameRef::NamedNode((&cv_graphname).into()); let cv_graphname_ref = GraphNameRef::NamedNode((&cv_graphname).into());
let ov_main = if branch_is_main { let ov_main = if branch_is_main {

@ -369,7 +369,7 @@ impl Verifier {
if msg.body.from_inbox.is_none() { if msg.body.from_inbox.is_none() {
// TODO log error // TODO log error
// we do nothing as this is invalid msg. it must have a from. // we do nothing as this is invalid msg. it must have a from.
return Ok(()) return Err(VerifierError::InvalidSocialQuery)
} }
// TODO: first we open the response.forwarder_id (because in webapp, it might not be loaded yet) // TODO: first we open the response.forwarder_id (because in webapp, it might not be loaded yet)
@ -544,12 +544,6 @@ impl Verifier {
let quads = triples.into_iter().map(|t| t.in_graph(graph_name.clone()) ).collect(); let quads = triples.into_iter().map(|t| t.in_graph(graph_name.clone()) ).collect();
// let quad = Quad {
// subject: NamedNode::new_unchecked(&nuri).into(),
// predicate: NG_ONTOLOGY_CLASS_NAME.clone().into(),
// object: Literal::new_simple_literal(primary_class).into(),
// graph_name: NamedNode::new_unchecked(&header_branch_nuri).into(),
// };
let commits = self.prepare_sparql_update(quads, vec![], vec![]).await?; let commits = self.prepare_sparql_update(quads, vec![], vec![]).await?;

@ -17,6 +17,8 @@ use futures::SinkExt;
use futures::StreamExt; use futures::StreamExt;
use ng_net::actor::SoS; use ng_net::actor::SoS;
use ng_net::types::InboxPost; use ng_net::types::InboxPost;
use ng_net::types::NgQRCode;
use ng_net::types::NgQRCodeProfileSharingV0;
use ng_oxigraph::oxigraph::sparql::EvaluationError; use ng_oxigraph::oxigraph::sparql::EvaluationError;
use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults}; use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults};
use ng_oxigraph::oxrdf::{Literal, NamedNode, Quad, Term}; use ng_oxigraph::oxrdf::{Literal, NamedNode, Quad, Term};
@ -151,6 +153,69 @@ impl Verifier {
} }
} }
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(
&branch_id,
&repo_id,
&store_repo.overlay_id_for_storage_purpose(),
);
let base = NuriV0::repo_id(&repo_id);
let mut deletes = String::new();
let mut wheres = String::new();
let mut inserts = String::new();
if let Some(about) = about {
deletes += &format!("<> <{NG_ONTOLOGY_ABOUT}> ?a. ");
wheres += &format!("OPTIONAL {{ <> <{NG_ONTOLOGY_ABOUT}> ?a }} ");
if about.len() > 0 {
inserts += &format!(
"<> <{NG_ONTOLOGY_ABOUT}> \"{}\". ",
about.replace("\\", "\\\\").replace("\"", "\\\"")
);
}
}
if let Some(title) = title {
deletes += &format!("<> <{NG_ONTOLOGY_TITLE}> ?n. ");
wheres += &format!("OPTIONAL {{ <> <{NG_ONTOLOGY_TITLE}> ?n }} ");
if title.len() > 0 {
inserts += &format!(
"<> <{NG_ONTOLOGY_TITLE}> \"{}\". ",
title.replace("\\", "\\\\").replace("\"", "\\\"")
);
}
}
let query = format!(
"DELETE {{ {deletes} }} INSERT {{ {inserts} }} WHERE {{ {wheres} }}"
);
let oxistore = self.graph_dataset.as_ref().unwrap();
let update = ng_oxigraph::oxigraph::sparql::Update::parse(&query, Some(&base))
.map_err(|e| NgError::InternalError)?;
let res = oxistore.ng_update(update, Some(graph_name));
match res {
Err(e) => Err(VerifierError::InternalError),
Ok((inserts, removes)) => {
if inserts.is_empty() && removes.is_empty() {
Ok(())
} else {
self
.prepare_sparql_update(
Vec::from_iter(inserts),
Vec::from_iter(removes),
self.get_peer_id_for_skolem(),
)
.await?;
Ok(())
}
}
}
}
fn resolve_header_branch( fn resolve_header_branch(
&self, &self,
target: &NuriTargetV0, target: &NuriTargetV0,
@ -648,6 +713,48 @@ impl Verifier {
Ok(nuri_result) Ok(nuri_result)
} }
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" };
// 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?
{
QueryResults::Boolean(true) => {
return Err(VerifierError::ContactAlreadyExists);
}
_ => {}
}
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}\"."));
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}>.
<> a vcard:Individual .
<> vcard:fn \"{}\".
{has_email} }}", contact.name);
let ret = self
.process_sparql_update(&contact_doc_nuri, &sparql_update, &Some(contact_doc_nuri_string), vec![])
.await;
if let Err(e) = ret {
return Err(VerifierError::SparqlError(e));
}
self.update_header(&contact_doc_nuri.target, Some(contact.name), None).await?;
Ok(())
}
pub(crate) async fn process( pub(crate) async fn process(
&mut self, &mut self,
command: &AppRequestCommandV0, command: &AppRequestCommandV0,
@ -665,6 +772,24 @@ impl Verifier {
return Err(NgError::InvalidPayload); return Err(NgError::InvalidPayload);
}; };
let query_id = nuri.target.repo_id();
// checking that the query hasn't been started yet
match self.sparql_query(
&NuriV0::new_repo_target_from_id(query_id),
format!("ASK {{ <> <did:ng:x:ng#social_query_forwarder> ?forwarder }}"), Some(NuriV0::repo_id(query_id))).await?
{
QueryResults::Boolean(true) => {
return Err(NgError::SocialQueryAlreadyStarted);
}
_ => {}
}
// return error if not connected
if self.connected_broker.is_none() {
return Err(NgError::NotConnected);
}
// TODO: search for contacts (all stores, one store, a sparql query, etc..) // TODO: search for contacts (all stores, one store, a sparql query, etc..)
// (profile_nuri, inbox_nuri) // (profile_nuri, inbox_nuri)
let contacts = if contacts_string.as_str() == "did:ng:d:c" { let contacts = if contacts_string.as_str() == "did:ng:d:c" {
@ -672,7 +797,7 @@ impl Verifier {
res.push(("did:ng:a:rjoQTS4LMBDcuh8CEjmTYrgALeApBg2cgKqyPEuQDUgA".to_string(),"did:ng:d:KMFdOcGjdFBQgA9QNEDWcgEErQ1isbvDe7d_xndNOUMA".to_string())); res.push(("did:ng:a:rjoQTS4LMBDcuh8CEjmTYrgALeApBg2cgKqyPEuQDUgA".to_string(),"did:ng:d:KMFdOcGjdFBQgA9QNEDWcgEErQ1isbvDe7d_xndNOUMA".to_string()));
res res
} else { } else {
unimplemented!(); return Ok(AppResponse::error(NgError::NotImplemented.to_string()));
}; };
// if no contact found, return here with an AppResponse::error // if no contact found, return here with an AppResponse::error
@ -694,7 +819,7 @@ impl Verifier {
let repo = self.repos.get(&from_profile_id).ok_or(NgError::RepoNotFound)?; let repo = self.repos.get(&from_profile_id).ok_or(NgError::RepoNotFound)?;
repo.store.clone() repo.store.clone()
}; };
let query_id = nuri.target.repo_id();
let definition_commit_body_ref = nuri.get_first_commit_ref()?; let definition_commit_body_ref = nuri.get_first_commit_ref()?;
let block_ids = Commit::collect_block_ids(definition_commit_body_ref.clone(), &store, true)?; let block_ids = Commit::collect_block_ids(definition_commit_body_ref.clone(), &store, true)?;
let mut blocks= Vec::with_capacity(block_ids.len()); let mut blocks= Vec::with_capacity(block_ids.len());
@ -777,76 +902,55 @@ impl Verifier {
// Ok(SoS::Single(_)) => Ok(AppResponse::ok()), // Ok(SoS::Single(_)) => Ok(AppResponse::ok()),
// }; // };
} }
AppRequestCommandV0::QrCodeProfile => {
let size = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfile(size))) =
payload
{
size
} else {
return Err(NgError::InvalidPayload);
};
let public = match nuri.target {
NuriTargetV0::PublicProfile => true,
NuriTargetV0::ProtectedProfile => false,
_ => return Err(NgError::InvalidPayload)
};
return match self.get_qrcode_for_profile(public, size).await {
Err(e) => Ok(AppResponse::error(e.to_string())),
Ok(qrcode) => Ok(AppResponse::text(qrcode)),
};
}
AppRequestCommandV0::QrCodeProfileImport => {
let profile = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfileImport( text))) =
payload
{
let ser = base64_url::decode(&text).map_err(|_| NgError::SerializationError)?;
let code:NgQRCode = serde_bare::from_slice(&ser)?;
let profile = match code {
NgQRCode::ProfileSharingV0(profile) => profile,
_ => return Err(NgError::InvalidPayload)
};
profile
} else {
return Err(NgError::InvalidPayload);
};
let repo_id = match nuri.target {
NuriTargetV0::Repo(id) => id,
_ => return Err(NgError::InvalidPayload)
};
return match self.import_contact_from_qrcode(repo_id, profile).await {
Err(e) => Ok(AppResponse::error(e.to_string())),
Ok(()) => Ok(AppResponse::ok()),
};
}
AppRequestCommandV0::Header => { AppRequestCommandV0::Header => {
if let Some(AppRequestPayload::V0(AppRequestPayloadV0::Header(doc_header))) = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::Header(doc_header))) =
payload payload
{ {
let (repo_id, branch_id, store_repo) = return match self.update_header(&nuri.target, doc_header.title, doc_header.about).await {
match self.resolve_header_branch(&nuri.target) { Ok(_) => Ok(AppResponse::ok()),
Err(e) => return Ok(AppResponse::error(e.to_string())), Err(e) => Ok(AppResponse::error(e.to_string()))
Ok(a) => a, };
};
let graph_name = NuriV0::branch_repo_graph_name(
&branch_id,
&repo_id,
&store_repo.overlay_id_for_storage_purpose(),
);
let base = NuriV0::repo_id(&repo_id);
let mut deletes = String::new();
let mut wheres = String::new();
let mut inserts = String::new();
if let Some(about) = doc_header.about {
deletes += &format!("<> <{NG_ONTOLOGY_ABOUT}> ?a. ");
wheres += &format!("OPTIONAL {{ <> <{NG_ONTOLOGY_ABOUT}> ?a }} ");
if about.len() > 0 {
inserts += &format!(
"<> <{NG_ONTOLOGY_ABOUT}> \"{}\". ",
about.replace("\\", "\\\\").replace("\"", "\\\"")
);
}
}
if let Some(title) = doc_header.title {
deletes += &format!("<> <{NG_ONTOLOGY_TITLE}> ?n. ");
wheres += &format!("OPTIONAL {{ <> <{NG_ONTOLOGY_TITLE}> ?n }} ");
if title.len() > 0 {
inserts += &format!(
"<> <{NG_ONTOLOGY_TITLE}> \"{}\". ",
title.replace("\\", "\\\\").replace("\"", "\\\"")
);
}
}
let query = format!(
"DELETE {{ {deletes} }} INSERT {{ {inserts} }} WHERE {{ {wheres} }}"
);
let oxistore = self.graph_dataset.as_ref().unwrap();
let update = ng_oxigraph::oxigraph::sparql::Update::parse(&query, Some(&base))
.map_err(|e| NgError::InternalError)?;
let res = oxistore.ng_update(update, Some(graph_name));
return Ok(match res {
Err(e) => AppResponse::error(NgError::InternalError.to_string()),
Ok((inserts, removes)) => {
if inserts.is_empty() && removes.is_empty() {
AppResponse::ok()
} else {
match self
.prepare_sparql_update(
Vec::from_iter(inserts),
Vec::from_iter(removes),
self.get_peer_id_for_skolem(),
)
.await
{
Err(e) => AppResponse::error(e.to_string()),
Ok(_) => AppResponse::ok(),
}
}
}
});
} else { } else {
return Err(NgError::InvalidPayload); return Err(NgError::InvalidPayload);
} }
@ -871,9 +975,7 @@ impl Verifier {
Err(e) => return Ok(AppResponse::error(e.to_string())), Err(e) => return Ok(AppResponse::error(e.to_string())),
Ok(a) => a, Ok(a) => a,
}; };
self.open_branch(&repo_id, &branch_id, true).await?; self.open_branch(&repo_id, &branch_id, true).await?;
let graph_name = NuriV0::branch_repo_graph_name( let graph_name = NuriV0::branch_repo_graph_name(
&branch_id, &branch_id,
&repo_id, &repo_id,
@ -1143,7 +1245,7 @@ impl Verifier {
}, },
}, },
_ => unimplemented!(), _ => return Err(NgError::NotImplemented),
} }
Ok(AppResponse::V0(AppResponseV0::Ok)) Ok(AppResponse::V0(AppResponseV0::Ok))
} }

@ -29,6 +29,8 @@ use ng_oxigraph::oxigraph::sparql::Query;
use ng_oxigraph::oxigraph::sparql::QueryResults; use ng_oxigraph::oxigraph::sparql::QueryResults;
use ng_oxigraph::oxrdf::Term; use ng_oxigraph::oxrdf::Term;
use ng_repo::utils::derive_key; use ng_repo::utils::derive_key;
use qrcode::render::svg;
use qrcode::QrCode;
use sbbf_rs_safe::Filter; use sbbf_rs_safe::Filter;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use web_time::SystemTime; use web_time::SystemTime;
@ -285,11 +287,9 @@ impl Verifier {
let base = NuriV0::repo_id(&repo.id); let base = NuriV0::repo_id(&repo.id);
let parsed = Query::parse(&format!("SELECT ?title ?about WHERE {{ OPTIONAL {{ <> <{NG_ONTOLOGY_ABOUT}> ?about }} OPTIONAL {{ <> <{NG_ONTOLOGY_TITLE}> ?title }} }}"), let parsed = Query::parse(&format!("SELECT ?title ?about WHERE {{ OPTIONAL {{ <> <{NG_ONTOLOGY_ABOUT}> ?about }} OPTIONAL {{ <> <{NG_ONTOLOGY_TITLE}> ?title }} }}"),
Some(&base)).map_err(|e| NgError::OxiGraphError(e.to_string()))?; Some(&base)).map_err(|e| NgError::OxiGraphError(e.to_string()))?;
let results = oxistore let results = oxistore
.query(parsed, Some(header_graph)) .query(parsed, Some(header_graph))
.map_err(|e| NgError::OxiGraphError(e.to_string()))?; .map_err(|e| NgError::OxiGraphError(e.to_string()))?;
match results { match results {
QueryResults::Solutions(mut sol) => { QueryResults::Solutions(mut sol) => {
if let Some(Ok(s)) = sol.next() { if let Some(Ok(s)) = sol.next() {
@ -1566,6 +1566,84 @@ impl Verifier {
Ok(()) Ok(())
} }
pub async fn get_qrcode_for_profile(&self, public: bool, size: u32) -> Result<String, VerifierError> {
let profile_id = if public {
self.public_store_id()
} else {
self.protected_store_id()
};
let repo = self.repos.get(&profile_id).ok_or(NgError::RepoNotFound)?;
let inbox = repo.inbox.to_owned().ok_or(NgError::InboxNotFound)?.to_pub();
let profile = repo.store.get_store_repo().clone();
let sparql = format!("
PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
SELECT ?name ?email WHERE
{{ <> vcard:fn ?name .
<> vcard:hasEmail ?email .
}}");
//log_info!("{sparql}");
let (mut name, mut email) = match self.sparql_query(
&NuriV0::new_repo_target_from_id(profile_id),
sparql, Some(NuriV0::repo_id(profile_id))).await?
{
QueryResults::Solutions(mut sols) => {
match sols.next() {
None => {
//log_info!("name or email not found");
(None, None)
}
Some(Err(e)) => {
return Err(VerifierError::SparqlError(e.to_string()));
}
Some(Ok(sol)) => {
let name = if let Some(Term::Literal(l)) = sol.get("name") {
Some(l.value().to_string())
} else {
None
};
let email = if let Some(Term::Literal(l)) = sol.get("email") {
Some(l.value().to_string())
} else {
None
};
(name, email)
}
}
}
_ => 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());
}
let profile_sharing = NgQRCode::ProfileSharingV0(NgQRCodeProfileSharingV0 {
inbox,
profile,
name: name.unwrap(),
email
});
let ser = serde_bare::to_vec(&profile_sharing)?;
let encoded = base64_url::encode(&ser);
log_info!("qrcode= {encoded}");
match QrCode::with_error_correction_level(encoded.as_bytes(), qrcode::EcLevel::M) {
Ok(qr) => {
Ok(qr
.render()
.max_dimensions(size, size)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.build()
)
}
Err(e) => Err(VerifierError::QrCode(e.to_string())),
}
}
pub async fn inbox(&mut self, msg: InboxMsg, from_queue: bool) { pub async fn inbox(&mut self, msg: InboxMsg, from_queue: bool) {
log_info!("RECEIVED INBOX MSG {:?}", msg); log_info!("RECEIVED INBOX MSG {:?}", msg);

@ -1479,36 +1479,3 @@ pub struct ShuffledPazzle {
pub emoji_indices: Vec<Vec<u8>>, pub emoji_indices: Vec<Vec<u8>>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NgQRCodeWalletTransferV0 {
pub broker: BrokerServerV0,
pub rendezvous: SymKey, // Rendez-vous ID
pub secret_key: SymKey,
pub is_rendezvous: bool,
}
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct NgQRCodeWalletRecoveryV0 {
#[zeroize(skip)]
pub wallet: WalletContentV0, //of which security_img is emptied
pub pazzle: Vec<u8>,
pub mnemonic: [u16; 12],
pub pin: [u8; 4],
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum NgQRCode {
WalletTransferV0(NgQRCodeWalletTransferV0),
WalletRecoveryV0(NgQRCodeWalletRecoveryV0),
}
impl NgQRCode {
pub fn from_code(code: String) -> Result<Self, NgError> {
let decoded = base64_url::decode(&code).map_err(|_| NgError::SerializationError)?;
Ok(serde_bare::from_slice(&decoded)?)
}
pub fn to_code(&self) -> String {
let ser = serde_bare::to_vec(self).unwrap();
base64_url::encode(&ser)
}
}

Loading…
Cancel
Save