forked from didirus/AstralRinth
Authentication (#37)
* Initial authentication implementation * Store user info in the database, improve encapsulation in profiles * Add user list, remove unused dependencies, add spantraces * Implement user remove, update UUID crate * Add user set-default * Revert submodule macro usage * Make tracing significantly less verbose
This commit is contained in:
1190
Cargo.lock
generated
1190
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
flake.nix
20
flake.nix
@@ -38,8 +38,7 @@
|
||||
gtk4 gdk-pixbuf atk webkitgtk dbus
|
||||
];
|
||||
shell = [
|
||||
toolchain
|
||||
(with fenix; combine [toolchain default.clippy rust-analyzer])
|
||||
(with fenix; combine [toolchain default.clippy complete.rust-src rust-analyzer])
|
||||
git
|
||||
jdk17 jdk8
|
||||
];
|
||||
@@ -58,19 +57,10 @@
|
||||
cli = utils.mkApp {
|
||||
drv = self.packages.${system}.theseus-cli;
|
||||
};
|
||||
cli-test = utils.mkApp {
|
||||
drv = pkgs.writeShellApplication {
|
||||
name = "theseus-test-cli";
|
||||
runtimeInputs = [
|
||||
(self.packages.${system}.theseus-cli.overrideAttrs (old: old // {
|
||||
release = false;
|
||||
}))
|
||||
];
|
||||
text = ''
|
||||
DUMMY_ID="$(printf '%0.sa' {1..32})"
|
||||
theseus_cli profile run -t "" -n "Test" -i "$DUMMY_ID" "$@"
|
||||
'';
|
||||
};
|
||||
cli-dev = utils.mkApp {
|
||||
drv = self.packages.${system}.theseus-cli.overrideAttrs (old: old // {
|
||||
release = false;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
rustc cargo clippy openssl pkg-config
|
||||
gtk4 gdk-pixbuf atk webkitgtk
|
||||
];
|
||||
}
|
||||
@@ -7,38 +7,32 @@ edition = "2018"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
async-trait = "0.1.51"
|
||||
|
||||
daedalus = { version = "0.1.16", features = ["bincode"] }
|
||||
|
||||
bytes = "1"
|
||||
bincode = { version = "2.0.0-rc.1", features = ["serde"] }
|
||||
sled = { version = "0.34.7", features = ["compression"] }
|
||||
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
json5 = "0.4.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
bytes = "1"
|
||||
zip = "0.5"
|
||||
zip-extensions = "0.6"
|
||||
sha1 = { version = "0.6.0", features = ["std"]}
|
||||
path-clean = "0.1.0"
|
||||
fs_extra = "1.2.0"
|
||||
sled = { version = "0.34.7", features = ["compression"] }
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1", features = ["serde", "v4"] }
|
||||
zip = "0.5"
|
||||
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
daedalus = { version = "0.1.16", features = ["bincode"] }
|
||||
dirs = "4.0"
|
||||
|
||||
regex = "1.5"
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures = "0.3"
|
||||
|
||||
sys-info = "0.9.0"
|
||||
|
||||
# TODO: possibly replace with tracing to have structured logging
|
||||
log = "0.4.14"
|
||||
const_format = "0.2.22"
|
||||
regex = "1.5"
|
||||
sys-info = "0.9.0"
|
||||
thiserror = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-error = "0.2"
|
||||
|
||||
async-tungstenite = { version = "0.17", features = ["tokio-runtime", "tokio-native-tls"] }
|
||||
futures = "0.3"
|
||||
once_cell = "1.9.0"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
argh = "0.1.6"
|
||||
|
||||
100
theseus/src/api/auth.rs
Normal file
100
theseus/src/api/auth.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
//! Authentication flow interface
|
||||
use crate::{launcher::auth as inner, State};
|
||||
use futures::prelude::*;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub use inner::Credentials;
|
||||
|
||||
/// Authenticate a user with Hydra
|
||||
/// To run this, you need to first spawn this function as a task, then
|
||||
/// open a browser to the given URL and finally wait on the spawned future
|
||||
/// with the ability to cancel in case the browser is closed before finishing
|
||||
#[tracing::instrument]
|
||||
pub async fn authenticate(
|
||||
browser_url: oneshot::Sender<url::Url>,
|
||||
) -> crate::Result<Credentials> {
|
||||
let mut flow = inner::HydraAuthFlow::new().await?;
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
let url = flow.prepare_login_url().await?;
|
||||
browser_url.send(url).map_err(|url| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Error sending browser url to parent: {url}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let credentials = flow.extract_credentials().await?;
|
||||
users.insert(&credentials)?;
|
||||
|
||||
if state.settings.read().await.default_user.is_none() {
|
||||
let mut settings = state.settings.write().await;
|
||||
settings.default_user = Some(credentials.id);
|
||||
}
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
/// Refresh some credentials using Hydra, if needed
|
||||
#[tracing::instrument]
|
||||
pub async fn refresh(
|
||||
user: uuid::Uuid,
|
||||
update_name: bool,
|
||||
) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
futures::future::ready(users.get(user)?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to refresh nonexistent user with ID {user}"
|
||||
))
|
||||
.as_error()
|
||||
}))
|
||||
.and_then(|mut credentials| async move {
|
||||
if chrono::offset::Utc::now() > credentials.expires {
|
||||
inner::refresh_credentials(&mut credentials).await?;
|
||||
if update_name {
|
||||
inner::refresh_username(&mut credentials).await?;
|
||||
}
|
||||
}
|
||||
users.insert(&credentials)?;
|
||||
Ok(credentials)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Remove a user account from the database
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
if state.settings.read().await.default_user == Some(user) {
|
||||
let mut settings = state.settings.write().await;
|
||||
settings.default_user = users
|
||||
.0
|
||||
.first()?
|
||||
.map(|it| uuid::Uuid::from_slice(&it.0))
|
||||
.transpose()?;
|
||||
}
|
||||
|
||||
users.remove(user)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a user exists in Theseus
|
||||
#[tracing::instrument]
|
||||
pub async fn has_user(user: uuid::Uuid) -> crate::Result<bool> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
|
||||
Ok(users.contains(user)?)
|
||||
}
|
||||
|
||||
/// Get a copy of the list of all user credentials
|
||||
#[tracing::instrument]
|
||||
pub async fn users() -> crate::Result<Box<[Credentials]>> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
users.iter().collect()
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
//! API for interacting with Theseus
|
||||
pub mod auth;
|
||||
pub mod profile;
|
||||
|
||||
pub mod data {
|
||||
pub use crate::{
|
||||
launcher::Credentials,
|
||||
state::{
|
||||
DirectoryInfo, Hooks, JavaSettings, MemorySettings, ModLoader,
|
||||
ProfileMetadata, Settings, WindowSize,
|
||||
},
|
||||
pub use crate::state::{
|
||||
DirectoryInfo, Hooks, JavaSettings, MemorySettings, ModLoader,
|
||||
ProfileMetadata, Settings, WindowSize,
|
||||
};
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
auth::{self, Credentials},
|
||||
data::*,
|
||||
profile::{self, Profile},
|
||||
State,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
//! Theseus profile management interface
|
||||
|
||||
pub use crate::{
|
||||
state::{JavaSettings, Profile},
|
||||
State,
|
||||
};
|
||||
use daedalus as d;
|
||||
use std::{future::Future, path::Path};
|
||||
use std::{
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::process::{Child, Command};
|
||||
|
||||
/// Add a profile to the in-memory state
|
||||
#[tracing::instrument]
|
||||
pub async fn add(profile: Profile) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
@@ -18,6 +21,7 @@ pub async fn add(profile: Profile) -> crate::Result<()> {
|
||||
}
|
||||
|
||||
/// Add a path as a profile in-memory
|
||||
#[tracing::instrument]
|
||||
pub async fn add_path(path: &Path) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
@@ -27,6 +31,7 @@ pub async fn add_path(path: &Path) -> crate::Result<()> {
|
||||
}
|
||||
|
||||
/// Remove a profile
|
||||
#[tracing::instrument]
|
||||
pub async fn remove(path: &Path) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
@@ -36,19 +41,22 @@ pub async fn remove(path: &Path) -> crate::Result<()> {
|
||||
}
|
||||
|
||||
/// Get a profile by path,
|
||||
#[tracing::instrument]
|
||||
pub async fn get(path: &Path) -> crate::Result<Option<Profile>> {
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
|
||||
profiles.0.get(path).map_or(Ok(None), |prof| match prof {
|
||||
Some(prof) => Ok(Some(prof.clone())),
|
||||
None => Err(crate::Error::UnloadedProfileError(
|
||||
None => Err(crate::ErrorKind::UnloadedProfileError(
|
||||
path.display().to_string(),
|
||||
)),
|
||||
)
|
||||
.as_error()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a profile is already managed by Theseus
|
||||
#[tracing::instrument]
|
||||
pub async fn is_managed(profile: &Path) -> crate::Result<bool> {
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
@@ -56,6 +64,7 @@ pub async fn is_managed(profile: &Path) -> crate::Result<bool> {
|
||||
}
|
||||
|
||||
/// Check if a profile is loaded
|
||||
#[tracing::instrument]
|
||||
pub async fn is_loaded(profile: &Path) -> crate::Result<bool> {
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
@@ -75,29 +84,41 @@ pub async fn edit<Fut>(
|
||||
where
|
||||
Fut: Future<Output = crate::Result<()>>,
|
||||
{
|
||||
let state = State::get().await.unwrap();
|
||||
let state = State::get().await?;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
match profiles.0.get_mut(path) {
|
||||
Some(&mut Some(ref mut profile)) => action(profile).await,
|
||||
Some(&mut None) => Err(crate::Error::UnloadedProfileError(
|
||||
Some(&mut None) => Err(crate::ErrorKind::UnloadedProfileError(
|
||||
path.display().to_string(),
|
||||
)),
|
||||
None => Err(crate::Error::UnmanagedProfileError(
|
||||
)
|
||||
.as_error()),
|
||||
None => Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
path.display().to_string(),
|
||||
)),
|
||||
)
|
||||
.as_error()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a copy of the profile set
|
||||
#[tracing::instrument]
|
||||
pub async fn list(
|
||||
) -> crate::Result<std::collections::HashMap<PathBuf, Option<Profile>>> {
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
Ok(profiles.0.clone())
|
||||
}
|
||||
|
||||
/// Run Minecraft using a profile
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn run(
|
||||
path: &Path,
|
||||
credentials: &crate::launcher::Credentials,
|
||||
credentials: &crate::auth::Credentials,
|
||||
) -> crate::Result<Child> {
|
||||
let state = State::get().await.unwrap();
|
||||
let settings = state.settings.read().await;
|
||||
let profile = get(path).await?.ok_or_else(|| {
|
||||
crate::Error::OtherError(format!(
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to run a nonexistent or unloaded profile at path {}!",
|
||||
path.display()
|
||||
))
|
||||
@@ -110,7 +131,7 @@ pub async fn run(
|
||||
.iter()
|
||||
.find(|it| it.id == profile.metadata.game_version.as_ref())
|
||||
.ok_or_else(|| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Invalid or unknown Minecraft version: {}",
|
||||
profile.metadata.game_version
|
||||
))
|
||||
@@ -130,10 +151,11 @@ pub async fn run(
|
||||
.await?;
|
||||
|
||||
if !result.success() {
|
||||
return Err(crate::Error::LauncherError(format!(
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Non-zero exit code for pre-launch hook: {}",
|
||||
result.code().unwrap_or(-1)
|
||||
)));
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +175,7 @@ pub async fn run(
|
||||
settings.java_8_path.as_ref()
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"No Java installed for version {}",
|
||||
version_info.java_version.map_or(8, |it| it.major_version),
|
||||
))
|
||||
@@ -161,10 +183,11 @@ pub async fn run(
|
||||
};
|
||||
|
||||
if !java_install.exists() {
|
||||
return Err(crate::Error::LauncherError(format!(
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Could not find Java install: {}",
|
||||
java_install.display()
|
||||
)));
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
|
||||
let ref java_args = profile
|
||||
@@ -195,21 +218,26 @@ pub async fn run(
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn kill(running: &mut Child) -> crate::Result<()> {
|
||||
running.kill().await?;
|
||||
wait_for(running).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn wait_for(running: &mut Child) -> crate::Result<()> {
|
||||
let result = running.wait().await.map_err(|err| {
|
||||
crate::Error::LauncherError(format!("Error running minecraft: {err}"))
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Error running minecraft: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
match result.success() {
|
||||
false => Err(crate::Error::LauncherError(format!(
|
||||
false => Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Minecraft exited with non-zero code {}",
|
||||
result.code().unwrap_or(-1)
|
||||
))),
|
||||
))
|
||||
.as_error()),
|
||||
true => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
//! Theseus error type
|
||||
use tracing_error::InstrumentError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
pub enum ErrorKind {
|
||||
#[error("Filesystem error: {0}")]
|
||||
FSError(String),
|
||||
|
||||
#[error("Serialization error (JSON): {0}")]
|
||||
JSONError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Error parsing UUID: {0}")]
|
||||
UUIDError(#[from] uuid::Error),
|
||||
|
||||
#[error("Serialization error (Bincode): {0}")]
|
||||
EncodeError(#[from] bincode::error::DecodeError),
|
||||
EncodeError(#[from] bincode::error::EncodeError),
|
||||
|
||||
#[error("Deserialization error (Bincode): {0}")]
|
||||
DecodeError(#[from] bincode::error::EncodeError),
|
||||
DecodeError(#[from] bincode::error::DecodeError),
|
||||
|
||||
#[error("Error parsing URL: {0}")]
|
||||
URLError(#[from] url::ParseError),
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
DBError(#[from] sled::Error),
|
||||
@@ -22,6 +30,9 @@ pub enum Error {
|
||||
#[error("Metadata error: {0}")]
|
||||
MetadataError(#[from] daedalus::Error),
|
||||
|
||||
#[error("Minecraft authentication error: {0}")]
|
||||
HydraError(String),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
@@ -31,6 +42,12 @@ pub enum Error {
|
||||
#[error("Error fetching URL: {0}")]
|
||||
FetchError(#[from] reqwest::Error),
|
||||
|
||||
#[error("Websocket error: {0}")]
|
||||
WSError(#[from] async_tungstenite::tungstenite::Error),
|
||||
|
||||
#[error("Websocket closed before {0} could be received!")]
|
||||
WSClosedError(String),
|
||||
|
||||
#[error("Incorrect Sha1 hash for download: {0} != {1}")]
|
||||
HashError(String, String),
|
||||
|
||||
@@ -52,4 +69,35 @@ pub enum Error {
|
||||
OtherError(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
source: tracing_error::TracedError<ErrorKind>,
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.source.source()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.source)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Into<ErrorKind>> From<E> for Error {
|
||||
fn from(source: E) -> Self {
|
||||
Self {
|
||||
source: Into::<ErrorKind>::into(source).in_current_span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
pub fn as_error(self) -> Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
@@ -40,10 +40,11 @@ pub fn get_class_paths(
|
||||
client_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified class path {} does not exist",
|
||||
client_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
@@ -70,10 +71,11 @@ pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
|
||||
path.push(get_path_from_artifact(lib.as_ref())?);
|
||||
|
||||
let path = &path.canonicalize().map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Library file at path {} does not exist",
|
||||
path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
@@ -104,10 +106,11 @@ pub fn get_jvm_arguments(
|
||||
"-Djava.library.path={}",
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
.map_err(|_| crate::Error::LauncherError(format!(
|
||||
.map_err(|_| crate::ErrorKind::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
)))?
|
||||
))
|
||||
.as_error())?
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
));
|
||||
@@ -142,10 +145,11 @@ fn parse_jvm_argument(
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy(),
|
||||
)
|
||||
@@ -154,10 +158,11 @@ fn parse_jvm_argument(
|
||||
&libraries_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified libraries path {} does not exist",
|
||||
libraries_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
@@ -239,7 +244,7 @@ fn parse_minecraft_argument(
|
||||
.replace("${auth_access_token}", access_token)
|
||||
.replace("${auth_session}", access_token)
|
||||
.replace("${auth_player_name}", username)
|
||||
.replace("${auth_uuid}", &uuid.to_hyphenated().to_string())
|
||||
.replace("${auth_uuid}", &uuid.hyphenated().to_string())
|
||||
.replace("${user_properties}", "{}")
|
||||
.replace("${user_type}", "mojang")
|
||||
.replace("${version_name}", version)
|
||||
@@ -249,10 +254,11 @@ fn parse_minecraft_argument(
|
||||
&game_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified game directory {} does not exist",
|
||||
game_directory.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_owned(),
|
||||
@@ -262,10 +268,11 @@ fn parse_minecraft_argument(
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_owned(),
|
||||
@@ -275,10 +282,11 @@ fn parse_minecraft_argument(
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_owned(),
|
||||
@@ -361,17 +369,19 @@ pub async fn get_processor_main_class(
|
||||
Ok(tokio::task::spawn_blocking(move || {
|
||||
let zipfile = std::fs::File::open(&path)?;
|
||||
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Cannot read processor at {}",
|
||||
path
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let file = archive.by_name("META-INF/MANIFEST.MF").map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Cannot read processor manifest at {}",
|
||||
path
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
@@ -1,8 +1,168 @@
|
||||
//! Authentication flow
|
||||
// TODO: Implement authentication
|
||||
#[derive(Debug)]
|
||||
//! Authentication flow based on Hydra
|
||||
use async_tungstenite as ws;
|
||||
use bincode::{Decode, Encode};
|
||||
use chrono::{prelude::*, Duration};
|
||||
use futures::prelude::*;
|
||||
use once_cell::sync::*;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
pub const HYDRA_URL: Lazy<Url> =
|
||||
Lazy::new(|| Url::parse("https://hydra.modrinth.com").unwrap());
|
||||
|
||||
// Socket messages
|
||||
#[derive(Deserialize)]
|
||||
struct ErrorJSON {
|
||||
error: String,
|
||||
}
|
||||
|
||||
impl ErrorJSON {
|
||||
pub fn unwrap<'a, T: Deserialize<'a>>(data: &'a [u8]) -> crate::Result<T> {
|
||||
if let Ok(err) = serde_json::from_slice::<Self>(data) {
|
||||
Err(crate::ErrorKind::HydraError(err.error).as_error())
|
||||
} else {
|
||||
Ok(serde_json::from_slice::<T>(data)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginCodeJSON {
|
||||
login_code: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TokenJSON {
|
||||
token: String,
|
||||
refresh_token: String,
|
||||
expires_after: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ProfileInfoJSON {
|
||||
id: uuid::Uuid,
|
||||
name: String,
|
||||
}
|
||||
|
||||
// Login information
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct Credentials {
|
||||
#[bincode(with_serde)]
|
||||
pub id: uuid::Uuid,
|
||||
pub username: String,
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
#[bincode(with_serde)]
|
||||
pub expires: DateTime<Utc>,
|
||||
_ctor_scope: std::marker::PhantomData<()>,
|
||||
}
|
||||
|
||||
// Implementation
|
||||
pub struct HydraAuthFlow<S: AsyncRead + AsyncWrite + Unpin> {
|
||||
socket: ws::WebSocketStream<S>,
|
||||
}
|
||||
|
||||
impl HydraAuthFlow<ws::tokio::ConnectStream> {
|
||||
pub async fn new() -> crate::Result<Self> {
|
||||
let sock_url = wrap_ref_builder!(
|
||||
it = HYDRA_URL =>
|
||||
{ it.set_scheme("wss").ok() }
|
||||
);
|
||||
let (socket, _) = ws::tokio::connect_async(sock_url.clone()).await?;
|
||||
Ok(Self { socket })
|
||||
}
|
||||
|
||||
pub async fn prepare_login_url(&mut self) -> crate::Result<Url> {
|
||||
let code_resp = self
|
||||
.socket
|
||||
.try_next()
|
||||
.await?
|
||||
.ok_or(
|
||||
crate::ErrorKind::WSClosedError(String::from(
|
||||
"login socket ID",
|
||||
))
|
||||
.as_error(),
|
||||
)?
|
||||
.into_data();
|
||||
let code = ErrorJSON::unwrap::<LoginCodeJSON>(&code_resp)?;
|
||||
Ok(wrap_ref_builder!(
|
||||
it = HYDRA_URL.join("login")? =>
|
||||
{ it.query_pairs_mut().append_pair("id", &code.login_code); }
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn extract_credentials(&mut self) -> crate::Result<Credentials> {
|
||||
// Minecraft bearer token
|
||||
let token_resp = self
|
||||
.socket
|
||||
.try_next()
|
||||
.await?
|
||||
.ok_or(
|
||||
crate::ErrorKind::WSClosedError(String::from(
|
||||
"login socket ID",
|
||||
))
|
||||
.as_error(),
|
||||
)?
|
||||
.into_data();
|
||||
let token = ErrorJSON::unwrap::<TokenJSON>(&token_resp)?;
|
||||
let expires =
|
||||
Utc::now() + Duration::seconds(token.expires_after.into());
|
||||
|
||||
// Get account credentials
|
||||
let info = fetch_info(&token.token).await?;
|
||||
|
||||
// Return structure from response
|
||||
Ok(Credentials {
|
||||
username: info.name,
|
||||
id: info.id,
|
||||
refresh_token: token.refresh_token,
|
||||
access_token: token.token,
|
||||
expires,
|
||||
_ctor_scope: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh_credentials(
|
||||
credentials: &mut Credentials,
|
||||
) -> crate::Result<()> {
|
||||
let resp = crate::config::REQWEST_CLIENT
|
||||
.post(HYDRA_URL.join("/refresh")?)
|
||||
.json(
|
||||
&serde_json::json!({ "refresh_token": credentials.refresh_token }),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<TokenJSON>()
|
||||
.await?;
|
||||
|
||||
credentials.access_token = resp.token;
|
||||
credentials.refresh_token = resp.refresh_token;
|
||||
credentials.expires =
|
||||
Utc::now() + Duration::seconds(resp.expires_after.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn refresh_username(
|
||||
credentials: &mut Credentials,
|
||||
) -> crate::Result<()> {
|
||||
let info = fetch_info(&credentials.access_token).await?;
|
||||
credentials.username = info.name;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helpers
|
||||
async fn fetch_info(token: &str) -> crate::Result<ProfileInfoJSON> {
|
||||
let url =
|
||||
Url::parse("https://api.minecraftservices.com/minecraft/profile")?;
|
||||
Ok(crate::config::REQWEST_CLIENT
|
||||
.get(url)
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<ProfileInfoJSON>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ use daedalus::{
|
||||
modded::LoaderVersion,
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use tokio::{fs, sync::OnceCell};
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn download_minecraft(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
@@ -33,6 +33,7 @@ pub async fn download_minecraft(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))]
|
||||
pub async fn download_version_info(
|
||||
st: &State,
|
||||
version: &GameVersion,
|
||||
@@ -69,6 +70,7 @@ pub async fn download_version_info(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn download_client(
|
||||
st: &State,
|
||||
version_info: &GameVersionInfo,
|
||||
@@ -78,9 +80,12 @@ pub async fn download_client(
|
||||
let client_download = version_info
|
||||
.downloads
|
||||
.get(&d::minecraft::DownloadType::Client)
|
||||
.ok_or(crate::Error::LauncherError(format!(
|
||||
"No client downloads exist for version {version}"
|
||||
)))?;
|
||||
.ok_or(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"No client downloads exist for version {version}"
|
||||
))
|
||||
.as_error(),
|
||||
)?;
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(version)
|
||||
@@ -99,6 +104,7 @@ pub async fn download_client(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn download_assets_index(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
@@ -126,6 +132,7 @@ pub async fn download_assets_index(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(st, index))]
|
||||
pub async fn download_assets(
|
||||
st: &State,
|
||||
with_legacy: bool,
|
||||
@@ -180,16 +187,13 @@ pub async fn download_assets(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(st, libraries))]
|
||||
pub async fn download_libraries(
|
||||
st: &State,
|
||||
libraries: &[Library],
|
||||
version: &str,
|
||||
) -> crate::Result<()> {
|
||||
log::debug!("Loading libraries");
|
||||
let (libraries_dir, natives_dir) = (
|
||||
Arc::new(st.directories.libraries_dir()),
|
||||
Arc::new(st.directories.version_natives_dir(version)),
|
||||
);
|
||||
|
||||
tokio::try_join! {
|
||||
fs::create_dir_all(st.directories.libraries_dir()),
|
||||
|
||||
@@ -6,11 +6,11 @@ use tokio::process::{Child, Command};
|
||||
|
||||
mod args;
|
||||
|
||||
mod auth;
|
||||
pub use auth::Credentials;
|
||||
pub mod auth;
|
||||
|
||||
mod download;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn parse_rule(rule: &d::minecraft::Rule) -> bool {
|
||||
use d::minecraft::{Rule, RuleAction};
|
||||
|
||||
@@ -44,6 +44,7 @@ macro_rules! processor_rules {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(path = ?instance_path))]
|
||||
pub async fn launch_minecraft(
|
||||
game_version: &str,
|
||||
loader_version: &Option<d::modded::LoaderVersion>,
|
||||
@@ -64,7 +65,7 @@ pub async fn launch_minecraft(
|
||||
.versions
|
||||
.iter()
|
||||
.find(|it| it.id == game_version)
|
||||
.ok_or(crate::Error::LauncherError(format!(
|
||||
.ok_or(crate::ErrorKind::LauncherError(format!(
|
||||
"Invalid game version: {game_version}"
|
||||
)))?;
|
||||
|
||||
@@ -115,8 +116,9 @@ pub async fn launch_minecraft(
|
||||
}
|
||||
}
|
||||
|
||||
let mut cp = processor.classpath.clone();
|
||||
cp.push(processor.jar.clone());
|
||||
let cp = wrap_ref_builder!(cp = processor.classpath.clone() => {
|
||||
cp.push(processor.jar.clone())
|
||||
});
|
||||
|
||||
let child = Command::new("java")
|
||||
.arg("-cp")
|
||||
@@ -131,7 +133,7 @@ pub async fn launch_minecraft(
|
||||
)?)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Could not find processor main class for {}",
|
||||
processor.jar
|
||||
))
|
||||
@@ -145,16 +147,17 @@ pub async fn launch_minecraft(
|
||||
.output()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Error running processor: {err}",
|
||||
))
|
||||
})?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(crate::Error::LauncherError(format!(
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Processor error: {}",
|
||||
String::from_utf8_lossy(&child.stderr)
|
||||
)));
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,9 +166,7 @@ pub async fn launch_minecraft(
|
||||
let args = version_info.arguments.clone().unwrap_or_default();
|
||||
let mut command = match wrapper {
|
||||
Some(hook) => {
|
||||
let mut cmd = Command::new(hook);
|
||||
cmd.arg(java_install);
|
||||
cmd
|
||||
wrap_ref_builder!(it = Command::new(hook) => {it.arg(java_install)})
|
||||
}
|
||||
None => Command::new(String::from(java_install.to_string_lossy())),
|
||||
};
|
||||
@@ -203,10 +204,11 @@ pub async fn launch_minecraft(
|
||||
.stderr(Stdio::inherit());
|
||||
|
||||
command.spawn().map_err(|err| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Error running Minecraft (minecraft-{} @ {}): {err}",
|
||||
&version.id,
|
||||
instance_path.display()
|
||||
))
|
||||
.as_error()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
Theseus is a library which provides utilities for launching minecraft, creating Modrinth mod packs,
|
||||
and launching Modrinth mod packs
|
||||
*/
|
||||
#![warn(unused_import_braces, missing_debug_implementations)]
|
||||
#![warn(unused_import_braces)]
|
||||
#![deny(unused_must_use)]
|
||||
|
||||
#[macro_use]
|
||||
mod util;
|
||||
|
||||
mod api;
|
||||
mod config;
|
||||
mod error;
|
||||
mod launcher;
|
||||
mod state;
|
||||
mod util;
|
||||
|
||||
pub use api::*;
|
||||
pub use error::*;
|
||||
|
||||
@@ -10,10 +10,11 @@ pub struct DirectoryInfo {
|
||||
|
||||
impl DirectoryInfo {
|
||||
/// Get all paths needed for Theseus to operate properly
|
||||
#[tracing::instrument]
|
||||
pub async fn init() -> crate::Result<Self> {
|
||||
// Working directory
|
||||
let working_dir = std::env::current_dir().map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Could not open working directory: {err}"
|
||||
))
|
||||
})?;
|
||||
@@ -21,12 +22,12 @@ impl DirectoryInfo {
|
||||
// Config directory
|
||||
let config_dir = Self::env_path("THESEUS_CONFIG_DIR")
|
||||
.or_else(|| Some(dirs::config_dir()?.join("theseus")))
|
||||
.ok_or(crate::Error::FSError(
|
||||
.ok_or(crate::ErrorKind::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
))?;
|
||||
|
||||
fs::create_dir_all(&config_dir).await.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error creating Theseus config directory: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -13,6 +13,7 @@ use std::collections::LinkedList;
|
||||
const METADATA_URL: &str = "https://meta.modrinth.com/gamedata";
|
||||
const METADATA_DB_FIELD: &[u8] = b"metadata";
|
||||
|
||||
// TODO: store as subtree in database
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct Metadata {
|
||||
pub minecraft: MinecraftManifest,
|
||||
@@ -48,6 +49,7 @@ impl Metadata {
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
let mut metadata = None;
|
||||
|
||||
@@ -84,7 +86,10 @@ impl Metadata {
|
||||
db.flush_async().await?;
|
||||
Ok(meta)
|
||||
} else {
|
||||
Err(crate::Error::NoValueFor(String::from("launcher metadata")))
|
||||
Err(
|
||||
crate::ErrorKind::NoValueFor(String::from("launcher metadata"))
|
||||
.as_error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,19 @@ mod dirs;
|
||||
pub use self::dirs::*;
|
||||
|
||||
mod metadata;
|
||||
pub use metadata::*;
|
||||
|
||||
mod settings;
|
||||
pub use settings::*;
|
||||
pub use self::metadata::*;
|
||||
|
||||
mod profiles;
|
||||
pub use profiles::*;
|
||||
pub use self::profiles::*;
|
||||
|
||||
mod settings;
|
||||
pub use self::settings::*;
|
||||
|
||||
mod users;
|
||||
pub use self::users::*;
|
||||
|
||||
// Global state
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
/// Database, used to store some information
|
||||
pub(self) database: sled::Db,
|
||||
@@ -28,52 +30,62 @@ pub struct State {
|
||||
pub io_semaphore: Semaphore,
|
||||
/// Launcher metadata
|
||||
pub metadata: Metadata,
|
||||
// TODO: settings API
|
||||
/// Launcher configuration
|
||||
pub settings: RwLock<Settings>,
|
||||
/// Launcher profile metadata
|
||||
pub profiles: RwLock<Profiles>,
|
||||
pub(crate) profiles: RwLock<Profiles>,
|
||||
/// Launcher user account info
|
||||
pub(crate) users: RwLock<Users>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
#[tracing::instrument]
|
||||
/// Get the current launcher state, initializing it if needed
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(|| async {
|
||||
// Directories
|
||||
let directories = DirectoryInfo::init().await?;
|
||||
.get_or_try_init(|| {
|
||||
async {
|
||||
// Directories
|
||||
let directories = DirectoryInfo::init().await?;
|
||||
|
||||
// Database
|
||||
// TODO: make database versioned
|
||||
let database =
|
||||
sled_config().path(directories.database_file()).open()?;
|
||||
// Database
|
||||
// TODO: make database versioned
|
||||
let database = sled_config()
|
||||
.path(directories.database_file())
|
||||
.open()?;
|
||||
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&directories.settings_file()).await?;
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&directories.settings_file()).await?;
|
||||
|
||||
// Metadata
|
||||
let metadata = Metadata::init(&database).await?;
|
||||
// Launcher data
|
||||
let (metadata, profiles) = tokio::try_join! {
|
||||
Metadata::init(&database),
|
||||
Profiles::init(&database),
|
||||
}?;
|
||||
let users = Users::init(&database)?;
|
||||
|
||||
// Profiles
|
||||
let profiles = Profiles::init(&database).await?;
|
||||
// Loose initializations
|
||||
let io_semaphore =
|
||||
Semaphore::new(settings.max_concurrent_downloads);
|
||||
|
||||
// Loose initializations
|
||||
let io_semaphore =
|
||||
Semaphore::new(settings.max_concurrent_downloads);
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
database,
|
||||
directories,
|
||||
io_semaphore,
|
||||
metadata,
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
}))
|
||||
Ok(Arc::new(Self {
|
||||
database,
|
||||
directories,
|
||||
io_semaphore,
|
||||
metadata,
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
users: RwLock::new(users),
|
||||
}))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map(Arc::clone)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
/// Synchronize in-memory state with persistent state
|
||||
pub async fn sync() -> crate::Result<()> {
|
||||
let state = Self::get().await?;
|
||||
|
||||
@@ -12,8 +12,7 @@ use tokio::fs;
|
||||
const PROFILE_JSON_PATH: &str = "profile.json";
|
||||
const PROFILE_SUBTREE: &[u8] = b"profiles";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
|
||||
pub(crate) struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
|
||||
|
||||
// TODO: possibly add defaults to some of these values
|
||||
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
@@ -84,15 +83,17 @@ pub struct JavaSettings {
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
#[tracing::instrument]
|
||||
pub async fn new(
|
||||
name: String,
|
||||
version: String,
|
||||
path: PathBuf,
|
||||
) -> crate::Result<Self> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(crate::Error::InputError(String::from(
|
||||
return Err(crate::ErrorKind::InputError(String::from(
|
||||
"Empty name for instance!",
|
||||
)));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -114,11 +115,13 @@ impl Profile {
|
||||
|
||||
// TODO: deduplicate these builder methods
|
||||
// They are flat like this in order to allow builder-style usage
|
||||
#[tracing::instrument]
|
||||
pub fn with_name(&mut self, name: String) -> &mut Self {
|
||||
self.metadata.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn with_icon<'a>(
|
||||
&'a mut self,
|
||||
icon: &'a Path,
|
||||
@@ -136,17 +139,20 @@ impl Profile {
|
||||
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(crate::Error::InputError(format!(
|
||||
Err(crate::ErrorKind::InputError(format!(
|
||||
"Unsupported image type: {ext}"
|
||||
)))
|
||||
))
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
||||
self.metadata.game_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_loader(
|
||||
&mut self,
|
||||
loader: ModLoader,
|
||||
@@ -157,6 +163,7 @@ impl Profile {
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_java_settings(
|
||||
&mut self,
|
||||
settings: Option<JavaSettings>,
|
||||
@@ -165,6 +172,7 @@ impl Profile {
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_memory(
|
||||
&mut self,
|
||||
settings: Option<MemorySettings>,
|
||||
@@ -173,6 +181,7 @@ impl Profile {
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_resolution(
|
||||
&mut self,
|
||||
resolution: Option<WindowSize>,
|
||||
@@ -181,6 +190,7 @@ impl Profile {
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_hooks(&mut self, hooks: Option<Hooks>) -> &mut Self {
|
||||
self.hooks = hooks;
|
||||
self
|
||||
@@ -188,6 +198,7 @@ impl Profile {
|
||||
}
|
||||
|
||||
impl Profiles {
|
||||
#[tracing::instrument(skip(db))]
|
||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
|
||||
Ok(Default::default()),
|
||||
@@ -218,19 +229,23 @@ impl Profiles {
|
||||
Ok(Self(profiles))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||
self.0.insert(
|
||||
profile
|
||||
.path
|
||||
.canonicalize()?
|
||||
.to_str()
|
||||
.ok_or(crate::Error::UTFError(profile.path.clone()))?
|
||||
.ok_or(
|
||||
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
||||
)?
|
||||
.into(),
|
||||
Some(profile),
|
||||
);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn insert_from<'a>(
|
||||
&'a mut self,
|
||||
path: &'a Path,
|
||||
@@ -238,12 +253,14 @@ impl Profiles {
|
||||
self.insert(Self::read_profile_from_dir(&path.canonicalize()?).await?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
||||
let path = PathBuf::from(path.canonicalize()?.to_str().unwrap());
|
||||
self.0.remove(&path);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn sync<'a>(
|
||||
&'a self,
|
||||
batch: &'a mut sled::Batch,
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct Settings {
|
||||
pub custom_java_args: Vec<String>,
|
||||
pub java_8_path: Option<PathBuf>,
|
||||
pub java_17_path: Option<PathBuf>,
|
||||
pub default_user: Option<uuid::Uuid>,
|
||||
pub hooks: Hooks,
|
||||
pub max_concurrent_downloads: usize,
|
||||
pub version: u32,
|
||||
@@ -32,6 +33,7 @@ impl Default for Settings {
|
||||
custom_java_args: Vec::new(),
|
||||
java_8_path: None,
|
||||
java_17_path: None,
|
||||
default_user: None,
|
||||
hooks: Hooks::default(),
|
||||
max_concurrent_downloads: 64,
|
||||
version: CURRENT_FORMAT_VERSION,
|
||||
@@ -40,14 +42,16 @@ impl Default for Settings {
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
#[tracing::instrument]
|
||||
pub async fn init(file: &Path) -> crate::Result<Self> {
|
||||
if file.exists() {
|
||||
fs::read(&file)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error reading settings file: {err}"
|
||||
))
|
||||
.as_error()
|
||||
})
|
||||
.and_then(|it| {
|
||||
serde_json::from_slice::<Settings>(&it)
|
||||
@@ -58,13 +62,15 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
||||
fs::write(to, serde_json::to_vec_pretty(self)?)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error saving settings to file: {err}"
|
||||
))
|
||||
.as_error()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
79
theseus/src/state/users.rs
Normal file
79
theseus/src/state/users.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//! User login info
|
||||
use crate::{auth::Credentials, config::BINCODE_CONFIG};
|
||||
|
||||
const USER_DB_TREE: &[u8] = b"users";
|
||||
|
||||
/// The set of users stored in the launcher
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Users(pub(crate) sled::Tree);
|
||||
|
||||
impl Users {
|
||||
#[tracing::instrument(skip(db))]
|
||||
pub fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
Ok(Self(db.open_tree(USER_DB_TREE)?))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
credentials: &Credentials,
|
||||
) -> crate::Result<&Self> {
|
||||
let id = credentials.id.as_bytes();
|
||||
self.0.insert(
|
||||
id,
|
||||
bincode::encode_to_vec(credentials, *BINCODE_CONFIG)?,
|
||||
)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn contains(&self, id: uuid::Uuid) -> crate::Result<bool> {
|
||||
Ok(self.0.contains_key(id.as_bytes())?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn get(&self, id: uuid::Uuid) -> crate::Result<Option<Credentials>> {
|
||||
self.0.get(id.as_bytes())?.map_or(Ok(None), |prof| {
|
||||
bincode::decode_from_slice(&prof, *BINCODE_CONFIG)
|
||||
.map_err(crate::Error::from)
|
||||
.map(|it| Some(it.0))
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn remove(&mut self, id: uuid::Uuid) -> crate::Result<&Self> {
|
||||
self.0.remove(id.as_bytes())?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> UserIter<impl UserInnerIter> {
|
||||
UserIter(self.0.iter().values(), false)
|
||||
}
|
||||
}
|
||||
|
||||
alias_trait! {
|
||||
pub UserInnerIter: Iterator<Item = sled::Result<sled::IVec>>, Send, Sync
|
||||
}
|
||||
|
||||
/// An iterator over the set of users
|
||||
#[derive(Debug)]
|
||||
pub struct UserIter<I: UserInnerIter>(I, bool);
|
||||
|
||||
impl<I: UserInnerIter> Iterator for UserIter<I> {
|
||||
type Item = crate::Result<Credentials>;
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let it = self.0.next()?;
|
||||
let res = it.map_err(crate::Error::from).and_then(|it| {
|
||||
Ok(bincode::decode_from_slice(&it, *BINCODE_CONFIG)?.0)
|
||||
});
|
||||
|
||||
self.1 = res.is_err();
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use tokio::{
|
||||
|
||||
const FETCH_ATTEMPTS: usize = 3;
|
||||
|
||||
#[tracing::instrument(skip(_permit))]
|
||||
pub async fn fetch<'a>(
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
@@ -25,10 +26,11 @@ pub async fn fetch<'a>(
|
||||
if let Some(hash) = sha1 {
|
||||
let actual_hash = sha1_async(bytes.clone()).await;
|
||||
if actual_hash != hash {
|
||||
return Err(crate::Error::HashError(
|
||||
return Err(crate::ErrorKind::HashError(
|
||||
actual_hash,
|
||||
String::from(hash),
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +47,7 @@ pub async fn fetch<'a>(
|
||||
// This is implemented, as it will be useful in porting modpacks
|
||||
// For now, allow it to be dead code
|
||||
#[allow(dead_code)]
|
||||
#[tracing::instrument(skip(sem))]
|
||||
pub async fn fetch_mirrors(
|
||||
urls: &[&str],
|
||||
sha1: Option<&str>,
|
||||
@@ -70,6 +73,7 @@ pub async fn fetch_mirrors(
|
||||
.map(|it| it.0)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(bytes, _permit))]
|
||||
pub async fn write<'a>(
|
||||
path: &Path,
|
||||
bytes: &[u8],
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
//! Theseus utility functions
|
||||
pub mod fetch;
|
||||
pub mod platform;
|
||||
|
||||
/// Wrap a builder which uses a mut reference into one which outputs an owned value
|
||||
macro_rules! wrap_ref_builder {
|
||||
($id:ident = $init:expr => $transform:block) => {{
|
||||
let mut it = $init;
|
||||
{
|
||||
let $id = &mut it;
|
||||
$transform;
|
||||
}
|
||||
it
|
||||
}};
|
||||
}
|
||||
|
||||
/// Alias a trait, used to avoid needing nightly features
|
||||
macro_rules! alias_trait {
|
||||
($scope:vis $name:ident : $bound:path $(, $bounds:path)*) => {
|
||||
$scope trait $name: $bound $(+ $bounds)* {}
|
||||
impl<T: $bound $(+ $bounds)*> $name for T {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,16 @@ futures = "0.3"
|
||||
argh = "0.1"
|
||||
paris = { version = "1.5", features = ["macros", "no_logger"] }
|
||||
dialoguer = "0.10"
|
||||
eyre = "0.6"
|
||||
tabled = "0.5"
|
||||
dirs = "4.0"
|
||||
uuid = {version = "0.8", features = ["v4", "serde"]}
|
||||
# TODO: merge logging with paris logging
|
||||
pretty_env_logger = "0.4"
|
||||
log = "0.4.14"
|
||||
uuid = {version = "1.1", features = ["v4", "serde"]}
|
||||
url = "2.2"
|
||||
|
||||
color-eyre = "0.6"
|
||||
eyre = "0.6"
|
||||
tracing = "0.1"
|
||||
tracing-error = "0.2"
|
||||
tracing-futures = "0.2"
|
||||
tracing-subscriber = {version = "0.3", features = ["env-filter"]}
|
||||
|
||||
webbrowser = "0.7"
|
||||
@@ -1,26 +1,52 @@
|
||||
use eyre::Result;
|
||||
use futures::TryFutureExt;
|
||||
use paris::*;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_futures::WithSubscriber;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
mod subcommands;
|
||||
#[macro_use]
|
||||
mod util;
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
mod subcommands;
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// The official Modrinth CLI
|
||||
pub struct Args {
|
||||
#[argh(subcommand)]
|
||||
pub subcommand: subcommands::SubCommand,
|
||||
pub subcommand: subcommands::Subcommand,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
#[tracing::instrument]
|
||||
fn main() -> Result<()> {
|
||||
let args = argh::from_env::<Args>();
|
||||
pretty_env_logger::formatted_builder()
|
||||
.filter_module("theseus", log::LevelFilter::Info)
|
||||
.target(pretty_env_logger::env_logger::Target::Stderr)
|
||||
|
||||
color_eyre::install()?;
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new("info"))?;
|
||||
|
||||
let format = fmt::layer()
|
||||
.without_time()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_target(false)
|
||||
.compact();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(format)
|
||||
.with(filter)
|
||||
.with(ErrorLayer::default())
|
||||
.init();
|
||||
|
||||
args.dispatch()
|
||||
.inspect_err(|_| error!("An error has occurred!\n"))
|
||||
.await
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(
|
||||
async move {
|
||||
args.dispatch()
|
||||
.inspect_err(|_| error!("An error has occurred!\n"))
|
||||
.await
|
||||
}
|
||||
.with_current_subscriber(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
use eyre::Result;
|
||||
|
||||
mod profile;
|
||||
mod user;
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand)]
|
||||
pub enum SubCommand {
|
||||
pub enum Subcommand {
|
||||
Profile(profile::ProfileCommand),
|
||||
User(user::UserCommand),
|
||||
}
|
||||
|
||||
impl crate::Args {
|
||||
pub async fn dispatch(&self) -> Result<()> {
|
||||
match self.subcommand {
|
||||
SubCommand::Profile(ref cmd) => cmd.dispatch(self).await,
|
||||
}
|
||||
dispatch!(self.subcommand, (self) => {
|
||||
Subcommand::Profile,
|
||||
Subcommand::User
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
//! Profile management subcommand
|
||||
use crate::util::{
|
||||
confirm_async, prompt_async, select_async, table_path_display,
|
||||
confirm_async, prompt_async, select_async, table, table_path_display,
|
||||
};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use eyre::{ensure, Result};
|
||||
use futures::prelude::*;
|
||||
use paris::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tabled::{Table, Tabled};
|
||||
use tabled::Tabled;
|
||||
use theseus::prelude::*;
|
||||
use tokio::fs;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand, name = "profile")]
|
||||
/// profile management
|
||||
/// manage Minecraft instances
|
||||
pub struct ProfileCommand {
|
||||
#[argh(subcommand)]
|
||||
action: ProfileSubcommand,
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand)]
|
||||
pub enum ProfileSubcommand {
|
||||
Add(ProfileAdd),
|
||||
@@ -31,7 +30,7 @@ pub enum ProfileSubcommand {
|
||||
Run(ProfileRun),
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand, name = "add")]
|
||||
/// add a new profile to Theseus
|
||||
pub struct ProfileAdd {
|
||||
@@ -71,7 +70,7 @@ impl ProfileAdd {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand, name = "init")]
|
||||
/// create a new profile and manage it with Theseus
|
||||
pub struct ProfileInit {
|
||||
@@ -260,7 +259,7 @@ impl ProfileInit {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// list all managed profiles
|
||||
#[argh(subcommand, name = "list")]
|
||||
pub struct ProfileList {}
|
||||
@@ -311,16 +310,15 @@ impl ProfileList {
|
||||
_args: &crate::Args,
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
let profiles = profiles.0.iter().map(|(path, prof)| {
|
||||
let profiles = profile::list().await?;
|
||||
let rows = profiles.iter().map(|(path, prof)| {
|
||||
prof.as_ref().map_or_else(
|
||||
|| ProfileRow::from(path.as_path()),
|
||||
ProfileRow::from,
|
||||
)
|
||||
});
|
||||
|
||||
let table = Table::new(profiles).with(tabled::Style::psql()).with(
|
||||
let table = table(rows).with(
|
||||
tabled::Modify::new(tabled::Column(1..=1))
|
||||
.with(tabled::MaxWidth::wrapping(40)),
|
||||
);
|
||||
@@ -330,7 +328,7 @@ impl ProfileList {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// unmanage a profile
|
||||
#[argh(subcommand, name = "remove")]
|
||||
pub struct ProfileRemove {
|
||||
@@ -364,7 +362,7 @@ impl ProfileRemove {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// run a profile
|
||||
#[argh(subcommand, name = "run")]
|
||||
pub struct ProfileRun {
|
||||
@@ -372,18 +370,9 @@ pub struct ProfileRun {
|
||||
/// the profile to run
|
||||
profile: PathBuf,
|
||||
|
||||
// TODO: auth
|
||||
#[argh(option, short = 't')]
|
||||
/// the Minecraft token to use for player login. Should be replaced by auth when that is a thing.
|
||||
token: String,
|
||||
|
||||
#[argh(option, short = 'n')]
|
||||
/// the uername to use for running the game
|
||||
name: String,
|
||||
|
||||
#[argh(option, short = 'i')]
|
||||
/// the account id to use for running the game
|
||||
id: Uuid,
|
||||
#[argh(option)]
|
||||
/// the user to authenticate with
|
||||
user: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
impl ProfileRun {
|
||||
@@ -400,11 +389,18 @@ impl ProfileRun {
|
||||
"Profile not managed by Theseus (if it exists, try using `profile add` first!)",
|
||||
);
|
||||
|
||||
let credentials = Credentials {
|
||||
id: self.id.clone(),
|
||||
username: self.name.clone(),
|
||||
access_token: self.token.clone(),
|
||||
};
|
||||
let id = future::ready(self.user.ok_or(()))
|
||||
.or_else(|_| async move {
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
|
||||
settings.default_user
|
||||
.ok_or(eyre::eyre!(
|
||||
"Could not find any users, please add one using the `user add` command."
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
let credentials = auth::refresh(id, false).await?;
|
||||
|
||||
let mut proc = profile::run(&path, &credentials).await?;
|
||||
profile::wait_for(&mut proc).await?;
|
||||
@@ -415,14 +411,14 @@ impl ProfileRun {
|
||||
}
|
||||
|
||||
impl ProfileCommand {
|
||||
pub async fn dispatch(&self, args: &crate::Args) -> Result<()> {
|
||||
match &self.action {
|
||||
ProfileSubcommand::Add(ref cmd) => cmd.run(args, self).await,
|
||||
ProfileSubcommand::Init(ref cmd) => cmd.run(args, self).await,
|
||||
ProfileSubcommand::List(ref cmd) => cmd.run(args, self).await,
|
||||
ProfileSubcommand::Remove(ref cmd) => cmd.run(args, self).await,
|
||||
ProfileSubcommand::Run(ref cmd) => cmd.run(args, self).await,
|
||||
}
|
||||
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
||||
dispatch!(&self.action, (args, self) => {
|
||||
ProfileSubcommand::Add,
|
||||
ProfileSubcommand::Init,
|
||||
ProfileSubcommand::List,
|
||||
ProfileSubcommand::Remove,
|
||||
ProfileSubcommand::Run
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
178
theseus_cli/src/subcommands/user.rs
Normal file
178
theseus_cli/src/subcommands/user.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
//! User management subcommand
|
||||
use crate::util::{confirm_async, table};
|
||||
use eyre::Result;
|
||||
use paris::*;
|
||||
use tabled::Tabled;
|
||||
use theseus::prelude::*;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand, name = "user")]
|
||||
/// manage Minecraft accounts
|
||||
pub struct UserCommand {
|
||||
#[argh(subcommand)]
|
||||
action: UserSubcommand,
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand)]
|
||||
pub enum UserSubcommand {
|
||||
Add(UserAdd),
|
||||
List(UserList),
|
||||
Remove(UserRemove),
|
||||
SetDefault(UserDefault),
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// add a new user to Theseus
|
||||
#[argh(subcommand, name = "add")]
|
||||
pub struct UserAdd {
|
||||
#[argh(option)]
|
||||
/// the browser to authenticate using
|
||||
browser: Option<webbrowser::Browser>,
|
||||
}
|
||||
|
||||
impl UserAdd {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &UserCommand,
|
||||
) -> Result<()> {
|
||||
info!("Adding new user account to Theseus");
|
||||
info!("A browser window will now open, follow the login flow there.");
|
||||
|
||||
let (tx, rx) = oneshot::channel::<url::Url>();
|
||||
let flow = tokio::spawn(auth::authenticate(tx));
|
||||
|
||||
let url = rx.await?;
|
||||
match self.browser {
|
||||
Some(browser) => webbrowser::open_browser(browser, url.as_str()),
|
||||
None => webbrowser::open(url.as_str()),
|
||||
}?;
|
||||
|
||||
let credentials = flow.await??;
|
||||
State::sync().await?;
|
||||
success!("Logged in user {}.", credentials.username);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// list all known users
|
||||
#[argh(subcommand, name = "list")]
|
||||
pub struct UserList {}
|
||||
|
||||
#[derive(Tabled)]
|
||||
struct UserRow<'a> {
|
||||
username: &'a str,
|
||||
id: uuid::Uuid,
|
||||
default: bool,
|
||||
}
|
||||
|
||||
impl<'a> UserRow<'a> {
|
||||
pub fn from(
|
||||
credentials: &'a Credentials,
|
||||
default: Option<uuid::Uuid>,
|
||||
) -> Self {
|
||||
Self {
|
||||
username: &credentials.username,
|
||||
id: credentials.id,
|
||||
default: Some(credentials.id) == default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserList {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &UserCommand,
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let default = state.settings.read().await.default_user;
|
||||
|
||||
let users = auth::users().await?;
|
||||
let rows = users.iter().map(|user| UserRow::from(user, default));
|
||||
|
||||
let table = table(rows);
|
||||
println!("{table}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// remove a user
|
||||
#[argh(subcommand, name = "remove")]
|
||||
pub struct UserRemove {
|
||||
/// the user to remove
|
||||
#[argh(positional)]
|
||||
user: uuid::Uuid,
|
||||
}
|
||||
|
||||
impl UserRemove {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &UserCommand,
|
||||
) -> Result<()> {
|
||||
info!("Removing user {}", self.user.as_hyphenated());
|
||||
|
||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||
if !auth::has_user(self.user).await? {
|
||||
warn!("Profile was not managed by Theseus!");
|
||||
} else {
|
||||
auth::remove_user(self.user).await?;
|
||||
State::sync().await?;
|
||||
success!("User removed!");
|
||||
}
|
||||
} else {
|
||||
error!("Aborted!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// set the default user
|
||||
#[argh(subcommand, name = "set-default")]
|
||||
pub struct UserDefault {
|
||||
/// the user to set as default
|
||||
#[argh(positional)]
|
||||
user: uuid::Uuid,
|
||||
}
|
||||
|
||||
impl UserDefault {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &UserCommand,
|
||||
) -> Result<()> {
|
||||
info!("Setting user {} as default", self.user.as_hyphenated());
|
||||
|
||||
// TODO: settings API
|
||||
let state: std::sync::Arc<State> = State::get().await?;
|
||||
let mut settings = state.settings.write().await;
|
||||
|
||||
if settings.default_user == Some(self.user) {
|
||||
warn!("User is already the default!");
|
||||
} else {
|
||||
settings.default_user = Some(self.user);
|
||||
success!("User set as default!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl UserCommand {
|
||||
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
||||
dispatch!(&self.action, (args, self) => {
|
||||
UserSubcommand::Add,
|
||||
UserSubcommand::List,
|
||||
UserSubcommand::Remove,
|
||||
UserSubcommand::SetDefault
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
use eyre::Result;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
use tabled::{Table, Tabled};
|
||||
|
||||
// TODO: make primarily async to avoid copies
|
||||
|
||||
@@ -56,7 +57,11 @@ pub async fn confirm_async(prompt: String, default: bool) -> Result<bool> {
|
||||
tokio::task::spawn_blocking(move || confirm(&prompt, default)).await?
|
||||
}
|
||||
|
||||
// Table display helpers
|
||||
// Table helpers
|
||||
pub fn table<T: Tabled>(rows: impl IntoIterator<Item = T>) -> Table {
|
||||
Table::new(rows).with(tabled::Style::psql())
|
||||
}
|
||||
|
||||
pub fn table_path_display(path: &Path) -> String {
|
||||
let mut res = path.display().to_string();
|
||||
|
||||
@@ -67,6 +72,20 @@ pub fn table_path_display(path: &Path) -> String {
|
||||
res
|
||||
}
|
||||
|
||||
// Dispatch macros
|
||||
macro_rules! dispatch {
|
||||
($on:expr, $args:tt => {$($option:path),+}) => {
|
||||
match $on {
|
||||
$($option (ref cmd) => dispatch!(@apply cmd => $args)),+
|
||||
}
|
||||
};
|
||||
|
||||
(@apply $cmd:expr => ($($args:expr),*)) => {{
|
||||
use tracing_futures::WithSubscriber;
|
||||
$cmd.run($($args),*).with_current_subscriber().await
|
||||
}};
|
||||
}
|
||||
|
||||
// Internal helpers
|
||||
fn print_prompt(prompt: &str) {
|
||||
println!(
|
||||
|
||||
Reference in New Issue
Block a user