use crate::constants::{BOLD, END, NP_TEMP_URI, TEST_SERVER};
use crate::error::NpError;
use crate::extract::{extract_np_info, NpInfo};
use crate::network::{fetch_np, publish_np};
use crate::profile::NpProfile;
use crate::sign::{make_trusty, normalize_dataset, replace_bnodes, replace_ns_in_quads};
use crate::utils::{parse_rdf, serialize_rdf, Namespace};
use crate::vocab::{dct, foaf, np, npx, pav, prov};
use base64::{engine, Engine as _};
use chrono::Utc;
use oxrdf::{
vocab::{rdf, xsd},
Dataset, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNodeRef, QuadRef,
TripleRef,
};
use rsa::pkcs8::DecodePublicKey;
use rsa::{sha2::Digest, sha2::Sha256, Pkcs1v15Sign, RsaPublicKey};
use std::collections::HashSet;
use std::fmt;
/// Trait to provide the nanopub RDF as string or dataset
pub trait RdfSource {
fn get_dataset(self) -> Result<(Dataset, Vec<(String, String)>), NpError>;
}
impl RdfSource for Dataset {
fn get_dataset(self) -> Result<(Dataset, Vec<(String, String)>), NpError> {
Ok((self, [].to_vec()))
}
}
impl RdfSource for &str {
fn get_dataset(self) -> Result<(Dataset, Vec<(String, String)>), NpError> {
parse_rdf(self)
}
}
impl RdfSource for &String {
fn get_dataset(self) -> Result<(Dataset, Vec<(String, String)>), NpError> {
parse_rdf(self)
}
}
/// A Nanopublication, contains the nanopub info (graphs URIs, signature, etc), and the RDF dataset.
#[derive(Clone, Debug)]
pub struct Nanopub {
pub info: NpInfo,
pub dataset: Dataset,
}
impl fmt::Display for Nanopub {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n{:?}", self.rdf())?;
writeln!(f, "URI: {}", self.info.uri.as_str())?;
writeln!(f, "Trusty hash: {}", self.info.trusty_hash)?;
writeln!(f, "Signature hash: {}", self.info.signature)?;
writeln!(f, "Public key: {}", self.info.public_key)?;
if let Some(published) = &self.info.published {
writeln!(f, "Published: {published:?}")?;
}
Ok(())
}
}
impl Nanopub {
pub fn new<T: RdfSource>(rdf: T) -> Result<Self, NpError> {
let (dataset, prefixes) = rdf.get_dataset()?;
let np_info = extract_np_info(&dataset, prefixes)?;
Ok(Self {
info: np_info,
dataset,
})
}
/// Fetch a Nanopub given its URI.
///
/// # Arguments
///
/// * `uri` - The URI of the nanopub to fetch
///
/// # Examples
///
/// ```
/// use std::fs;
/// use nanopub::Nanopub;
/// use tokio::runtime;
/// let url = "https://w3id.org/np/RAltRkGOtHoj5LcBJZ62AMVOAVc0hnxt45LMaCXgxJ4fw";
/// let rt = runtime::Runtime::new().expect("Runtime failed");
///
/// let np = rt.block_on(async {
/// Nanopub::fetch(&url).await
/// }).unwrap();
/// ```
pub async fn fetch(url: &str) -> Result<Self, NpError> {
let np_rdf = fetch_np(url).await?;
let (dataset, prefixes) = parse_rdf(&np_rdf)?;
let mut np_info = extract_np_info(&dataset, prefixes)?;
np_info.published = Some(url.to_string());
Ok(Self {
info: np_info,
dataset,
})
}
/// Check a given Nanopub RDF is valid (check trusty hash and signature).
///
/// A failed check will throw an error
///
/// # Arguments
///
/// * `rdf` - A string slice that holds the RDF of the nanopub
///
/// # Examples
///
/// ```
/// use std::fs;
/// use nanopub::{Nanopub, ProfileBuilder};
/// let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjY1gsFxmak6SOCouJPuEzHNForkqFhgfHE3aAIAx+Y5q6UDEDM9Q0EksheNffJB4iPqsAfiFpY0ARQY92K5r8P4+a78eu9reYrb2WxZb1qPJmvR7XZ6sN1oHD7dd/EyQoJmQsmOKdrqaLRbzR7tZrf52yvKkwNWXcIVhW8uxe7iUgxiojZpW9srKoK/qFRpaUZSKn7Z/zgtDH9FJkYbBsGPDMqp78Kzt+sJb+U2W+wCSSy34jIUxx6QRbzvn6uexc/emFw/1DU5y7zBudhgC7mVk8vX1gUNKyjZBzlOmRcretrANgffqs5fx/TMHN1xtkA/H1u1IKBfKoyk/xThMLAgMBAAECggEAECuG0GZA3HF8OaqFgMG+W+agOvH04h4Pqv4cHjYNxnxpFcNV9nEssTKWSOvCwYy7hrwZBGV3PQzbjFmmrxVFs20+8yCD7KbyKKQZPVC0zf84bj6NTNgvr6DpGtDxINxuGaMjCt7enqhoRyRRuZ0fj2gD3Wqae/Ds8cpDCefkyMg0TvauHSUj244vGq5nt93txUv1Sa+/8tWZ77Dm0s5a3wUYB2IeAMl5WrO2GMvgzwH+zT+4kvNWg5S0Ze4KE+dG3lSIYZjo99h14LcQS9eALC/VBcAJ6pRXaCTT/TULtcLNeOpoc9Fu25f0yTsDt6Ga5ApliYkb7rDhV+OFrw1sYQKBgQDCE9so+dPg7qbp0cV+lbb7rrV43m5s9Klq0riS7u8m71oTwhmvm6gSLfjzqb8GLrmflCK4lKPDSTdwyvd+2SSmOXySw94zr1Pvc7sHdmMRyA7mH3m+zSOOgyCTTKyhDRCNcRIkysoL+DecDhNo4Fumf71tsqDYogfxpAQhn0re8wKBgQDXhMmmT2oXiMnYHhi2k7CJe3HUqkZgmW4W44SWqKHp0V6sjcHm0N0RT5Hz1BFFUd5Y0ZB3JLcah19myD1kKYCj7xz6oVLb8O7LeAZNlb0FsrtD7NU+Hciywo8qESiA7UYDkU6+hsmxaI01DsttMIdG4lSBbEjA7t4IQC5lyr7xiQKBgQCN87YGJ40Y5ZXCSgOZDepz9hqX2KGOIfnUv2HvXsIfiUwqTXs6HbD18xg3KL4myIBOvywSM+4ABYp+foY+Cpcq2btLIeZhiWjsKIrw71+Q/vIe0YDb1PGf6DsoYhmWBpdHzR9HN+hGjvwlsYny2L9Qbfhgxxmsuf7zeFLpQLijjwKBgH7TD28k8IOk5VKec2CNjKd600OYaA3UfCpP/OhDl/RmVtYoHWDcrBrRvkvEEd2/DZ8qw165Zl7gJs3vK+FTYvYVcfIzGPWA1KU7nkntwewmf3i7V8lT8ZTwVRsmObWU60ySJ8qKuwoBQodki2VX12NpMN1wgWe3qUUlr6gLJU4xAoGAet6nD3QKwk6TTmcGVfSWOzvpaDEzGkXjCLaxLKh9GreM/OE+h5aN2gUoFeQapG5rUwI/7Qq0xiLbRXw+OmfAoV2XKv7iI8DjdIh0F06mlEAwQ/B0CpbqkuuxphIbchtdcz/5ra233r3BMNIqBl3VDDVoJlgHPg9msOTRy13lFqc=";
/// let np_rdf = fs::read_to_string("./tests/resources/signed.simple1-rsa.trig").unwrap();
/// let profile = ProfileBuilder::new(private_key.to_string())
/// .with_orcid("https://orcid.org/0000-0000-0000-0000".to_string())
/// .build().unwrap();
/// let np = Nanopub::new(&np_rdf).unwrap().check();
/// ```
pub fn check(mut self) -> Result<Self, NpError> {
let _ = self.is_valid()?;
let mut msg: String = "".to_string();
if self.info.trusty_hash.is_empty() {
msg = format!("{msg}1 valid (not trusty)");
} else {
// Check Trusty hash if found
let expected_hash = make_trusty(
&self.dataset,
self.info.uri.as_str(),
&self.info.normalized_ns,
&self.info.separator_after_trusty,
)?;
if expected_hash != self.info.trusty_hash {
return Err(NpError(format!("Invalid Nanopub: the hash of the nanopublication is different than the expected hash \n{}\n{}", self.info.trusty_hash, expected_hash).to_string()));
}
msg = format!("{msg}1 trusty");
}
// Check the signature is valid if found
if !self.info.signature.is_empty() {
// Remove the signature from the graph before re-generating it
self.dataset.remove(QuadRef::new(
NamedOrBlankNodeRef::from(self.info.signature_iri.as_ref()),
npx::HAS_SIGNATURE,
LiteralRef::new_simple_literal(self.info.signature.as_str()),
GraphNameRef::from(self.info.pubinfo.as_ref()),
));
// Normalize nanopub nquads to a string
let norm_quads = normalize_dataset(
&self.dataset,
self.info.uri.as_str(),
&self.info.normalized_ns,
&self.info.separator_after_trusty,
)?;
// println!("NORMED QUADS CHECK\n{}", norm_quads);
// Load public key, and regenerate and check the signature hash
RsaPublicKey::from_public_key_der(
&engine::general_purpose::STANDARD.decode(&self.info.public_key)?,
)?
.verify(
Pkcs1v15Sign::new::<Sha256>(),
&Sha256::digest(norm_quads.as_bytes()),
&engine::general_purpose::STANDARD.decode(self.info.signature.as_bytes())?,
)?;
msg = format!("{msg} with signature");
} else {
msg = format!("{msg} without signature");
}
println!(
"\nā
Nanopub {}{}{} is valid: {}",
BOLD,
self.info.uri.as_str(),
END,
msg
);
// let rdf = serialize_rdf(&self.dataset, &self.info.uri.as_ref(), &self.info.ns.as_ref())?;
// TODO: should check return a string or a Nanopub? A string is not easy to process by machines
// Should we check if the np has been published with Nanopub::fetch?
Ok(self)
}
/// Sign a nanopub: generate and add signature and trusty URI. If the nanopub is already signed, unsign it first.
///
/// # Arguments
///
/// * `rdf` - A string slice that holds the RDF of the nanopub
/// * `profile` - The NpProfile with private key and ORCID
///
/// # Examples
///
/// ```
/// use std::fs;
/// use nanopub::{Nanopub, ProfileBuilder};
/// let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjY1gsFxmak6SOCouJPuEzHNForkqFhgfHE3aAIAx+Y5q6UDEDM9Q0EksheNffJB4iPqsAfiFpY0ARQY92K5r8P4+a78eu9reYrb2WxZb1qPJmvR7XZ6sN1oHD7dd/EyQoJmQsmOKdrqaLRbzR7tZrf52yvKkwNWXcIVhW8uxe7iUgxiojZpW9srKoK/qFRpaUZSKn7Z/zgtDH9FJkYbBsGPDMqp78Kzt+sJb+U2W+wCSSy34jIUxx6QRbzvn6uexc/emFw/1DU5y7zBudhgC7mVk8vX1gUNKyjZBzlOmRcretrANgffqs5fx/TMHN1xtkA/H1u1IKBfKoyk/xThMLAgMBAAECggEAECuG0GZA3HF8OaqFgMG+W+agOvH04h4Pqv4cHjYNxnxpFcNV9nEssTKWSOvCwYy7hrwZBGV3PQzbjFmmrxVFs20+8yCD7KbyKKQZPVC0zf84bj6NTNgvr6DpGtDxINxuGaMjCt7enqhoRyRRuZ0fj2gD3Wqae/Ds8cpDCefkyMg0TvauHSUj244vGq5nt93txUv1Sa+/8tWZ77Dm0s5a3wUYB2IeAMl5WrO2GMvgzwH+zT+4kvNWg5S0Ze4KE+dG3lSIYZjo99h14LcQS9eALC/VBcAJ6pRXaCTT/TULtcLNeOpoc9Fu25f0yTsDt6Ga5ApliYkb7rDhV+OFrw1sYQKBgQDCE9so+dPg7qbp0cV+lbb7rrV43m5s9Klq0riS7u8m71oTwhmvm6gSLfjzqb8GLrmflCK4lKPDSTdwyvd+2SSmOXySw94zr1Pvc7sHdmMRyA7mH3m+zSOOgyCTTKyhDRCNcRIkysoL+DecDhNo4Fumf71tsqDYogfxpAQhn0re8wKBgQDXhMmmT2oXiMnYHhi2k7CJe3HUqkZgmW4W44SWqKHp0V6sjcHm0N0RT5Hz1BFFUd5Y0ZB3JLcah19myD1kKYCj7xz6oVLb8O7LeAZNlb0FsrtD7NU+Hciywo8qESiA7UYDkU6+hsmxaI01DsttMIdG4lSBbEjA7t4IQC5lyr7xiQKBgQCN87YGJ40Y5ZXCSgOZDepz9hqX2KGOIfnUv2HvXsIfiUwqTXs6HbD18xg3KL4myIBOvywSM+4ABYp+foY+Cpcq2btLIeZhiWjsKIrw71+Q/vIe0YDb1PGf6DsoYhmWBpdHzR9HN+hGjvwlsYny2L9Qbfhgxxmsuf7zeFLpQLijjwKBgH7TD28k8IOk5VKec2CNjKd600OYaA3UfCpP/OhDl/RmVtYoHWDcrBrRvkvEEd2/DZ8qw165Zl7gJs3vK+FTYvYVcfIzGPWA1KU7nkntwewmf3i7V8lT8ZTwVRsmObWU60ySJ8qKuwoBQodki2VX12NpMN1wgWe3qUUlr6gLJU4xAoGAet6nD3QKwk6TTmcGVfSWOzvpaDEzGkXjCLaxLKh9GreM/OE+h5aN2gUoFeQapG5rUwI/7Qq0xiLbRXw+OmfAoV2XKv7iI8DjdIh0F06mlEAwQ/B0CpbqkuuxphIbchtdcz/5ra233r3BMNIqBl3VDDVoJlgHPg9msOTRy13lFqc=";
/// let np_rdf = fs::read_to_string("./tests/resources/simple1-rsa.trig").unwrap();
/// let profile = ProfileBuilder::new(private_key.to_string())
/// .with_orcid("https://orcid.org/0000-0000-0000-0000".to_string())
/// .build().unwrap();
/// let np = Nanopub::new(&np_rdf).unwrap().sign(&profile);
/// ```
pub fn sign(mut self, profile: &NpProfile) -> Result<Self, NpError> {
// openssl_probe::init_ssl_cert_env_vars();
// unsafe {
// openssl_probe::init_openssl_env_vars();
// }
self.dataset =
replace_bnodes(&self.dataset, self.info.ns.as_str(), self.info.uri.as_str())?;
self.info = extract_np_info(&self.dataset, self.info.prefixes)?;
if !self.info.signature.is_empty() {
println!("Nanopub already signed, unsigning it before re-signing");
self = self.unsign()?;
// println!("DEBUG: Unsigned: {}", self.rdf()?);
}
let sig_iri = self.info.ns.get("sig");
let sig_node = sig_iri.as_ref();
let ns_node = self.info.ns.as_iri_ref();
let uri_subject_term = NamedOrBlankNodeRef::from(self.info.uri.as_ref());
let ns_subject_term = NamedOrBlankNodeRef::from(ns_node);
let pubinfo_graph_iri = GraphNameRef::from(self.info.pubinfo.as_ref());
let mut pubinfo_graph = self.dataset.graph_mut(pubinfo_graph_iri);
// Add triples about the signature in the pubinfo
pubinfo_graph.insert(TripleRef::new(
sig_node,
npx::HAS_PUBLIC_KEY,
LiteralRef::new_simple_literal(profile.public_key.as_str()),
));
pubinfo_graph.insert(TripleRef::new(
sig_node,
npx::HAS_ALGORITHM,
LiteralRef::new_simple_literal("RSA"),
));
pubinfo_graph.insert(TripleRef::new(sig_node, npx::HAS_SIGNATURE_TARGET, ns_node));
// If not already set, automatically add the current date to pubinfo created
if !pubinfo_graph
.triples_for_predicate(dct::CREATED)
.any(|x| x.subject == uri_subject_term || x.subject == ns_subject_term)
{
pubinfo_graph.insert(TripleRef::new(
ns_node,
dct::CREATED,
LiteralRef::new_typed_literal(
Utc::now()
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
.to_string()
.as_str(),
xsd::DATE_TIME,
),
));
}
// If ORCID provided in profile, and not already defined in nanopub, add to pubinfo graph
if let Some(orcid) = &profile.orcid_id {
if !pubinfo_graph.iter().any(|x| {
(x.subject == uri_subject_term || x.subject == ns_subject_term)
&& (x.predicate == dct::CREATOR
|| x.predicate == prov::WAS_ATTRIBUTED_TO
|| x.predicate == pav::CREATED_BY)
}) {
let orcid_node = NamedNodeRef::new_unchecked(orcid.as_str());
pubinfo_graph.insert(TripleRef::new(ns_node, dct::CREATOR, orcid_node));
}
}
// Normalize nanopub nquads to a string
let norm_quads = normalize_dataset(
&self.dataset,
self.info.ns.as_str(),
&self.info.normalized_ns,
&self.info.separator_after_trusty,
)?;
// println!("NORMED QUADS sign before add signature\n{}", norm_quads);
// Generate signature using the private key and normalized RDF
let signature_hash =
engine::general_purpose::STANDARD.encode(profile.get_private_key()?.sign(
Pkcs1v15Sign::new::<Sha256>(),
&Sha256::digest(norm_quads.as_bytes()),
)?);
// Add the signature to the pubinfo graph
self.dataset.insert(QuadRef::new(
sig_node,
npx::HAS_SIGNATURE,
LiteralRef::new_simple_literal(signature_hash.as_str()),
pubinfo_graph_iri,
));
// Generate Trusty URI, and replace the old URI with the trusty URI in the dataset
let trusty_hash = make_trusty(
&self.dataset,
self.info.ns.as_str(),
&self.info.normalized_ns,
&self.info.separator_after_trusty,
)?;
let trusty_uri = format!("{}{trusty_hash}", self.info.normalized_ns);
let trusty_ns = format!("{trusty_uri}/");
self.dataset = replace_ns_in_quads(
&self.dataset,
self.info.ns.as_str(),
self.info.uri.as_str(),
&trusty_ns,
&trusty_uri,
)?;
// TODO: it would be more efficient to assign the self.info field directly in the code above
self.info = extract_np_info(&self.dataset, self.info.prefixes)?;
let _ = self.is_valid()?;
// Return the signed Nanopub object
Ok(self)
}
/// Publish a nanopub to a nanopub server. If the nanopub is not signed and a profile is provided, it will be signed before publishing.
///
/// # Arguments
///
/// * `rdf` - A string slice that holds the RDF of the nanopub
/// * `profile` - The NpProfile with private key and ORCID
/// * `server_url` - The URL of the server to publish to
///
/// # Examples
///
/// ```
/// use std::fs;
/// use nanopub::{Nanopub, ProfileBuilder};
/// use tokio::runtime;
///
/// let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjY1gsFxmak6SOCouJPuEzHNForkqFhgfHE3aAIAx+Y5q6UDEDM9Q0EksheNffJB4iPqsAfiFpY0ARQY92K5r8P4+a78eu9reYrb2WxZb1qPJmvR7XZ6sN1oHD7dd/EyQoJmQsmOKdrqaLRbzR7tZrf52yvKkwNWXcIVhW8uxe7iUgxiojZpW9srKoK/qFRpaUZSKn7Z/zgtDH9FJkYbBsGPDMqp78Kzt+sJb+U2W+wCSSy34jIUxx6QRbzvn6uexc/emFw/1DU5y7zBudhgC7mVk8vX1gUNKyjZBzlOmRcretrANgffqs5fx/TMHN1xtkA/H1u1IKBfKoyk/xThMLAgMBAAECggEAECuG0GZA3HF8OaqFgMG+W+agOvH04h4Pqv4cHjYNxnxpFcNV9nEssTKWSOvCwYy7hrwZBGV3PQzbjFmmrxVFs20+8yCD7KbyKKQZPVC0zf84bj6NTNgvr6DpGtDxINxuGaMjCt7enqhoRyRRuZ0fj2gD3Wqae/Ds8cpDCefkyMg0TvauHSUj244vGq5nt93txUv1Sa+/8tWZ77Dm0s5a3wUYB2IeAMl5WrO2GMvgzwH+zT+4kvNWg5S0Ze4KE+dG3lSIYZjo99h14LcQS9eALC/VBcAJ6pRXaCTT/TULtcLNeOpoc9Fu25f0yTsDt6Ga5ApliYkb7rDhV+OFrw1sYQKBgQDCE9so+dPg7qbp0cV+lbb7rrV43m5s9Klq0riS7u8m71oTwhmvm6gSLfjzqb8GLrmflCK4lKPDSTdwyvd+2SSmOXySw94zr1Pvc7sHdmMRyA7mH3m+zSOOgyCTTKyhDRCNcRIkysoL+DecDhNo4Fumf71tsqDYogfxpAQhn0re8wKBgQDXhMmmT2oXiMnYHhi2k7CJe3HUqkZgmW4W44SWqKHp0V6sjcHm0N0RT5Hz1BFFUd5Y0ZB3JLcah19myD1kKYCj7xz6oVLb8O7LeAZNlb0FsrtD7NU+Hciywo8qESiA7UYDkU6+hsmxaI01DsttMIdG4lSBbEjA7t4IQC5lyr7xiQKBgQCN87YGJ40Y5ZXCSgOZDepz9hqX2KGOIfnUv2HvXsIfiUwqTXs6HbD18xg3KL4myIBOvywSM+4ABYp+foY+Cpcq2btLIeZhiWjsKIrw71+Q/vIe0YDb1PGf6DsoYhmWBpdHzR9HN+hGjvwlsYny2L9Qbfhgxxmsuf7zeFLpQLijjwKBgH7TD28k8IOk5VKec2CNjKd600OYaA3UfCpP/OhDl/RmVtYoHWDcrBrRvkvEEd2/DZ8qw165Zl7gJs3vK+FTYvYVcfIzGPWA1KU7nkntwewmf3i7V8lT8ZTwVRsmObWU60ySJ8qKuwoBQodki2VX12NpMN1wgWe3qUUlr6gLJU4xAoGAet6nD3QKwk6TTmcGVfSWOzvpaDEzGkXjCLaxLKh9GreM/OE+h5aN2gUoFeQapG5rUwI/7Qq0xiLbRXw+OmfAoV2XKv7iI8DjdIh0F06mlEAwQ/B0CpbqkuuxphIbchtdcz/5ra233r3BMNIqBl3VDDVoJlgHPg9msOTRy13lFqc=";
/// let np_rdf = fs::read_to_string("./tests/resources/simple1-rsa.trig").unwrap();
/// let profile = ProfileBuilder::new(private_key.to_string())
/// .with_orcid("https://orcid.org/0000-0000-0000-0000".to_string())
/// .build().unwrap();
/// let rt = runtime::Runtime::new().expect("Runtime failed");
///
/// let np = rt.block_on(async {
/// Nanopub::new(&np_rdf).unwrap().publish(Some(&profile), None).await
/// }).unwrap();
/// ```
pub async fn publish(
mut self,
profile: Option<&NpProfile>,
server_url: Option<&str>,
) -> Result<Self, NpError> {
self = if let Some(profile) = profile {
// If profile provided we sign the nanopub
self.sign(profile)?
} else if self.info.signature.is_empty() {
// If no profile and nanopub not signed we throw an error
return Err(NpError(format!(
"No profile provided and nanopub not signed, could not sign the Nanopublication \n{self}"
)));
} else {
// If no profile provided, but the nanopub is already signed, we verify it, then publish it
self.check()?
};
// Use test server if server_url not provided
let server_url = if let Some(server_url) = server_url {
if server_url.is_empty() {
TEST_SERVER.to_string()
} else {
server_url.to_string()
}
} else {
TEST_SERVER.to_string()
};
let published = publish_np(&server_url, &self.rdf()?).await?;
if published {
if TEST_SERVER == server_url {
self.info.published = Some(format!("{}{}", server_url, self.info.trusty_hash));
} else {
self.info.published = Some(self.info.uri.to_string());
}
// println!(
// "\nš Nanopublication published at {}{:?}{}",
// BOLD, self.info.published, END
// );
} else {
return Err(NpError(format!(
"Issue publishing the Nanopublication \n{self}"
)));
}
Ok(self)
}
/// Unsign a signed nanopub RDF. Remove signature triples and replace trusty URI with default temp URI
pub fn unsign(mut self) -> Result<Self, NpError> {
let signature_node = NamedOrBlankNodeRef::from(self.info.signature_iri.as_ref());
let pubinfo_graph = GraphNameRef::from(self.info.pubinfo.as_ref());
self.dataset.remove(QuadRef::new(
signature_node,
npx::HAS_PUBLIC_KEY,
LiteralRef::new_simple_literal(self.info.public_key.as_str()),
pubinfo_graph,
));
self.dataset.remove(QuadRef::new(
signature_node,
npx::HAS_ALGORITHM,
LiteralRef::new_simple_literal(self.info.algo.as_str()),
pubinfo_graph,
));
self.dataset.remove(QuadRef::new(
signature_node,
npx::HAS_SIGNATURE_TARGET,
NamedOrBlankNodeRef::from(self.info.uri.as_ref()),
pubinfo_graph,
));
self.dataset.remove(QuadRef::new(
signature_node,
npx::HAS_SIGNATURE,
LiteralRef::new_simple_literal(self.info.signature.as_str()),
pubinfo_graph,
));
self.dataset = replace_ns_in_quads(
&self.dataset,
self.info.ns.as_str(),
self.info.uri.as_str(),
NP_TEMP_URI,
NP_TEMP_URI,
)?;
self.info.uri = NamedNode::new_unchecked(NP_TEMP_URI.to_string());
self.info.ns = Namespace(NP_TEMP_URI.to_string());
self.info = extract_np_info(&self.dataset, self.info.prefixes)?;
Ok(self)
}
/// Create a Nanopub intro for a Profile
///
/// # Arguments
///
/// * `profile` - The NpProfile with private key and ORCID
/// * `server_url` - The URL of the server to publish to
///
/// # Example
///
/// ```
/// use std::fs;
/// use nanopub::{Nanopub, ProfileBuilder};
/// use tokio::runtime;
///
/// let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjY1gsFxmak6SOCouJPuEzHNForkqFhgfHE3aAIAx+Y5q6UDEDM9Q0EksheNffJB4iPqsAfiFpY0ARQY92K5r8P4+a78eu9reYrb2WxZb1qPJmvR7XZ6sN1oHD7dd/EyQoJmQsmOKdrqaLRbzR7tZrf52yvKkwNWXcIVhW8uxe7iUgxiojZpW9srKoK/qFRpaUZSKn7Z/zgtDH9FJkYbBsGPDMqp78Kzt+sJb+U2W+wCSSy34jIUxx6QRbzvn6uexc/emFw/1DU5y7zBudhgC7mVk8vX1gUNKyjZBzlOmRcretrANgffqs5fx/TMHN1xtkA/H1u1IKBfKoyk/xThMLAgMBAAECggEAECuG0GZA3HF8OaqFgMG+W+agOvH04h4Pqv4cHjYNxnxpFcNV9nEssTKWSOvCwYy7hrwZBGV3PQzbjFmmrxVFs20+8yCD7KbyKKQZPVC0zf84bj6NTNgvr6DpGtDxINxuGaMjCt7enqhoRyRRuZ0fj2gD3Wqae/Ds8cpDCefkyMg0TvauHSUj244vGq5nt93txUv1Sa+/8tWZ77Dm0s5a3wUYB2IeAMl5WrO2GMvgzwH+zT+4kvNWg5S0Ze4KE+dG3lSIYZjo99h14LcQS9eALC/VBcAJ6pRXaCTT/TULtcLNeOpoc9Fu25f0yTsDt6Ga5ApliYkb7rDhV+OFrw1sYQKBgQDCE9so+dPg7qbp0cV+lbb7rrV43m5s9Klq0riS7u8m71oTwhmvm6gSLfjzqb8GLrmflCK4lKPDSTdwyvd+2SSmOXySw94zr1Pvc7sHdmMRyA7mH3m+zSOOgyCTTKyhDRCNcRIkysoL+DecDhNo4Fumf71tsqDYogfxpAQhn0re8wKBgQDXhMmmT2oXiMnYHhi2k7CJe3HUqkZgmW4W44SWqKHp0V6sjcHm0N0RT5Hz1BFFUd5Y0ZB3JLcah19myD1kKYCj7xz6oVLb8O7LeAZNlb0FsrtD7NU+Hciywo8qESiA7UYDkU6+hsmxaI01DsttMIdG4lSBbEjA7t4IQC5lyr7xiQKBgQCN87YGJ40Y5ZXCSgOZDepz9hqX2KGOIfnUv2HvXsIfiUwqTXs6HbD18xg3KL4myIBOvywSM+4ABYp+foY+Cpcq2btLIeZhiWjsKIrw71+Q/vIe0YDb1PGf6DsoYhmWBpdHzR9HN+hGjvwlsYny2L9Qbfhgxxmsuf7zeFLpQLijjwKBgH7TD28k8IOk5VKec2CNjKd600OYaA3UfCpP/OhDl/RmVtYoHWDcrBrRvkvEEd2/DZ8qw165Zl7gJs3vK+FTYvYVcfIzGPWA1KU7nkntwewmf3i7V8lT8ZTwVRsmObWU60ySJ8qKuwoBQodki2VX12NpMN1wgWe3qUUlr6gLJU4xAoGAet6nD3QKwk6TTmcGVfSWOzvpaDEzGkXjCLaxLKh9GreM/OE+h5aN2gUoFeQapG5rUwI/7Qq0xiLbRXw+OmfAoV2XKv7iI8DjdIh0F06mlEAwQ/B0CpbqkuuxphIbchtdcz/5ra233r3BMNIqBl3VDDVoJlgHPg9msOTRy13lFqc=";
/// let orcid = "https://orcid.org/0000-0000-0000-0000";
/// let profile = ProfileBuilder::new(private_key.to_string())
/// .with_orcid("https://orcid.org/0000-0000-0000-0000".to_string())
/// .with_name("User Name".to_string())
/// .build().unwrap();
/// let rt = runtime::Runtime::new().expect("Runtime failed");
///
/// let np = rt.block_on(async {
/// Nanopub::new_intro(&profile).unwrap().publish(Some(&profile), None).await.unwrap()
/// });
/// ```
pub fn new_intro(profile: &NpProfile) -> Result<Self, NpError> {
let orcid = profile
.orcid_id
.as_ref()
.ok_or_else(|| NpError("Invalid Profile: ORCID is empty.".to_string()))?
.as_str();
let name = profile
.name
.as_ref()
.ok_or_else(|| NpError("Invalid Profile: name is empty.".to_string()))?
.as_str();
let mut dataset = create_base_dataset()?;
let np_ns = Namespace(NP_TEMP_URI.to_string());
let key_declaration_iri = np_ns.get("keyDeclaration");
let assertion_iri = np_ns.get("assertion");
let prov_iri = np_ns.get("provenance");
let key_declaration_node = key_declaration_iri.as_ref();
let orcid_node = NamedNodeRef::new_unchecked(orcid);
let assertion_node = assertion_iri.as_ref();
let assertion_graph = GraphNameRef::from(assertion_node);
let prov_node = prov_iri.as_ref();
// Assertion graph triples, add key declaration
dataset.insert(QuadRef::new(
key_declaration_node,
npx::DECLARED_BY,
orcid_node,
assertion_graph,
));
dataset.insert(QuadRef::new(
key_declaration_node,
npx::HAS_ALGORITHM,
LiteralRef::new_simple_literal("RSA"),
assertion_graph,
));
dataset.insert(QuadRef::new(
key_declaration_node,
npx::HAS_PUBLIC_KEY,
LiteralRef::new_simple_literal(profile.public_key.as_str()),
assertion_graph,
));
dataset.insert(QuadRef::new(
orcid_node,
foaf::NAME,
LiteralRef::new_simple_literal(name),
assertion_graph,
));
// Provenance graph triples
dataset.insert(QuadRef::new(
assertion_node,
prov::WAS_ATTRIBUTED_TO,
assertion_node,
prov_node,
));
Ok(Self {
info: extract_np_info(&dataset, [].to_vec())?,
dataset,
})
}
/// Check if Nanopub is valid: minimal required triples in assertion, prov, pubinfo graphs
pub fn is_valid(&self) -> Result<bool, NpError> {
let prov_node = self.info.prov.as_ref();
let pubinfo_node = self.info.pubinfo.as_ref();
if self
.dataset
.quads_for_graph_name(self.info.assertion.as_ref())
.next()
.is_none()
{
return Err(NpError(
"Invalid Nanopub: no triples in the assertion graph.".to_string(),
));
}
if self
.dataset
.quads_for_graph_name(prov_node)
.next()
.is_none()
{
return Err(NpError(
"Invalid Nanopub: no triples in the provenance graph.".to_string(),
));
}
if self
.dataset
.graph(prov_node)
.triples_for_subject(self.info.assertion.as_ref())
.next()
.is_none()
{
return Err(NpError("Invalid Nanopub: no triples with the assertion graph as subject in the provenance graph.".to_string()));
}
if self
.dataset
.quads_for_graph_name(pubinfo_node)
.next()
.is_none()
{
return Err(NpError(
"Invalid Nanopub: no triples in the pubinfo graph.".to_string(),
));
}
if !self.dataset.quads_for_graph_name(pubinfo_node).any(|x| {
x.subject == NamedOrBlankNodeRef::from(self.info.uri.as_ref())
|| x.subject == NamedOrBlankNodeRef::from(self.info.ns.as_iri_ref())
}) {
return Err(NpError(
"Invalid Nanopub: no triples with the nanopub URI as subject in the pubinfo graph."
.to_string(),
));
}
let graph_names: HashSet<GraphNameRef> = self
.dataset
.into_iter()
.filter(|x| x.graph_name.is_named_node())
.map(|g| g.graph_name)
.collect();
if graph_names.len() > 4 {
return Err(NpError(
format!("Invalid Nanopub: it should have 4 graphs (head, assertion, provenance, pubinfo), but the given nanopub has {} graphs.", graph_names.len())
));
}
Ok(true)
}
/// Returns the RDF of the nanopub
pub fn rdf(&self) -> Result<String, NpError> {
serialize_rdf(
&self.dataset,
self.info.uri.as_str(),
self.info.ns.as_str(),
&self.info.prefixes,
)
}
}
/// Bootstrap a base nanopub dataset that can be edited later
pub fn create_base_dataset() -> Result<Dataset, NpError> {
let mut dataset = Dataset::new();
let np_ns = Namespace(NP_TEMP_URI.to_string());
let assertion_iri = np_ns.get("assertion");
let prov_iri = np_ns.get("provenance");
let pubinfo_iri = np_ns.get("pubinfo");
let head_iri = np_ns.get("Head");
let np_node = NamedNodeRef::new_unchecked(NP_TEMP_URI);
let assertion_node = assertion_iri.as_ref();
let head_graph = GraphNameRef::from(head_iri.as_ref());
// Add Head graph triples
dataset.insert(QuadRef::new(
np_node,
np::HAS_ASSERTION,
assertion_node,
head_graph,
));
dataset.insert(QuadRef::new(
np_node,
np::HAS_PROVENANCE,
prov_iri.as_ref(),
head_graph,
));
dataset.insert(QuadRef::new(
np_node,
np::HAS_PUBLICATION_INFO,
pubinfo_iri.as_ref(),
head_graph,
));
dataset.insert(QuadRef::new(
np_node,
rdf::TYPE,
np::NANOPUBLICATION,
head_graph,
));
Ok(dataset)
}