Add first horrible implementation of hydrus dictionaries

Signed-off-by: trivernis <trivernis@protonmail.com>
main
trivernis 2 years ago
commit a6c8ad8795
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

3
.gitignore vendored

@ -0,0 +1,3 @@
/target
Cargo.lock
.env

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="CPP_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/hydrus-ptr-client.iml" filepath="$PROJECT_DIR$/.idea/hydrus-ptr-client.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,29 @@
[package]
name = "hydrus-ptr-client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tracing = "0.1.31"
thiserror = "1.0.30"
serde_json = "1.0.79"
flate2 = "1.0.22"
reqwest = "0.11.9"
[dependencies.serde]
version = "1.0.136"
features = ["derive"]
[dev-dependencies]
tracing-subscriber = "0.3.9"
dotenv = "0.15.0"
lazy_static = "1.4.0"
[dev-dependencies.tokio]
version = "1.17.0"
features = ["rt-multi-thread", "macros"]
[features]
rustls = ["reqwest/rustls"]

@ -0,0 +1,89 @@
pub use crate::endpoints::*;
use crate::{ClientBuilder, Error, Result};
use flate2::write::ZlibDecoder;
use reqwest::Response;
use serde::Serialize;
use std::fmt::Debug;
use std::io::Write;
pub struct Client {
pub(crate) client: reqwest::Client,
pub(crate) base_url: String,
pub(crate) access_key: String,
}
impl Client {
/// Creates a new client builder
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
/// Creates a new PTR Client
pub fn new<S1: ToString, S2: ToString>(endpoint: S1, access_key: S2) -> Self {
Self {
base_url: endpoint.to_string(),
client: reqwest::Client::new(),
access_key: access_key.to_string(),
}
}
#[tracing::instrument(skip(self), level = "debug")]
pub async fn options(&self) -> Result<OptionsResponse> {
self.get::<Options, ()>(&()).await
}
/// Performs a get request to the given Get Endpoint
#[tracing::instrument(skip(self), level = "trace")]
async fn get<E: GetEndpoint, Q: Serialize + Debug>(&self, query: &Q) -> Result<E::Response> {
tracing::trace!("GET request to {}", E::path());
let response = self
.client
.get(format!("{}/{}", self.base_url, E::path()))
.query(query)
.header("Hydrus-Key", self.access_key.to_string())
.send()
.await?;
let body = Self::get_body(response).await?;
let bytes = Self::decompress_body(body)?;
let response_type = Self::deserialize_body(bytes)?;
tracing::trace!("response is: {:?}", response_type);
Ok(response_type)
}
/// Returns the body from the response
#[tracing::instrument(level = "trace")]
async fn get_body(response: Response) -> Result<Vec<u8>> {
if response.status().is_success() {
Ok(response.bytes().await?.to_vec())
} else {
let message = response.text().await?;
Err(Error::Response(message))
}
}
/// Uses zlib to decompress the body
#[tracing::instrument(skip(bytes), level = "trace")]
fn decompress_body(mut bytes: Vec<u8>) -> Result<Vec<u8>> {
tracing::trace!("body length {}", bytes.len());
let mut buf = Vec::new();
let mut decoder = ZlibDecoder::new(buf);
decoder.write_all(&mut bytes)?;
buf = decoder.finish()?;
tracing::trace!("result length {}", buf.len());
Ok(buf)
}
/// Deserializes the body to the given type
#[tracing::instrument(skip(bytes), level = "trace")]
fn deserialize_body<T: FromJson>(bytes: Vec<u8>) -> Result<T> {
let json_value: serde_json::Value = serde_json::from_reader(&bytes[..])?;
tracing::trace!("json value = {}", json_value.to_string());
T::from_json(json_value)
}
}

@ -0,0 +1,67 @@
use crate::Client;
use crate::{Error, Result};
use std::time::Duration;
pub struct ClientBuilder {
reqwest_builder: reqwest::ClientBuilder,
endpoint: String,
access_key: Option<String>,
}
impl Default for ClientBuilder {
fn default() -> Self {
Self {
reqwest_builder: reqwest::ClientBuilder::new(),
endpoint: String::from("https://ptr.hydrus.network:45871"),
access_key: None,
}
}
}
impl ClientBuilder {
/// Doesn't validate ssl certificates of the endpoint.
///
/// # Warning
/// Turning this on allows invalid and expired certificates which is a security risk.
pub fn accept_invalid_certs(mut self, accept: bool) -> Self {
self.reqwest_builder = self.reqwest_builder.danger_accept_invalid_certs(accept);
self
}
/// Sets the request timeout
pub fn timeout(mut self, timeout: Duration) -> Self {
self.reqwest_builder = self.reqwest_builder.timeout(timeout);
self
}
/// Sets the endpoint of the client.
/// The default endpoint is `https://ptr.hydrus.network:45871`
pub fn endpoint<S: ToString>(mut self, endpoint: S) -> Self {
self.endpoint = endpoint.to_string();
self
}
/// Sets the access key. This key is required for requests
/// to the PTR.
pub fn access_key<S: ToString>(mut self, access_key: S) -> Self {
self.access_key = Some(access_key.to_string());
self
}
/// Validates the configuration and builds the client
pub fn build(self) -> Result<Client> {
let access_key = self
.access_key
.ok_or_else(|| Error::Builder(String::from("missing access key")))?;
Ok(Client {
client: self.reqwest_builder.build()?,
base_url: self.endpoint,
access_key,
})
}
}

@ -0,0 +1,111 @@
#![allow(unused)]
pub const HYDRUS_TYPE_BASE: u64 = 0;
pub const HYDRUS_TYPE_BASE_NAMED: u64 = 1;
pub const HYDRUS_TYPE_SHORTCUT_SET: u64 = 2;
pub const HYDRUS_TYPE_SUBSCRIPTION_LEGACY: u64 = 3;
pub const HYDRUS_TYPE_PERIODIC: u64 = 4;
pub const HYDRUS_TYPE_GALLERY_IDENTIFIER: u64 = 5;
pub const HYDRUS_TYPE_TAG_IMPORT_OPTIONS: u64 = 6;
pub const HYDRUS_TYPE_FILE_IMPORT_OPTIONS: u64 = 7;
pub const HYDRUS_TYPE_FILE_SEED_CACHE: u64 = 8;
pub const HYDRUS_TYPE_HDD_IMPORT: u64 = 9;
pub const HYDRUS_TYPE_SERVER_TO_CLIENT_CONTENT_UPDATE_PACKAGE: u64 = 10;
pub const HYDRUS_TYPE_SERVER_TO_CLIENT_SERVICE_UPDATE_PACKAGE: u64 = 11;
pub const HYDRUS_TYPE_MANAGEMENT_CONTROLLER: u64 = 12;
pub const HYDRUS_TYPE_GUI_SESSION_LEGACY: u64 = 13;
pub const HYDRUS_TYPE_PREDICATE: u64 = 14;
pub const HYDRUS_TYPE_FILE_SEARCH_CONTEXT: u64 = 15;
pub const HYDRUS_TYPE_EXPORT_FOLDER: u64 = 16;
pub const HYDRUS_TYPE_WATCHER_IMPORT: u64 = 17;
pub const HYDRUS_TYPE_SIMPLE_DOWNLOADER_IMPORT: u64 = 18;
pub const HYDRUS_TYPE_IMPORT_FOLDER: u64 = 19;
pub const HYDRUS_TYPE_MULTIPLE_GALLERY_IMPORT: u64 = 20;
pub const HYDRUS_TYPE_DICTIONARY: u64 = 21;
pub const HYDRUS_TYPE_CLIENT_OPTIONS: u64 = 22;
pub const HYDRUS_TYPE_CONTENT: u64 = 23;
pub const HYDRUS_TYPE_PETITION: u64 = 24;
pub const HYDRUS_TYPE_ACCOUNT_IDENTIFIER: u64 = 25;
pub const HYDRUS_TYPE_LIST: u64 = 26;
pub const HYDRUS_TYPE_PARSE_FORMULA_HTML: u64 = 27;
pub const HYDRUS_TYPE_URLS_IMPORT: u64 = 28;
pub const HYDRUS_TYPE_PARSE_NODE_CONTENT_LINK: u64 = 29;
pub const HYDRUS_TYPE_CONTENT_PARSER: u64 = 30;
pub const HYDRUS_TYPE_PARSE_FORMULA_JSON: u64 = 31;
pub const HYDRUS_TYPE_PARSE_ROOT_FILE_LOOKUP: u64 = 32;
pub const HYDRUS_TYPE_BYTES_DICT: u64 = 33;
pub const HYDRUS_TYPE_CONTENT_UPDATE: u64 = 34;
pub const HYDRUS_TYPE_CREDENTIALS: u64 = 35;
pub const HYDRUS_TYPE_DEFINITIONS_UPDATE: u64 = 36;
pub const HYDRUS_TYPE_METADATA: u64 = 37;
pub const HYDRUS_TYPE_BANDWIDTH_RULES: u64 = 38;
pub const HYDRUS_TYPE_BANDWIDTH_TRACKER: u64 = 39;
pub const HYDRUS_TYPE_CLIENT_TO_SERVER_UPDATE: u64 = 40;
pub const HYDRUS_TYPE_SHORTCUT: u64 = 41;
pub const HYDRUS_TYPE_APPLICATION_COMMAND: u64 = 42;
pub const HYDRUS_TYPE_DUPLICATE_ACTION_OPTIONS: u64 = 43;
pub const HYDRUS_TYPE_TAG_FILTER: u64 = 44;
pub const HYDRUS_TYPE_NETWORK_BANDWIDTH_MANAGER_LEGACY: u64 = 45;
pub const HYDRUS_TYPE_NETWORK_SESSION_MANAGER_LEGACY: u64 = 46;
pub const HYDRUS_TYPE_NETWORK_CONTEXT: u64 = 47;
pub const HYDRUS_TYPE_NETWORK_LOGIN_MANAGER: u64 = 48;
pub const HYDRUS_TYPE_MEDIA_SORT: u64 = 49;
pub const HYDRUS_TYPE_URL_CLASS: u64 = 50;
pub const HYDRUS_TYPE_STRING_MATCH: u64 = 51;
pub const HYDRUS_TYPE_CHECKER_OPTIONS: u64 = 52;
pub const HYDRUS_TYPE_NETWORK_DOMAIN_MANAGER: u64 = 53;
pub const HYDRUS_TYPE_SUBSCRIPTION_QUERY_LEGACY: u64 = 54;
pub const HYDRUS_TYPE_STRING_CONVERTER: u64 = 55;
pub const HYDRUS_TYPE_FILENAME_TAGGING_OPTIONS: u64 = 56;
pub const HYDRUS_TYPE_FILE_SEED: u64 = 57;
pub const HYDRUS_TYPE_PAGE_PARSER: u64 = 58;
pub const HYDRUS_TYPE_PARSE_FORMULA_COMPOUND: u64 = 59;
pub const HYDRUS_TYPE_PARSE_FORMULA_CONTEXT_VARIABLE: u64 = 60;
pub const HYDRUS_TYPE_TAG_SUMMARY_GENERATOR: u64 = 61;
pub const HYDRUS_TYPE_PARSE_RULE_HTML: u64 = 62;
pub const HYDRUS_TYPE_SIMPLE_DOWNLOADER_PARSE_FORMULA: u64 = 63;
pub const HYDRUS_TYPE_MULTIPLE_WATCHER_IMPORT: u64 = 64;
pub const HYDRUS_TYPE_SERVICE_TAG_IMPORT_OPTIONS: u64 = 65;
pub const HYDRUS_TYPE_GALLERY_SEED: u64 = 66;
pub const HYDRUS_TYPE_GALLERY_SEED_LOG: u64 = 67;
pub const HYDRUS_TYPE_GALLERY_IMPORT: u64 = 68;
pub const HYDRUS_TYPE_GALLERY_URL_GENERATOR: u64 = 69;
pub const HYDRUS_TYPE_NESTED_GALLERY_URL_GENERATOR: u64 = 70;
pub const HYDRUS_TYPE_DOMAIN_METADATA_PACKAGE: u64 = 71;
pub const HYDRUS_TYPE_LOGIN_CREDENTIAL_DEFINITION: u64 = 72;
pub const HYDRUS_TYPE_LOGIN_SCRIPT_DOMAIN: u64 = 73;
pub const HYDRUS_TYPE_LOGIN_STEP: u64 = 74;
pub const HYDRUS_TYPE_CLIENT_API_MANAGER: u64 = 75;
pub const HYDRUS_TYPE_CLIENT_API_PERMISSIONS: u64 = 76;
pub const HYDRUS_TYPE_SERVICE_KEYS_TO_TAGS: u64 = 77;
pub const HYDRUS_TYPE_MEDIA_COLLECT: u64 = 78;
pub const HYDRUS_TYPE_TAG_DISPLAY_MANAGER: u64 = 79;
pub const HYDRUS_TYPE_TAG_SEARCH_CONTEXT: u64 = 80;
pub const HYDRUS_TYPE_FAVOURITE_SEARCH_MANAGER: u64 = 81;
pub const HYDRUS_TYPE_NOTE_IMPORT_OPTIONS: u64 = 82;
pub const HYDRUS_TYPE_STRING_SPLITTER: u64 = 83;
pub const HYDRUS_TYPE_STRING_PROCESSOR: u64 = 84;
pub const HYDRUS_TYPE_TAG_AUTOCOMPLETE_OPTIONS: u64 = 85;
pub const HYDRUS_TYPE_SUBSCRIPTION_QUERY_LOG_CONTAINER: u64 = 86;
pub const HYDRUS_TYPE_SUBSCRIPTION_QUERY_HEADER: u64 = 87;
pub const HYDRUS_TYPE_SUBSCRIPTION: u64 = 88;
pub const HYDRUS_TYPE_FILE_SEED_CACHE_STATUS: u64 = 89;
pub const HYDRUS_TYPE_SUBSCRIPTION_CONTAINER: u64 = 90;
pub const HYDRUS_TYPE_COLUMN_LIST_STATUS: u64 = 91;
pub const HYDRUS_TYPE_COLUMN_LIST_MANAGER: u64 = 92;
pub const HYDRUS_TYPE_NUMBER_TEST: u64 = 93;
pub const HYDRUS_TYPE_NETWORK_BANDWIDTH_MANAGER: u64 = 94;
pub const HYDRUS_TYPE_NETWORK_SESSION_MANAGER: u64 = 95;
pub const HYDRUS_TYPE_NETWORK_SESSION_MANAGER_SESSION_CONTAINER: u64 = 96;
pub const HYDRUS_TYPE_NETWORK_BANDWIDTH_MANAGER_TRACKER_CONTAINER: u64 = 97;
pub const HYDRUS_TYPE_SIDECAR_EXPORTER: u64 = 98;
pub const HYDRUS_TYPE_STRING_SORTER: u64 = 99;
pub const HYDRUS_TYPE_STRING_SLICER: u64 = 100;
pub const HYDRUS_TYPE_TAG_SORT: u64 = 101;
pub const HYDRUS_TYPE_ACCOUNT_TYPE: u64 = 102;
pub const HYDRUS_TYPE_LOCATION_SEARCH_CONTEXT: u64 = 103;
pub const HYDRUS_TYPE_GUI_SESSION_CONTAINER: u64 = 104;
pub const HYDRUS_TYPE_GUI_SESSION_PAGE_DATA: u64 = 105;
pub const HYDRUS_TYPE_GUI_SESSION_CONTAINER_PAGE_NOTEBOOK: u64 = 106;
pub const HYDRUS_TYPE_GUI_SESSION_CONTAINER_PAGE_SINGLE: u64 = 107;
pub const HYDRUS_TYPE_PRESENTATION_IMPORT_OPTIONS: u64 = 108;

@ -0,0 +1,32 @@
mod options;
use crate::Result;
use std::fmt::Debug;
pub use options::*;
#[macro_export]
macro_rules! fix {
($opt:expr) => {
$opt.ok_or_else(|| crate::Error::Malformed)?
};
}
pub trait Endpoint {
fn path() -> &'static str;
}
pub trait GetEndpoint: Endpoint {
type Response: FromJson + Debug;
}
pub trait PostEndpoint: Endpoint {
type Request;
type Response: FromJson + Debug;
}
pub trait FromJson {
fn from_json(value: serde_json::Value) -> Result<Self>
where
Self: Sized;
}

@ -0,0 +1,49 @@
use crate::hydrus_serializable::dictionary::HydrusDictionary;
use crate::hydrus_serializable::wrapper::HydrusSerWrapper;
use crate::Result;
use crate::{fix, Endpoint, FromJson, GetEndpoint};
use serde_json::Value;
pub struct Options;
impl Endpoint for Options {
fn path() -> &'static str {
"options"
}
}
impl GetEndpoint for Options {
type Response = OptionsResponse;
}
#[derive(Clone, Debug)]
pub struct OptionsResponse {
server_message: String,
update_period: u64,
nullification_period: u64,
tag_filter: Value,
}
impl FromJson for OptionsResponse {
fn from_json(value: serde_json::Value) -> Result<Self> {
let response = serde_json::from_value::<HydrusSerWrapper<HydrusDictionary>>(value)?;
let options_value = fix!(response.inner.get_one(&"service_options".into()));
let options_value =
serde_json::from_value::<HydrusSerWrapper<HydrusDictionary>>(options_value.clone())?
.inner;
let server_message =
fix!(fix!(options_value.get_one(&"server_message".into())).as_str()).to_string();
let update_period = fix!(fix!(options_value.get_one(&"update_period".into())).as_u64());
let nullification_period =
fix!(fix!(options_value.get_one(&"nullification_period".into())).as_u64());
let tag_filter = fix!(options_value.get_one(&"tag_filter".into())).clone();
Ok(Self {
server_message,
update_period,
nullification_period,
tag_filter,
})
}
}

@ -0,0 +1,24 @@
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("reqwest error {0}")]
Reqwest(#[from] reqwest::Error),
#[error("api returned error response: {0}")]
Response(String),
#[error("failed to parse content as json: {0}")]
JSON(#[from] serde_json::Error),
#[error("io error {0}")]
Io(#[from] std::io::Error),
#[error("builder error: {0}")]
Builder(String),
#[error("malformed response")]
Malformed,
}

@ -0,0 +1,37 @@
use crate::constants::HYDRUS_TYPE_DICTIONARY;
use crate::hydrus_serializable::HydrusSerializable;
use serde::Deserialize;
use serde_json::Value;
#[derive(Clone, Debug, Deserialize)]
pub struct HydrusDictionary {
list_sim_sim: Vec<(Value, Value)>,
list_sim_ser: Vec<(Value, Value)>,
list_ser_sim: Vec<(Value, Value)>,
list_ser_ser: Vec<(Value, Value)>,
}
impl HydrusSerializable for HydrusDictionary {
fn type_id() -> u64 {
HYDRUS_TYPE_DICTIONARY
}
}
impl HydrusDictionary {
/// Returns the first value for a given key
pub fn get_one(&self, key: &Value) -> Option<&Value> {
self.get(key).into_iter().next()
}
/// Returns all values for a given key
pub fn get(&self, key: &Value) -> Vec<&Value> {
self.list_sim_sim
.iter()
.chain(self.list_sim_ser.iter())
.chain(self.list_ser_sim.iter())
.chain(self.list_ser_ser.iter())
.filter(|(k, _)| k == key)
.map(|(_, v)| v)
.collect()
}
}

@ -0,0 +1,71 @@
use crate::hydrus_serializable::dictionary::HydrusDictionary;
use serde::de::{DeserializeOwned, EnumAccess, Error, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::cmp::Ordering;
use std::fmt::Formatter;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
pub mod dictionary;
pub mod wrapper;
pub trait HydrusSerializable: DeserializeOwned {
fn type_id() -> u64;
}
#[derive(Clone, Debug)]
pub struct SerializableId<T: HydrusSerializable>(u64, PhantomData<T>);
impl<'de, T: HydrusSerializable> Deserialize<'de> for SerializableId<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_u64(SerIdVisitor(PhantomData))
}
}
struct SerIdVisitor<T>(PhantomData<T>);
impl<'de, T: HydrusSerializable> Visitor<'de> for SerIdVisitor<T> {
type Value = SerializableId<T>;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "an unsigned integer equal to {}", T::type_id())
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: Error,
{
self.visit_u64(v as u64)
}
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
where
E: Error,
{
self.visit_u64(v as u64)
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: Error,
{
self.visit_u64(v as u64)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
{
let expected_value = T::type_id();
if v != expected_value {
Err(E::custom(format!("type not equal to {}", expected_value)))
} else {
Ok(SerializableId(expected_value, PhantomData))
}
}
}

@ -0,0 +1,10 @@
use crate::hydrus_serializable::{HydrusSerializable, SerializableId};
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
#[serde(bound = "")]
pub struct HydrusSerWrapper<T: HydrusSerializable> {
pub type_id: SerializableId<T>,
pub version: u8,
pub inner: T,
}

@ -0,0 +1,10 @@
mod client;
mod client_builder;
pub(crate) mod constants;
mod endpoints;
mod error;
pub mod hydrus_serializable;
pub use client::*;
pub use client_builder::*;
pub use error::*;

@ -0,0 +1,25 @@
use hydrus_ptr_client::Client;
use std::env;
use std::sync::{Arc, Mutex, MutexGuard};
fn setup() {
lazy_static::lazy_static! { static ref SETUP_DONE: Arc<Mutex<bool>> = Arc::new(Mutex::new(false)); }
let mut setup_done: MutexGuard<bool> = SETUP_DONE.lock().unwrap();
if !*setup_done {
dotenv::dotenv().expect("failed to initialize dotenv");
tracing_subscriber::fmt::init();
*setup_done = true;
}
}
pub fn get_client() -> Client {
setup();
Client::builder()
.endpoint(env::var("PTR_URL").unwrap())
.access_key(env::var("PTR_ACCESS_KEY").unwrap())
.accept_invalid_certs(true)
.build()
.unwrap()
}

@ -0,0 +1,7 @@
mod common;
#[tokio::test]
async fn test_options() {
let client = common::get_client();
client.options().await.unwrap();
}
Loading…
Cancel
Save