feat: instance sharing thru shared-instances service (#6569)

* feat: implement instance share page + search_users backend call

* feat: invite players modal

* feat: use tanstack queries for friends sync across app pages

* feat: base shared instances implementation

* fix: admon style

* feat: impl instance admonitions like server panel

* fix: impl get + del usage

* feat: support modpack links

* feat: invite notif accepting

* fix: lint + fmt

* feat: impl install to play

* feat: impl usage of UpdateToPlayModal

* feat: warnings on deleting/disabling shared-instance version content

* fix: send instance name

* feat: align with backend

* feat: shared instances qa

* feat: wrong account protection

* feat: qa

* fix: smartly apply updates

* fix: install bug

* fix: 401/404 differentiation

* fix: fmt+prepr

* feat: qa

* feat: qa

* fix: signing out messes up revoke/deleted checks

* feat: qa

* fix: fmt + lint

* feat: lock content if part of shared instance

* fix: lint

* [do not merge] feat: rough invite links impl temp (#6666)

* fix: wrong cmd

* feat: invite page

* fix: server-manager DTO mismatch

* fix: drop anonymous invite link acceptance

* refactor: structured shared-instance unavailable errors

* refactor: centralise error presentations

* refactor: dedupe shared instance diff detection

* fix: logging in reqwests

* refactor: move app.vue shared instances into handler

* refactor: break up Share.vue

* refactor: split up shared instances state outside of instance index

* refactor: dedicated shared instances install/update modals + split up page

* refactor: centralized managed content

* refactor: split up install shared to own runner + shared.rs split up

* refactor: dedupe sql for instance metadata enrichmnt

* refactor: friends composable + dedupe friends logic across usages

* chore: reduced unused code

* fix: align with backend

* fix: lint

* fix: file sha changes

* fix: invite links not working due to icon signed

* feat: qa

* feat: reporting frontend dummy

* fix: try use header

* remove: file hash field

* fix: pin box

* feat: malware warning for shared instances

* fix: cache rule

* feat: config files syncing

* feat: disable config sharing

* fix: header

* fix: use mark ready

* fix: dont cause push update for configs

* fix: lint

* feat: sharing page in settings

* feat: move config + change flow

* fix: qa

* fix: lint prepr

* feat: proxy file upload thru shared instances backend

* fix: use collapisible

* fix: push config

* fix: config

* feat: swap out sign in modal for new one

* fix: report flow

* fix: exclude configs.zip from external warnings

* fix: nuxi init

* fix: config bundle downloading

* fix: error notif

* fix: polling

* fix: qa

* fix: lint + prepr

* feat: shared instances moderation frontend + hook up report flow

* fix: report copy

* fix: lint

* fix: lint

* fix: modrinth ids being undefined

* feat: instance quarantining

* fix: prepr + fmt

* fix: quarantined -> locked terminology

* fix: missing endpoint impls + fmt

* fix: missing api in build.rs

* fix: share tab jittery

* fix: fmt

*PT bug

* fix: invites count as users even if pending

* fix: prepr

* fix: invite page owner in users list

* fix: lint

* fix: qa

* fix: lint

* fix: members stale not clearing

* fix: invite use joined_at field

* fix: lint

* fix: qa

---------

Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-07-24 13:06:38 +00:00
committed by GitHub
co-authored by sychic
parent 2e0d797bb0
commit e58af98f21
269 changed files with 18202 additions and 2579 deletions
+30 -2
View File
@@ -55,7 +55,6 @@ fn main() {
InlinedPlugin::new()
.commands(&[
"get_importable_instances",
"import_instance",
"is_valid_importable_instance",
"get_default_launcher_path",
])
@@ -148,6 +147,11 @@ fn main() {
"install_get_modpack_preview",
"install_create_instance",
"install_create_modpack_instance",
"install_get_shared_instance_preview",
"install_accept_shared_instance_invite",
"install_get_shared_instance_update_preview",
"install_shared_instance",
"install_update_shared_instance",
"install_import_instance",
"install_duplicate_instance",
"install_existing_instance",
@@ -176,6 +180,14 @@ fn main() {
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"reports",
InlinedPlugin::new()
.commands(&["reports_create"])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"instance",
InlinedPlugin::new()
@@ -211,6 +223,15 @@ fn main() {
"instance_kill",
"instance_edit",
"instance_edit_icon",
"instance_share_can_current_user_use",
"instance_share_get_users",
"instance_share_invite_users",
"instance_share_create_invite_link",
"instance_share_remove_users",
"instance_share_get_publish_preview",
"instance_share_publish",
"instance_share_unlink",
"instance_share_unpublish",
"instance_export_mrpack",
"instance_get_pack_export_candidates",
])
@@ -252,6 +273,14 @@ fn main() {
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"users",
InlinedPlugin::new()
.commands(&["search_user"])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"utils",
InlinedPlugin::new()
@@ -276,7 +305,6 @@ fn main() {
.commands(&[
"init_ads_window",
"hide_ads_window",
"scroll_ads_window",
"show_ads_window",
"show_ads_consent_overlay",
"show_ads_consent_preferences",
+2
View File
@@ -102,11 +102,13 @@
"instance:default",
"install:default",
"process:default",
"reports:default",
"cache:default",
"files:default",
"settings:default",
"shortcuts:default",
"tags:default",
"users:default",
"utils:default",
"ads:default",
"friends:default",
+70
View File
@@ -6,6 +6,10 @@ use theseus::data::ModLoader;
use theseus::install::{
InstallJobSnapshot, InstallModpackPreview, InstallPostInstallEdit,
};
use theseus::instance::{
SharedInstanceInstallPreview, SharedInstanceInviteInstallPreview,
SharedInstanceUpdatePreview,
};
use theseus::pack::import::ImportLauncherType;
use theseus::pack::install_from::CreatePackLocation;
use uuid::Uuid;
@@ -16,6 +20,11 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
install_get_modpack_preview,
install_create_instance,
install_create_modpack_instance,
install_get_shared_instance_preview,
install_accept_shared_instance_invite,
install_get_shared_instance_update_preview,
install_shared_instance,
install_update_shared_instance,
install_import_instance,
install_duplicate_instance,
install_existing_instance,
@@ -101,6 +110,67 @@ pub async fn install_create_modpack_instance(
.await?)
}
#[tauri::command]
pub async fn install_get_shared_instance_preview(
shared_instance_id: String,
name: String,
) -> Result<SharedInstanceInstallPreview> {
Ok(theseus::instance::get_shared_instance_install_preview(
&shared_instance_id,
name,
)
.await?)
}
#[tauri::command]
pub async fn install_accept_shared_instance_invite(
invite_id: String,
) -> Result<SharedInstanceInviteInstallPreview> {
Ok(
theseus::instance::accept_shared_instance_invite_for_install(
&invite_id,
)
.await?,
)
}
#[tauri::command]
pub async fn install_get_shared_instance_update_preview(
instance_id: String,
) -> Result<Option<SharedInstanceUpdatePreview>> {
Ok(
theseus::instance::get_shared_instance_update_preview(&instance_id)
.await?,
)
}
#[tauri::command]
pub async fn install_shared_instance(
shared_instance_id: String,
name: String,
manager_id: Option<String>,
server_manager_name: Option<String>,
server_manager_icon_url: Option<String>,
instance_icon_url: Option<String>,
) -> Result<InstallJobSnapshot> {
Ok(theseus::instance::install_shared_instance(
&shared_instance_id,
name,
manager_id,
server_manager_name,
server_manager_icon_url,
instance_icon_url,
)
.await?)
}
#[tauri::command]
pub async fn install_update_shared_instance(
instance_id: String,
) -> Result<InstallJobSnapshot> {
Ok(theseus::instance::update_shared_instance(&instance_id).await?)
}
#[tauri::command]
pub async fn install_import_instance(
launcher_type: ImportLauncherType,
+147 -19
View File
@@ -10,6 +10,8 @@ use theseus::data::{
EditInstance as CoreEditInstance, InstanceInstallCandidate,
InstanceInstallTarget, InstanceLaunchOverridesPatch,
InstanceLink as CoreInstanceLink, InstanceMetadata, LinkedModpackInfo,
SharedInstanceAttachment as CoreSharedInstanceAttachment,
SharedInstanceRole,
};
use theseus::instance::InstallProjectWithDependenciesRequest;
use theseus::instance::QuickPlayType;
@@ -50,6 +52,15 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
instance_kill,
instance_edit,
instance_edit_icon,
instance_share_can_current_user_use,
instance_share_get_users,
instance_share_invite_users,
instance_share_create_invite_link,
instance_share_remove_users,
instance_share_get_publish_preview,
instance_share_publish,
instance_share_unlink,
instance_share_unpublish,
instance_export_mrpack,
instance_get_pack_export_candidates,
])
@@ -70,6 +81,8 @@ pub struct Instance {
pub loader_version: Option<String>,
pub groups: Vec<String>,
pub link: Option<InstanceLink>,
pub shared_instance: Option<SharedInstanceAttachment>,
pub quarantined: bool,
pub update_channel: ReleaseChannel,
pub created: chrono::DateTime<chrono::Utc>,
pub modified: chrono::DateTime<chrono::Utc>,
@@ -115,10 +128,40 @@ pub enum InstanceLink {
active_instance_id: Option<String>,
},
SharedInstance {
shared_instance_id: String,
modpack_project_id: Option<String>,
modpack_version_id: Option<String>,
},
}
#[derive(Serialize, Debug, Clone)]
pub struct SharedInstanceAttachment {
pub id: String,
pub role: SharedInstanceRole,
pub manager_id: Option<String>,
pub server_manager_name: Option<String>,
pub server_manager_icon_url: Option<String>,
pub linked_user_id: Option<String>,
pub status: String,
pub applied_version: Option<i32>,
pub latest_version: Option<i32>,
}
impl From<CoreSharedInstanceAttachment> for SharedInstanceAttachment {
fn from(attachment: CoreSharedInstanceAttachment) -> Self {
Self {
id: attachment.id.to_string(),
role: attachment.role,
manager_id: attachment.manager_id,
server_manager_name: attachment.server_manager_name,
server_manager_icon_url: attachment.server_manager_icon_url,
linked_user_id: attachment.linked_user_id,
status: attachment.status.as_str().to_string(),
applied_version: attachment.applied_version,
latest_version: attachment.latest_version,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct EditInstance {
pub name: Option<String>,
@@ -201,6 +244,8 @@ impl From<InstanceMetadata> for Instance {
loader_version: metadata.applied_content_set.loader_version,
groups: metadata.groups,
link: InstanceLink::from_core(metadata.link),
shared_instance: metadata.shared_instance.map(Into::into),
quarantined: metadata.quarantined,
update_channel: metadata.instance.update_channel,
created: metadata.instance.created,
modified: metadata.instance.modified,
@@ -268,11 +313,13 @@ impl InstanceLink {
.collect(),
active_instance_id: active_instance_id.map(|id| id.to_string()),
}),
CoreInstanceLink::SharedInstance { shared_instance_id } => {
Some(Self::SharedInstance {
shared_instance_id: shared_instance_id.to_string(),
})
}
CoreInstanceLink::SharedInstance {
modpack_project_id,
modpack_version_id,
} => Some(Self::SharedInstance {
modpack_project_id,
modpack_version_id,
}),
}
}
@@ -345,19 +392,13 @@ impl InstanceLink {
})
.transpose()?,
}),
Self::SharedInstance { shared_instance_id } => {
Ok(CoreInstanceLink::SharedInstance {
shared_instance_id: shared_instance_id.parse().map_err(
|err| {
theseus::Error::from(
theseus::ErrorKind::InputError(format!(
"Invalid shared instance id: {err}"
)),
)
},
)?,
})
}
Self::SharedInstance {
modpack_project_id,
modpack_version_id,
} => Ok(CoreInstanceLink::SharedInstance {
modpack_project_id,
modpack_version_id,
}),
}
}
}
@@ -744,3 +785,90 @@ pub async fn instance_edit_icon(
theseus::instance::edit_icon(instance_id, icon_path).await?;
Ok(())
}
#[tauri::command]
pub async fn instance_share_can_current_user_use() -> Result<bool> {
Ok(theseus::instance::can_active_user_use_shared_instances().await?)
}
#[tauri::command]
pub async fn instance_share_get_users(
instance_id: &str,
) -> Result<theseus::instance::SharedInstanceUsers> {
Ok(theseus::instance::get_shared_instance_users(instance_id).await?)
}
#[tauri::command]
pub async fn instance_share_invite_users(
instance_id: &str,
user_ids: Vec<String>,
) -> Result<theseus::instance::SharedInstanceUsers> {
Ok(
theseus::instance::invite_shared_instance_users(instance_id, user_ids)
.await?,
)
}
#[tauri::command]
pub async fn instance_share_create_invite_link(
instance_id: &str,
max_age_seconds: Option<i32>,
max_uses: Option<i32>,
replace_invite_id: Option<String>,
) -> Result<theseus::instance::SharedInstanceInviteLink> {
Ok(theseus::instance::create_shared_instance_invite_link(
instance_id,
max_age_seconds,
max_uses,
replace_invite_id,
)
.await?)
}
#[tauri::command]
pub async fn instance_share_remove_users(
instance_id: &str,
user_ids: Vec<String>,
has_pending_recipients: bool,
) -> Result<theseus::instance::SharedInstanceUsers> {
Ok(theseus::instance::remove_shared_instance_users(
instance_id,
user_ids,
has_pending_recipients,
)
.await?)
}
#[tauri::command]
pub async fn instance_share_get_publish_preview(
instance_id: &str,
) -> Result<Option<theseus::instance::SharedInstancePublishPreview>> {
Ok(
theseus::instance::get_shared_instance_publish_preview(instance_id)
.await?,
)
}
#[tauri::command]
pub async fn instance_share_publish(
instance_id: &str,
config_paths: Vec<String>,
) -> Result<SharedInstanceAttachment> {
Ok(
theseus::instance::publish_shared_instance(instance_id, config_paths)
.await?
.into(),
)
}
#[tauri::command]
pub async fn instance_share_unlink(instance_id: &str) -> Result<()> {
theseus::instance::unlink_shared_instance(instance_id).await?;
Ok(())
}
#[tauri::command]
pub async fn instance_share_unpublish(instance_id: &str) -> Result<()> {
theseus::instance::unpublish_shared_instance(instance_id).await?;
Ok(())
}
+14 -1
View File
@@ -12,9 +12,11 @@ pub mod metadata;
pub mod minecraft_skins;
pub mod mr_auth;
pub mod process;
pub mod reports;
pub mod settings;
pub mod shortcuts;
pub mod tags;
pub mod users;
pub mod utils;
pub mod ads;
@@ -85,9 +87,20 @@ macro_rules! impl_serialize {
TheseusSerializableError::Theseus(theseus_error) => {
$crate::error::display_tracing_error(theseus_error);
let mut state = serializer.serialize_struct("Theseus", 2)?;
let unavailable_reason = match theseus_error.raw.as_ref() {
theseus::ErrorKind::SharedInstanceUnavailable(reason) => Some(reason),
_ => None,
};
let mut state = serializer.serialize_struct(
"Theseus",
if unavailable_reason.is_some() { 4 } else { 2 },
)?;
state.serialize_field("field_name", "Theseus")?;
state.serialize_field("message", &theseus_error.to_string())?;
if let Some(reason) = unavailable_reason {
state.serialize_field("code", "shared_instance_unavailable")?;
state.serialize_field("reason", reason)?;
}
state.end()
}
$(
+2 -1
View File
@@ -22,6 +22,7 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
#[tauri::command]
pub async fn modrinth_login<R: Runtime>(
app: tauri::AppHandle<R>,
flow: mr_auth::ModrinthAuthFlow,
) -> Result<ModrinthCredentials> {
let (auth_code_recv_socket_tx, auth_code_recv_socket) = oneshot::channel();
let auth_code = tokio::spawn(oauth_utils::auth_code_reply::listen(
@@ -32,7 +33,7 @@ pub async fn modrinth_login<R: Runtime>(
let auth_request_uri = format!(
"{}?launcher=true&ipver={}&port={}",
mr_auth::authenticate_begin_flow(),
mr_auth::authenticate_begin_flow(flow),
if auth_code_recv_socket.is_ipv4() {
"4"
} else {
+15
View File
@@ -0,0 +1,15 @@
use crate::api::Result;
use theseus::reports::{CreateReportRequest, CreateReportResponse};
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("reports")
.invoke_handler(tauri::generate_handler![reports_create])
.build()
}
#[tauri::command]
pub async fn reports_create(
request: CreateReportRequest,
) -> Result<CreateReportResponse> {
Ok(theseus::reports::create_report(request).await?)
}
+13
View File
@@ -0,0 +1,13 @@
use crate::api::Result;
use theseus::users::SearchUser;
#[tauri::command]
pub async fn search_user(query: &str) -> Result<Vec<SearchUser>> {
Ok(theseus::users::search_user(query).await?)
}
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("users")
.invoke_handler(tauri::generate_handler![search_user])
.build()
}
+5 -3
View File
@@ -152,7 +152,7 @@ fn main() {
builder = builder
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
if let Some(payload) = args.get(1) {
tracing::info!("Handling deep link from arg {payload}");
tracing::info!("Handling command-line deep link");
let payload = payload.clone();
tauri::async_runtime::spawn(api::utils::handle_command(
payload,
@@ -197,7 +197,7 @@ fn main() {
.unwrap_or(request);
tauri::async_runtime::spawn(async move {
tracing::info!("Handling deep link {actual_request}");
tracing::info!("Handling macOS deep link");
let mut payload = mtx_copy_copy.lock().await;
if payload.is_none() {
@@ -213,7 +213,7 @@ fn main() {
#[cfg(not(target_os = "macos"))]
app.listen("deep-link://new-url", |url| {
let payload = url.payload().to_owned();
tracing::info!("Handling deep link {payload}");
tracing::info!("Handling deep link");
tauri::async_runtime::spawn(api::utils::handle_command(
payload,
));
@@ -240,9 +240,11 @@ fn main() {
.plugin(api::metadata::init())
.plugin(api::minecraft_skins::init())
.plugin(api::process::init())
.plugin(api::reports::init())
.plugin(api::settings::init())
.plugin(api::shortcuts::init())
.plugin(api::tags::init())
.plugin(api::users::init())
.plugin(api::utils::init())
.plugin(api::cache::init())
.plugin(api::files::init())