Initial Commit
This commit is contained in:
commit
0bdaa94dd5
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
*.profile
|
||||
*.sqlite
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,2 @@
|
|||
[workspace]
|
||||
members=["./niwl", "niwl-client","niwl-server"]
|
|
@ -0,0 +1,35 @@
|
|||
# niwl - a prototype system for metadata resistant notifications
|
||||
|
||||
**niwl** (_/nɪu̯l/_) - fog, mist or haze (Welsh).
|
||||
|
||||
Privacy preserving applications often require a mechanism for providing notifications to parties that an
|
||||
event has happened e.g. a new group message, a payment etc.
|
||||
|
||||
**niwl** provides a set of libraries, clients and servers to provide this in a metadata resistant, bandwidth
|
||||
efficient way based on [fuzzytags](https://crates.io/crates/fuzzytags).
|
||||
|
||||
# Overview
|
||||
|
||||
**niwl-server** provides a web server with a json API for posting new tags and querying the tags database.
|
||||
|
||||
**niwl-client** provides a command-line application for managing secrets, tagging keys of parties and posting / querying
|
||||
for new tags.
|
||||
|
||||
For a more detailed overview please check out each individual crate.
|
||||
|
||||
# Example
|
||||
|
||||
|
||||
niwl-client alice.profile generate "alice"
|
||||
Tagging Key: auaaaaaaaaaaaylmnfrwkgaaaaaaaaaaadlbii3y7r6vmc7upbxa4myohaqmr5xl22bdxeed4abkotnovlmakzdo5stq2ibtjewm4rnkgzqwglrt72zfeyomvdpxqnu4ci4hwebyiseyn7pqfxypnvef7a3flu2hby7gdluh6wocxa5mvmimi2xorydcqaca2p2aevmue4cwyxnw2h7fkps7e6grgls66zgohbnwjibt6nlsdqjbrdjrzlsc3at3f43jyniz2i67ng6xdty5pr3elzedhjlvefhd6pjfc7g4owrz3dkq5xt2hhh3vvctkywqkcwriguayyx3pourepfs7s76bekrjgcjgj6zyid3ixmeh5ewqhkhxhzevf3uogvscxtpbksaclhccht7pj2fungnztfghshd6lsmegmysiiuyav6schtmyxmne2vfi4j4cxllm2crj3cqofsxjlxov3ms2zgtjzyxtubwtnwspc4jhijz4kufm6r3qkhpcyibx7ulceckx2a4g23tkhtgshtxq3fga7ptbhq5gcebwiq6cfolt4zbn72gbmtc43nw63vd4soxf4bnbhrykaoudfs3mh6laap6iwbngo4ylocs4w5hgd4t22yrtrmhkewsc2eytsosxyhaiuaww24mszscsojm2bcoldpokwuxbnfx7lgnzdcuae3y55zoen47noltjqgcpuqzl6upjcvutgvvro6nu2uyl36rcqmw2by2e45uqtsdnolbispxv2e5aeeuz5gytuf5f5e44nldmywtmxkfqfljml5gye6tj3qswmz6d36f2k4v7fbiuv7jzplzmghsgxvmq7fo3qp655obysbggkd3iqpk76p5umbpc2tk64oiklrponulkqf3v337aaxyn6nvzz2rpj3o374tftscsr7oilzkah63xpe2jc45dd4fuwxvlg3c33zgkminemqqfz7jdjtnawy77vpxxgnosbw4fwadhhggofmipboiqo55xygojdnfdkuzgfe4455sdqv5ytzdl55yuzlbdgsnwtgnfakmoyjhblzbuwohq7esayfxe72yqgci5dappiad7bc3ikfsydv5b7stifajkxuosu345upxg2hwzajj4uu7lxaykxgo22pslkxnidaoyevn3gamx63ec4fkhzguhbu6jt7pukr4rpafx24vd622f5wzux4corlxthjuhi2ewiu6laxx3aqfkzv2d2hhqzsac25vycmmxy
|
||||
niwl-client bob.profile generate "bob"
|
||||
Tagging Key: auaaaaaaaaaaaylmnfrwkgaaaaaaaaaaadlbii3y7r6vmc7upbxa4myohaqmr5xl22bdxeed4abkotnovlmakzdo5stq2ibtjewm4rnkgzqwglrt72zfeyomvdpxqnu4ci4hwebyiseyn7pqfxypnvef7a3flu2hby7gdluh6wocxa5mvmimi2xorydcqaca2p2aevmue4cwyxnw2h7fkps7e6grgls66zgohbnwjibt6nlsdqjbrdjrzlsc3at3f43jyniz2i67ng6xdty5pr3elzedhjlvefhd6pjfc7g4owrz3dkq5xt2hhh3vvctkywqkcwriguayyx3pourepfs7s76bekrjgcjgj6zyid3ixmeh5ewqhkhxhzevf3uogvscxtpbksaclhccht7pj2fungnztfghshd6lsmegmysiiuyav6schtmyxmne2vfi4j4cxllm2crj3cqofsxjlxov3ms2zgtjzyxtubwtnwspc4jhijz4kufm6r3qkhpcyibx7ulceckx2a4g23tkhtgshtxq3fga7ptbhq5gcebwiq6cfolt4zbn72gbmtc43nw63vd4soxf4bnbhrykaoudfs3mh6laap6iwbngo4ylocs4w5hgd4t22yrtrmhkewsc2eytsosxyhaiuaww24mszscsojm2bcoldpokwuxbnfx7lgnzdcuae3y55zoen47noltjqgcpuqzl6upjcvutgvvro6nu2uyl36rcqmw2by2e45uqtsdnolbispxv2e5aeeuz5gytuf5f5e44nldmywtmxkfqfljml5gye6tj3qswmz6d36f2k4v7fbiuv7jzplzmghsgxvmq7fo3qp655obysbggkd3iqpk76p5umbpc2tk64oiklrponulkqf3v337aaxyn6nvzz2rpj3o374tftscsr7oilzkah63xpe2jc45dd4fuwxvlg3c33zgkminemqqfz7jdjtnawy77vpxxgnosbw4fwadhhggofmipboiqo55xygojdnfdkuzgfe4455sdqv5ytzdl55yuzlbdgsnwtgnfakmoyjhblzbuwohq7esayfxe72yqgci5dappiad7bc3ikfsydv5b7stifajkxuosu345upxg2hwzajj4uu7lxaykxgo22pslkxnidaoyevn3gamx63ec4fkhzguhbu6jt7pukr4rpafx24vd622f5wzux4corlxthjuhi2ewiu6laxx3aqfkzv2d2hhqzsac25vycmmxy
|
||||
|
||||
niwl-client alice.profile import-tagging-key amaaaaaaaaaaaytpmimaaaaaaaaaaafaukgy7543bcnjyq4jbthaovnfxtdnya3jajmbwa4t5gmihqgudbta4nzigrhzirkekers23ng2lr4zbjspthybajjj7vbwn6wnied27e2jvuipqbinru2q7eumgbt62spztz3rpslymv4iwsujozb7ylcfr7ugroilpgxzrjniussojm4q3kun247o4kqjzcrec4ohcuiyaiinourb7h7j4qjv4ne46xhnptwsfjr5s7yz2igqsbpvrqeiy5u6khmxwpi2jzxrnk5qlixewjcbe3zzy4qpxnl7ybdds6tld522amonc2dxncff2ihribsdnd5fc5dozqu2eqqxqmyvnd5pdngozhqikdc6ovj4uzf2ttabckbr4sim6z3fkl7kd5wqjjdaosahqsi67gy47q3vd3ubtu5btx2lmgkmyzm2wuupvwxxvc65lcxghm43bu4yah76jb3u36kg4nzdemuxewxcswofymuvdxh24uqyyhn7ymlr6mnuuk6g6acy4bcu7gsiacu3am6qwfve7s5wckgbaqc6veafbzynjmv6wubkleas2ghkirnl3pdznf37pyz62hkjssiqzqlduhkcyghdkdzccrtnesdkob447zlxaj2qz24chuxpy7hkffx64fi7aqzkpujifagrkcxvroq43wl2hme7udqcwpdjdtqm7yhnnanazuahtqlvf3ux7kvmidevorrgaiephptm7qgk7ezw6aa7o3fjyra7m3xknbmpniqa4dnwg44cfgbj2ln6kcecgat4d5cokabzk64jjhfq4m6upoptya4bjy2chdhged4jsqvp646u4klk5zk2fmhgf7l365k2g4z4i2u2dveajldebgnwc4esdrmequawk5sk6oskurfsuj6i4v7jzeqcd5hbujllka4dq3cn3hidx67jspqefnwj6yr7fvwjvvtjo3ri5wcqqd3m5kjrmhjuxbho7sl5xnhzntdtha7ldwml6xkwg3x7scsmhikvkrzfenggxccunpyp6cmsslblfbdrwvl4qurnrdg7wqtlgy6pfhjrqwqej26e7ddxwax34fagkgqksupdutbaq2juvicn7rq3xrxs6bi6a2ozdfdyfbtun5ckz6ln2jxyu254appisier35e3d6nmicywn5p7m32cvcct3ilub4ovk4jpuizegyudyrcgoud2rmuyei6fdgrbvtdqv7gqy6a
|
||||
Got: bob: 4211f14667b425649390eee099c4e84bf758a1a4e6375e23b50c6de347c25654
|
||||
|
||||
niwl-client alice.profile tag-and-send bob
|
||||
Tag for bob 7e441275a5c3f88606c34c3451a44eaeaa025680cfcb3d9db53992501cc22134 4f7a7f961bc19297fee98da5f8601aa8373429b80b10c55dbe8116aa8c497a0e 71d8da
|
||||
|
||||
niwl-client bob.profile detect 10
|
||||
7e441275a5c3f88606c34c3451a44eaeaa025680cfcb3d9db53992501cc22134 4f7a7f961bc19297fee98da5f8601aa8373429b80b10c55dbe8116aa8c497a0e 71d8da
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "niwl-client"
|
||||
version = "0.1.0"
|
||||
authors = ["Sarah Jamie Lewis <sarah@openprivacy.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
niwl = {path="../niwl"}
|
||||
fuzzytags = {path="../../fuzzymetatag"}
|
||||
clap = "3.0.0-beta.2"
|
||||
serde = {version="1.0.123", features=["derive"]}
|
||||
serde_json = "1.0.61"
|
||||
bincode = "1.3.1"
|
||||
hex = "0.4.2"
|
||||
base32 = "0.4.0"
|
||||
reqwest = {version="0.11.0", features=["json"]}
|
||||
tokio = "1.2.0"
|
|
@ -0,0 +1,114 @@
|
|||
use clap::Clap;
|
||||
use std::fs;
|
||||
use niwl::{Profile};
|
||||
|
||||
#[derive(Clap)]
|
||||
#[clap(version = "1.0", author = "Sarah Jamie Lewis <sarah@openprivacy.ca>")]
|
||||
struct Opts {
|
||||
|
||||
#[clap(default_value = "niwl.profile")]
|
||||
profile_filename: String,
|
||||
|
||||
#[clap(default_value = "http://localhost:8000")]
|
||||
niwl_server: String,
|
||||
|
||||
#[clap(subcommand)]
|
||||
subcmd: SubCommand,
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
enum SubCommand {
|
||||
Generate(Generate),
|
||||
ImportTaggingKey(ImportTaggingKey),
|
||||
TagAndSend(TagAndSend),
|
||||
Detect(Detect)
|
||||
}
|
||||
|
||||
/// Generate a new niwl.profile file
|
||||
#[derive(Clap)]
|
||||
struct Generate {
|
||||
name: String
|
||||
}
|
||||
|
||||
/// Import a friends tagging key into this profile so you can send messages to them
|
||||
#[derive(Clap)]
|
||||
struct ImportTaggingKey {
|
||||
key: String
|
||||
}
|
||||
|
||||
/// Connect to a server and check for new notifications
|
||||
#[derive(Clap)]
|
||||
struct Detect {
|
||||
#[clap(default_value = "2")]
|
||||
length: u8
|
||||
}
|
||||
|
||||
/// Send a message to a friend tagged with their niwl key
|
||||
#[derive(Clap)]
|
||||
struct TagAndSend {
|
||||
/// the id of the friend e.g. "alice"
|
||||
id: String,
|
||||
/// the message you want to send.
|
||||
message: String,
|
||||
}
|
||||
|
||||
fn get_profile(profile_filename: &String) -> Profile {
|
||||
match fs::read_to_string(profile_filename) {
|
||||
Ok(json) => serde_json::from_str(json.as_str()).unwrap(),
|
||||
Err(why) => {
|
||||
panic!("couldn't read orb.profile : {}", why);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn main() {
|
||||
let opts: Opts = Opts::parse();
|
||||
match opts.subcmd {
|
||||
SubCommand::Generate(g) => {
|
||||
let profile = Profile::new(g.name.clone());
|
||||
let hotk = profile.human_readable_tagging_key();
|
||||
println!("Tagging Key: {}", base32::encode(base32::Alphabet::RFC4648{padding:false} ,bincode::serialize(&hotk).unwrap().as_slice()).to_ascii_lowercase());
|
||||
profile.save(&opts.profile_filename);
|
||||
}
|
||||
SubCommand::ImportTaggingKey(cmd) => {
|
||||
let mut profile = get_profile(&opts.profile_filename);
|
||||
profile.import_tagging_key(&cmd.key);
|
||||
profile.save(&opts.profile_filename);
|
||||
},
|
||||
SubCommand::TagAndSend(cmd) => {
|
||||
let mut profile = get_profile(&opts.profile_filename);
|
||||
let server = opts.niwl_server.clone();
|
||||
let contact = cmd.id.clone();
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let result = profile.tag_and_send(server, contact).await;
|
||||
println!("{}", result.unwrap().text().await.unwrap());
|
||||
});
|
||||
},
|
||||
SubCommand::Detect(cmd) => {
|
||||
let mut profile = get_profile(&opts.profile_filename);
|
||||
let server = opts.niwl_server.clone();
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
match profile.detect_tags(server).await {
|
||||
Ok(detected_tags) => {
|
||||
for tag in detected_tags.detected_tags {
|
||||
println!("{}", tag);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
println!("Error: {}", err)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "niwl-server"
|
||||
version = "0.1.0"
|
||||
authors = ["Sarah Jamie Lewis <sarah@openprivacy.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
fuzzytags = {path="../../fuzzymetatag"}
|
||||
rocket = "0.4.6"
|
||||
rocket_contrib = {version="0.4.6", features=["sqlite_pool"]}
|
||||
chrono = "0.4.19"
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
## Building
|
||||
|
||||
Requires `libsqlite3`
|
||||
|
||||
sudo apt install libsqlite3-dev
|
||||
|
||||
## Running
|
||||
|
||||
First setup the database:
|
||||
|
||||
cat sql/create.sql | sqlite3 tags.sqlite
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[global.databases]
|
||||
tags = { url = "./tags.sqlite", pool_size = 20 }
|
|
@ -0,0 +1,4 @@
|
|||
create table if not exists tags (
|
||||
id text primary key,
|
||||
tag blob not null unique
|
||||
)
|
|
@ -0,0 +1,55 @@
|
|||
#![feature(proc_macro_hygiene, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
#[macro_use] extern crate rocket_contrib;
|
||||
|
||||
use rocket_contrib::json;
|
||||
use fuzzytags::{DetectionKey, Tag};
|
||||
use rocket_contrib::json::{Json, JsonValue};
|
||||
use rocket_contrib::databases::rusqlite;
|
||||
use rocket_contrib::databases::rusqlite::types::ToSql;
|
||||
use chrono::{Utc, Duration};
|
||||
use std::ops::Sub;
|
||||
|
||||
#[database("tags")]
|
||||
struct TagsDbConn(rusqlite::Connection);
|
||||
|
||||
#[post("/new", format = "application/json", data = "<tag>")]
|
||||
fn new(conn:TagsDbConn, tag: Json<Tag<24>>) -> JsonValue {
|
||||
conn.0.execute(
|
||||
"INSERT INTO tags (id, tag) VALUES (strftime('%Y-%m-%d %H:%M:%S:%f', 'now'), ?1)",
|
||||
&[&tag.0.compress() as &dyn ToSql],
|
||||
).unwrap();
|
||||
json!({"tag" : tag.to_string()})
|
||||
}
|
||||
|
||||
#[post("/tags", format = "application/json", data = "<detection_key>")]
|
||||
fn tags(conn:TagsDbConn, detection_key: Json<DetectionKey<24>>) -> JsonValue {
|
||||
|
||||
let mut stmt = conn.0.prepare(
|
||||
"SELECT tag FROM tags WHERE id > (?1) AND id < (?2)",
|
||||
).unwrap();
|
||||
|
||||
let now = Utc::now();
|
||||
let after = now.sub(Duration::days(1)).format("%Y-%m-%d %H:%M:%S:%f").to_string();
|
||||
let before = now.format("%Y-%m-%d %H:%M:%S:%f").to_string();
|
||||
let selected_tags = stmt.query_map(&[&after, &before], |row| {
|
||||
let tag_bytes : Vec<u8> = row.get(0);
|
||||
let tag = Tag::<24>::decompress(tag_bytes.as_slice()).unwrap();
|
||||
tag
|
||||
}).unwrap();
|
||||
|
||||
let mut detected_tags : Vec<Tag<24>> = vec![];
|
||||
for tag in selected_tags {
|
||||
let tag : Tag<24> = tag.unwrap();
|
||||
if detection_key.0.test_tag(&tag) {
|
||||
detected_tags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
json!({"detected_tags" : detected_tags})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
rocket::ignite().attach(TagsDbConn::fairing()).mount("/", routes![tags, new]).launch();
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "niwl"
|
||||
version = "0.1.0"
|
||||
authors = ["Sarah Jamie Lewis <sarah@openprivacy.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
fuzzytags = {path="../../fuzzymetatag"}
|
||||
serde = {version="1.0.123", features=["derive"]}
|
||||
serde_json = "1.0.61"
|
||||
bincode = "1.3.1"
|
||||
hex = "0.4.2"
|
||||
base32 = "0.4.0"
|
||||
reqwest = {version="0.11.0", features=["json"]}
|
|
@ -0,0 +1,121 @@
|
|||
#![feature(into_future)]
|
||||
use fuzzytags::{RootSecret, TaggingKey, Tag};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use reqwest::{Response, Error};
|
||||
use std::future::{Future, IntoFuture};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NiwlError {
|
||||
NoKnownContactError(String),
|
||||
RemoteServerError(String)
|
||||
}
|
||||
|
||||
#[derive(Serialize,Deserialize)]
|
||||
pub struct Profile {
|
||||
profile_name: String,
|
||||
root_secret: RootSecret<24>,
|
||||
tagging_keys: HashMap<String, TaggingKey<24>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize,Deserialize)]
|
||||
pub struct HumanOrientedTaggingKey {
|
||||
profile_name: String,
|
||||
tagging_key: TaggingKey<24>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DetectedTags {
|
||||
pub detected_tags: Vec<Tag<24>>,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn new(profile_name: String) -> Profile {
|
||||
let root_secret = RootSecret::<24>::generate();
|
||||
Profile {
|
||||
profile_name,
|
||||
root_secret,
|
||||
tagging_keys: Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn human_readable_tagging_key(&self) -> HumanOrientedTaggingKey {
|
||||
let tagging_key = self.root_secret.tagging_key();
|
||||
HumanOrientedTaggingKey {
|
||||
profile_name: self.profile_name.clone(),
|
||||
tagging_key
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self, profile_filename: &String) -> std::io::Result<()> {
|
||||
let j = serde_json::to_string(&self);
|
||||
let mut file = match File::create(profile_filename) {
|
||||
Err(why) => panic!("couldn't create : {}", why),
|
||||
Ok(file) => file,
|
||||
};
|
||||
file.write_all(j.unwrap().as_bytes())
|
||||
}
|
||||
|
||||
pub fn generate_tag(&self, id: &String) -> Result<Tag<24>, NiwlError> {
|
||||
if self.tagging_keys.contains_key(id) {
|
||||
let tag = self.tagging_keys[id].generate_tag();
|
||||
println!("Tag for {} {}", id, tag.to_string());
|
||||
return Ok(tag)
|
||||
}
|
||||
Err(NiwlError::NoKnownContactError(format!("No known friend {}. Perhaps you need to import-tagging-key first?", id)))
|
||||
}
|
||||
|
||||
pub fn import_tagging_key(&mut self, key: &String) {
|
||||
match base32::decode(base32::Alphabet::RFC4648 { padding: false }, key.as_str()) {
|
||||
Some(data) => {
|
||||
let tagging_key_result: Result<HumanOrientedTaggingKey, bincode::Error> = bincode::deserialize(&data);
|
||||
match tagging_key_result {
|
||||
Ok(hotk) => {
|
||||
println!("Got: {}: {}", hotk.profile_name, hotk.tagging_key.id());
|
||||
if self.tagging_keys.contains_key(&hotk.profile_name) == false {
|
||||
self.tagging_keys.insert(hotk.profile_name, hotk.tagging_key);
|
||||
} else {
|
||||
println!("There is already an entry for {}", hotk.profile_name)
|
||||
}
|
||||
return
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.to_string());
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
println!("Error Reporting Tagging Key")
|
||||
}
|
||||
|
||||
pub async fn tag_and_send(&self, server: String, contact: String) -> Result<Response, NiwlError> {
|
||||
let client = reqwest::Client::new();
|
||||
match self.generate_tag(&contact) {
|
||||
Ok(tag) => {
|
||||
let result = client.
|
||||
post(&String::from(server + "/new"))
|
||||
.json(&tag)
|
||||
.send().await;
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => Err(NiwlError::RemoteServerError(err.to_string()))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn detect_tags(&mut self, server: String) -> Result<DetectedTags, Error> {
|
||||
let client = reqwest::Client::new();
|
||||
let detection_key = self.root_secret.extract_detection_key(1);
|
||||
let result = client.post(&String::from(server + "/tags"))
|
||||
.json(&detection_key)
|
||||
.send().await;
|
||||
result.unwrap().json().await
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue