2021-04-21 11:13:51 -06:00
|
|
|
use std::collections::HashMap;
|
2023-06-12 10:58:46 -06:00
|
|
|
use std::env;
|
2023-09-18 01:39:19 -06:00
|
|
|
use std::time::Duration;
|
2021-04-21 11:13:51 -06:00
|
|
|
|
2022-04-21 01:05:57 -06:00
|
|
|
use eyre::{bail, Result};
|
2022-04-28 11:53:59 -06:00
|
|
|
use reqwest::{
|
|
|
|
header::{HeaderMap, AUTHORIZATION, USER_AGENT},
|
|
|
|
StatusCode, Url,
|
|
|
|
};
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2023-07-14 13:44:08 -06:00
|
|
|
use atuin_common::record::{EncryptedData, HostId, Record, RecordId};
|
|
|
|
use atuin_common::{
|
|
|
|
api::{
|
|
|
|
AddHistoryRequest, CountResponse, DeleteHistoryRequest, ErrorResponse, IndexResponse,
|
|
|
|
LoginRequest, LoginResponse, RegisterResponse, StatusResponse, SyncHistoryResponse,
|
|
|
|
},
|
|
|
|
record::RecordIndex,
|
2021-04-21 11:13:51 -06:00
|
|
|
};
|
2022-10-14 03:59:21 -06:00
|
|
|
use semver::Version;
|
2023-09-11 02:26:05 -06:00
|
|
|
use time::format_description::well_known::Rfc3339;
|
|
|
|
use time::OffsetDateTime;
|
2021-04-20 14:53:07 -06:00
|
|
|
|
2023-06-21 01:45:23 -06:00
|
|
|
use crate::{history::History, sync::hash_str};
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-05-09 14:17:24 -06:00
|
|
|
static APP_USER_AGENT: &str = concat!("atuin/", env!("CARGO_PKG_VERSION"),);
|
2021-04-21 11:13:51 -06:00
|
|
|
|
2021-04-13 12:14:07 -06:00
|
|
|
pub struct Client<'a> {
|
2021-04-20 10:07:11 -06:00
|
|
|
sync_addr: &'a str,
|
|
|
|
client: reqwest::Client,
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|
|
|
|
|
2021-12-08 06:37:49 -07:00
|
|
|
pub async fn register(
|
2021-04-21 11:13:51 -06:00
|
|
|
address: &str,
|
|
|
|
username: &str,
|
|
|
|
email: &str,
|
|
|
|
password: &str,
|
2022-04-12 16:06:19 -06:00
|
|
|
) -> Result<RegisterResponse> {
|
2021-04-21 11:13:51 -06:00
|
|
|
let mut map = HashMap::new();
|
|
|
|
map.insert("username", username);
|
|
|
|
map.insert("email", email);
|
|
|
|
map.insert("password", password);
|
|
|
|
|
2023-02-06 04:59:01 -07:00
|
|
|
let url = format!("{address}/user/{username}");
|
2022-04-22 14:14:23 -06:00
|
|
|
let resp = reqwest::get(url).await?;
|
2021-04-21 11:13:51 -06:00
|
|
|
|
|
|
|
if resp.status().is_success() {
|
2022-04-21 01:05:57 -06:00
|
|
|
bail!("username already in use");
|
2021-04-21 11:13:51 -06:00
|
|
|
}
|
|
|
|
|
2023-02-06 04:59:01 -07:00
|
|
|
let url = format!("{address}/register");
|
2021-12-08 06:37:49 -07:00
|
|
|
let client = reqwest::Client::new();
|
2021-04-21 11:13:51 -06:00
|
|
|
let resp = client
|
|
|
|
.post(url)
|
2021-05-09 14:17:24 -06:00
|
|
|
.header(USER_AGENT, APP_USER_AGENT)
|
2021-04-21 11:13:51 -06:00
|
|
|
.json(&map)
|
2021-12-08 06:37:49 -07:00
|
|
|
.send()
|
|
|
|
.await?;
|
2021-04-21 11:13:51 -06:00
|
|
|
|
|
|
|
if !resp.status().is_success() {
|
2022-10-07 21:33:07 -06:00
|
|
|
let error = resp.json::<ErrorResponse>().await?;
|
|
|
|
bail!("failed to register user: {}", error.reason);
|
2021-04-21 11:13:51 -06:00
|
|
|
}
|
|
|
|
|
2021-12-08 06:37:49 -07:00
|
|
|
let session = resp.json::<RegisterResponse>().await?;
|
2021-04-21 11:13:51 -06:00
|
|
|
Ok(session)
|
|
|
|
}
|
|
|
|
|
2022-04-12 16:06:19 -06:00
|
|
|
pub async fn login(address: &str, req: LoginRequest) -> Result<LoginResponse> {
|
2023-02-06 04:59:01 -07:00
|
|
|
let url = format!("{address}/login");
|
2021-12-08 06:37:49 -07:00
|
|
|
let client = reqwest::Client::new();
|
2021-04-21 11:13:51 -06:00
|
|
|
|
|
|
|
let resp = client
|
|
|
|
.post(url)
|
2021-05-09 14:17:24 -06:00
|
|
|
.header(USER_AGENT, APP_USER_AGENT)
|
|
|
|
.json(&req)
|
2021-12-08 06:37:49 -07:00
|
|
|
.send()
|
|
|
|
.await?;
|
2021-04-21 11:13:51 -06:00
|
|
|
|
|
|
|
if resp.status() != reqwest::StatusCode::OK {
|
2022-10-07 21:33:07 -06:00
|
|
|
let error = resp.json::<ErrorResponse>().await?;
|
|
|
|
bail!("invalid login details: {}", error.reason);
|
2021-04-21 11:13:51 -06:00
|
|
|
}
|
|
|
|
|
2021-12-08 06:37:49 -07:00
|
|
|
let session = resp.json::<LoginResponse>().await?;
|
2021-04-21 11:13:51 -06:00
|
|
|
Ok(session)
|
|
|
|
}
|
|
|
|
|
2022-10-14 03:59:21 -06:00
|
|
|
pub async fn latest_version() -> Result<Version> {
|
|
|
|
let url = "https://api.atuin.sh";
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
|
|
|
let resp = client
|
|
|
|
.get(url)
|
|
|
|
.header(USER_AGENT, APP_USER_AGENT)
|
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
if resp.status() != reqwest::StatusCode::OK {
|
|
|
|
let error = resp.json::<ErrorResponse>().await?;
|
|
|
|
bail!("failed to check latest version: {}", error.reason);
|
|
|
|
}
|
|
|
|
|
|
|
|
let index = resp.json::<IndexResponse>().await?;
|
|
|
|
let version = Version::parse(index.version.as_str())?;
|
|
|
|
|
|
|
|
Ok(version)
|
|
|
|
}
|
|
|
|
|
2021-04-13 12:14:07 -06:00
|
|
|
impl<'a> Client<'a> {
|
2023-09-18 01:39:19 -06:00
|
|
|
pub fn new(
|
|
|
|
sync_addr: &'a str,
|
2023-09-28 19:56:40 -06:00
|
|
|
session_token: &str,
|
2023-09-18 01:39:19 -06:00
|
|
|
connect_timeout: u64,
|
|
|
|
timeout: u64,
|
|
|
|
) -> Result<Self> {
|
2021-05-09 14:17:24 -06:00
|
|
|
let mut headers = HeaderMap::new();
|
2023-02-06 04:59:01 -07:00
|
|
|
headers.insert(AUTHORIZATION, format!("Token {session_token}").parse()?);
|
2021-05-09 14:17:24 -06:00
|
|
|
|
2021-04-21 11:13:51 -06:00
|
|
|
Ok(Client {
|
2021-04-20 10:07:11 -06:00
|
|
|
sync_addr,
|
2021-05-09 14:17:24 -06:00
|
|
|
client: reqwest::Client::builder()
|
|
|
|
.user_agent(APP_USER_AGENT)
|
|
|
|
.default_headers(headers)
|
2023-09-18 01:39:19 -06:00
|
|
|
.connect_timeout(Duration::new(connect_timeout, 0))
|
|
|
|
.timeout(Duration::new(timeout, 0))
|
2021-05-09 14:17:24 -06:00
|
|
|
.build()?,
|
2021-04-21 11:13:51 -06:00
|
|
|
})
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
pub async fn count(&self) -> Result<i64> {
|
|
|
|
let url = format!("{}/sync/count", self.sync_addr);
|
|
|
|
let url = Url::parse(url.as_str())?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-05-09 14:17:24 -06:00
|
|
|
let resp = self.client.get(url).send().await?;
|
2021-04-21 11:13:51 -06:00
|
|
|
|
|
|
|
if resp.status() != StatusCode::OK {
|
2022-04-21 01:05:57 -06:00
|
|
|
bail!("failed to get count (are you logged in?)");
|
2021-04-21 11:13:51 -06:00
|
|
|
}
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
let count = resp.json::<CountResponse>().await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
|
|
|
Ok(count.count)
|
|
|
|
}
|
|
|
|
|
2023-03-20 03:26:54 -06:00
|
|
|
pub async fn status(&self) -> Result<StatusResponse> {
|
|
|
|
let url = format!("{}/sync/status", self.sync_addr);
|
|
|
|
let url = Url::parse(url.as_str())?;
|
|
|
|
|
|
|
|
let resp = self.client.get(url).send().await?;
|
|
|
|
|
|
|
|
if resp.status() != StatusCode::OK {
|
|
|
|
bail!("failed to get status (are you logged in?)");
|
|
|
|
}
|
|
|
|
|
|
|
|
let status = resp.json::<StatusResponse>().await?;
|
|
|
|
|
|
|
|
Ok(status)
|
|
|
|
}
|
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
pub async fn get_history(
|
2021-04-13 12:14:07 -06:00
|
|
|
&self,
|
2023-09-11 02:26:05 -06:00
|
|
|
sync_ts: OffsetDateTime,
|
|
|
|
history_ts: OffsetDateTime,
|
2021-04-13 12:14:07 -06:00
|
|
|
host: Option<String>,
|
2023-06-21 01:45:23 -06:00
|
|
|
) -> Result<SyncHistoryResponse> {
|
2023-06-12 10:58:46 -06:00
|
|
|
let host = host.unwrap_or_else(|| {
|
|
|
|
hash_str(&format!(
|
|
|
|
"{}:{}",
|
|
|
|
env::var("ATUIN_HOST_NAME").unwrap_or_else(|_| whoami::hostname()),
|
|
|
|
env::var("ATUIN_HOST_USER").unwrap_or_else(|_| whoami::username())
|
|
|
|
))
|
|
|
|
});
|
2021-04-13 12:14:07 -06:00
|
|
|
|
|
|
|
let url = format!(
|
|
|
|
"{}/sync/history?sync_ts={}&history_ts={}&host={}",
|
2021-04-20 10:07:11 -06:00
|
|
|
self.sync_addr,
|
2023-09-11 02:26:05 -06:00
|
|
|
urlencoding::encode(sync_ts.format(&Rfc3339)?.as_str()),
|
|
|
|
urlencoding::encode(history_ts.format(&Rfc3339)?.as_str()),
|
2021-04-13 12:14:07 -06:00
|
|
|
host,
|
|
|
|
);
|
|
|
|
|
2021-05-09 14:17:24 -06:00
|
|
|
let resp = self.client.get(url).send().await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2023-09-26 07:44:56 -06:00
|
|
|
let status = resp.status();
|
|
|
|
if status.is_success() {
|
|
|
|
let history = resp.json::<SyncHistoryResponse>().await?;
|
|
|
|
Ok(history)
|
|
|
|
} else if status.is_client_error() {
|
|
|
|
let error = resp.json::<ErrorResponse>().await?.reason;
|
|
|
|
bail!("Could not fetch history: {error}.")
|
|
|
|
} else if status.is_server_error() {
|
|
|
|
let error = resp.json::<ErrorResponse>().await?.reason;
|
|
|
|
bail!("There was an error with the atuin sync service: {error}.\nIf the problem persists, contact the host")
|
|
|
|
} else {
|
|
|
|
bail!("There was an error with the atuin sync service: Status {status:?}.\nIf the problem persists, contact the host")
|
|
|
|
}
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|
|
|
|
|
2022-04-12 16:06:19 -06:00
|
|
|
pub async fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> {
|
2021-04-20 10:07:11 -06:00
|
|
|
let url = format!("{}/history", self.sync_addr);
|
|
|
|
let url = Url::parse(url.as_str())?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-05-09 14:17:24 -06:00
|
|
|
self.client.post(url).json(history).send().await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-03-20 03:26:54 -06:00
|
|
|
|
|
|
|
pub async fn delete_history(&self, h: History) -> Result<()> {
|
|
|
|
let url = format!("{}/history", self.sync_addr);
|
|
|
|
let url = Url::parse(url.as_str())?;
|
|
|
|
|
|
|
|
self.client
|
|
|
|
.delete(url)
|
|
|
|
.json(&DeleteHistoryRequest { client_id: h.id })
|
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-05-16 15:00:59 -06:00
|
|
|
|
2023-07-14 13:44:08 -06:00
|
|
|
pub async fn post_records(&self, records: &[Record<EncryptedData>]) -> Result<()> {
|
|
|
|
let url = format!("{}/record", self.sync_addr);
|
|
|
|
let url = Url::parse(url.as_str())?;
|
|
|
|
|
|
|
|
self.client.post(url).json(records).send().await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn next_records(
|
|
|
|
&self,
|
|
|
|
host: HostId,
|
|
|
|
tag: String,
|
|
|
|
start: Option<RecordId>,
|
|
|
|
count: u64,
|
|
|
|
) -> Result<Vec<Record<EncryptedData>>> {
|
|
|
|
let url = format!(
|
|
|
|
"{}/record/next?host={}&tag={}&count={}",
|
|
|
|
self.sync_addr, host.0, tag, count
|
|
|
|
);
|
|
|
|
let mut url = Url::parse(url.as_str())?;
|
|
|
|
|
|
|
|
if let Some(start) = start {
|
|
|
|
url.set_query(Some(
|
|
|
|
format!(
|
|
|
|
"host={}&tag={}&count={}&start={}",
|
|
|
|
host.0, tag, count, start.0
|
|
|
|
)
|
|
|
|
.as_str(),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
let resp = self.client.get(url).send().await?;
|
|
|
|
|
|
|
|
let records = resp.json::<Vec<Record<EncryptedData>>>().await?;
|
|
|
|
|
|
|
|
Ok(records)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn record_index(&self) -> Result<RecordIndex> {
|
|
|
|
let url = format!("{}/record", self.sync_addr);
|
|
|
|
let url = Url::parse(url.as_str())?;
|
|
|
|
|
|
|
|
let resp = self.client.get(url).send().await?;
|
|
|
|
let index = resp.json().await?;
|
|
|
|
|
|
|
|
Ok(index)
|
|
|
|
}
|
|
|
|
|
2023-05-16 15:00:59 -06:00
|
|
|
pub async fn delete(&self) -> Result<()> {
|
2023-05-17 14:28:37 -06:00
|
|
|
let url = format!("{}/account", self.sync_addr);
|
2023-05-16 15:00:59 -06:00
|
|
|
let url = Url::parse(url.as_str())?;
|
|
|
|
|
|
|
|
let resp = self.client.delete(url).send().await?;
|
|
|
|
|
|
|
|
if resp.status() == 403 {
|
|
|
|
bail!("invalid login details");
|
|
|
|
} else if resp.status() == 200 {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
bail!("Unknown error");
|
|
|
|
}
|
|
|
|
}
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|