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
@@ -43,6 +43,7 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
public readonly launchermeta!: InferredClientModules['launchermeta']
public readonly paper!: InferredClientModules['paper']
public readonly purpur!: InferredClientModules['purpur']
public readonly sharedinstances!: InferredClientModules['sharedinstances']
constructor(config: ClientConfig) {
super()
@@ -50,6 +51,7 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
timeout: 10000,
labrinthBaseUrl: 'https://api.modrinth.com',
archonBaseUrl: 'https://archon.modrinth.com',
sharedInstancesBaseUrl: 'https://shared-instances.modrinth.com',
...config,
}
this.features = config.features ?? []
@@ -123,6 +125,8 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
baseUrl = this.resolveBaseUrl(this.config.labrinthBaseUrl!)
} else if (options.api === 'archon') {
baseUrl = this.resolveBaseUrl(this.config.archonBaseUrl!)
} else if (options.api === 'sharedinstances') {
baseUrl = this.resolveBaseUrl(this.config.sharedInstancesBaseUrl!)
} else {
baseUrl = options.api
}
@@ -170,6 +174,8 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
baseUrl = this.resolveBaseUrl(this.config.labrinthBaseUrl!)
} else if (options.api === 'archon') {
baseUrl = this.resolveBaseUrl(this.config.archonBaseUrl!)
} else if (options.api === 'sharedinstances') {
baseUrl = this.resolveBaseUrl(this.config.sharedInstancesBaseUrl!)
} else {
baseUrl = options.api
}
+8
View File
@@ -57,6 +57,10 @@ import { MclogsInsightsV1Module } from './mclogs/insights/v1'
import { MclogsLogsV1Module } from './mclogs/logs/v1'
import { PaperVersionsV3Module } from './paper/v3'
import { PurpurVersionsV2Module } from './purpur/v2'
import { SharedInstancesInstancesV1Module } from './shared-instances/instances/v1'
import { SharedInstancesInvitesV1Module } from './shared-instances/invites/v1'
import { SharedInstancesModerationV1Module } from './shared-instances/moderation/v1'
import { SharedInstancesUsersV1Module } from './shared-instances/users/v1'
type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule
@@ -128,6 +132,10 @@ export const MODULE_REGISTRY = {
labrinth_versions_v3: LabrinthVersionsV3Module,
paper_versions_v3: PaperVersionsV3Module,
purpur_versions_v2: PurpurVersionsV2Module,
sharedinstances_invites_v1: SharedInstancesInvitesV1Module,
sharedinstances_instances_v1: SharedInstancesInstancesV1Module,
sharedinstances_moderation_v1: SharedInstancesModerationV1Module,
sharedinstances_users_v1: SharedInstancesUsersV1Module,
} as const satisfies Record<string, ModuleConstructor>
export type ModuleID = keyof typeof MODULE_REGISTRY
@@ -21,7 +21,7 @@ function buildImageQueryParams(
params.thread_message_id = target.thread_message_id
break
case 'report':
params.report_id = target.report_id
if (target.report_id) params.report_id = target.report_id
break
}
return params
@@ -434,7 +434,7 @@ export namespace Labrinth {
| { context: 'project'; project_id: string }
| { context: 'version'; version_id: string }
| { context: 'thread_message'; thread_message_id: string }
| { context: 'report'; report_id: string }
| { context: 'report'; report_id: string | null }
)
export type UploadedImageFor<C extends ImageUploadContext> = Extract<
@@ -450,7 +450,7 @@ export namespace Labrinth {
| { context: 'project'; project_id: string }
| { context: 'version'; version_id: string }
| { context: 'thread_message'; thread_message_id: string }
| { context: 'report'; report_id: string }
| { context: 'report'; report_id?: string }
}
}
@@ -1917,13 +1917,14 @@ export namespace Labrinth {
export namespace Reports {
export namespace v3 {
export type ItemType = 'project' | 'version' | 'user' | 'unknown'
export type ItemType = 'project' | 'version' | 'user' | 'shared-instance' | 'unknown'
export type Report = {
id: string
report_type: string
item_id: string
item_type: ItemType
shared_instance_version_id?: number
reporter: string
body: string
created: string
@@ -0,0 +1,66 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { SharedInstances } from '../types'
export class SharedInstancesInstancesV1Module extends AbstractModule {
public getModuleID(): string {
return 'sharedinstances_instances_v1'
}
public async get(instanceId: string): Promise<SharedInstances.Instances.v1.Instance> {
return this.client.request<SharedInstances.Instances.v1.Instance>(
`/instances/${encodeURIComponent(instanceId)}`,
{
api: 'sharedinstances',
version: 1,
method: 'GET',
},
)
}
public async getForUser(userId: string): Promise<string[]> {
return this.client.request<string[]>('/instances', {
api: 'sharedinstances',
version: 1,
method: 'GET',
params: { user: userId },
})
}
public async getUsers(instanceId: string): Promise<SharedInstances.Instances.v1.InstanceUsers> {
return this.client.request<SharedInstances.Instances.v1.InstanceUsers>(
`/instances/${encodeURIComponent(instanceId)}/users`,
{
api: 'sharedinstances',
version: 1,
method: 'GET',
},
)
}
public async getLatestVersion(
instanceId: string,
): Promise<SharedInstances.Instances.v1.InstanceVersion> {
return this.client.request<SharedInstances.Instances.v1.InstanceVersion>(
`/instances/${encodeURIComponent(instanceId)}/versions`,
{
api: 'sharedinstances',
version: 1,
method: 'GET',
},
)
}
public async getVersion(
instanceId: string,
version: number,
): Promise<SharedInstances.Instances.v1.InstanceVersion> {
return this.client.request<SharedInstances.Instances.v1.InstanceVersion>(
`/instances/${encodeURIComponent(instanceId)}/versions/${version}`,
{
api: 'sharedinstances',
version: 1,
method: 'GET',
},
)
}
}
@@ -0,0 +1,21 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { SharedInstances } from '../types'
export class SharedInstancesInvitesV1Module extends AbstractModule {
public getModuleID(): string {
return 'sharedinstances_invites_v1'
}
public async get(inviteId: string): Promise<SharedInstances.Invites.v1.Invite> {
return this.client.request<SharedInstances.Invites.v1.Invite>(
`/invites/${encodeURIComponent(inviteId)}`,
{
api: 'sharedinstances',
version: 1,
method: 'GET',
skipAuth: true,
retry: false,
},
)
}
}
@@ -0,0 +1,41 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { SharedInstances } from '../types'
export class SharedInstancesModerationV1Module extends AbstractModule {
public getModuleID(): string {
return 'sharedinstances_moderation_v1'
}
public async blacklistUsers(
request: SharedInstances.Moderation.v1.BlacklistUserRequest,
): Promise<void> {
return this.client.request('/moderation/blacklist', {
api: 'sharedinstances',
version: 1,
method: 'POST',
body: request,
})
}
public async unblacklistUsers(
request: SharedInstances.Moderation.v1.BlacklistUserRequest,
): Promise<void> {
return this.client.request('/moderation/blacklist', {
api: 'sharedinstances',
version: 1,
method: 'DELETE',
body: request,
})
}
public async deleteFile(instanceId: string, version: number, fileName: string): Promise<void> {
return this.client.request(
`/moderation/instances/${encodeURIComponent(instanceId)}/versions/${version}/files/${encodeURIComponent(fileName)}`,
{
api: 'sharedinstances',
version: 1,
method: 'DELETE',
},
)
}
}
@@ -0,0 +1,101 @@
export namespace SharedInstances {
export namespace Invites {
export namespace v1 {
export type UserManager = {
id: string
name: string
type: 'user'
avatar?: string | null
}
export type ServerManager = {
name: string
type: 'server'
icon?: string | null
}
export type Manager = UserManager | ServerManager
export type InviteUser = {
id: string
name: string
avatar?: string | null
joined_at: string | null
}
export type Invite = {
instance_id: string
instance_name: string
instance_icon?: string | null
game_version: string
loader_version: string
managers: Manager[]
instance_users?: InviteUser[]
}
}
}
export namespace Instances {
export namespace v1 {
export type Instance = {
name: string
icon: string | null
quarantine: boolean
}
export type JoinType = 'owner' | 'invite' | 'link'
export type InstanceUser = {
id: string
joined_at: string | null
join_type: JoinType
last_played: string | null
}
export type InstanceUsers = {
users: InstanceUser[]
tokens: number
}
export type FileMetadata = {
path: string
hash: string
}
export type ExternalFile = {
file_name: string
file_type: string
url: string
file_size?: number
metadata?: FileMetadata[]
}
export type InstanceVersion = {
version: number
modrinth_ids?: string[]
ready: boolean
external_files: ExternalFile[]
modpack_id: string | null
game_version: string
loader: string
loader_version: string
}
}
}
export namespace Moderation {
export namespace v1 {
export type BlacklistUserRequest = {
user_ids: string[]
}
}
}
export namespace Users {
export namespace v1 {
export type BlacklistStatus = {
blacklisted: boolean
}
}
}
}
@@ -0,0 +1,21 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { SharedInstances } from '../types'
export class SharedInstancesUsersV1Module extends AbstractModule {
public getModuleID(): string {
return 'sharedinstances_users_v1'
}
public async getBlacklistStatus(
userId: string,
): Promise<SharedInstances.Users.v1.BlacklistStatus> {
return this.client.request<SharedInstances.Users.v1.BlacklistStatus>(
`/blacklist/${encodeURIComponent(userId)}`,
{
api: 'sharedinstances',
version: 1,
method: 'GET',
},
)
}
}
+1
View File
@@ -6,3 +6,4 @@ export * from './launcher-meta/types'
export * from './mclogs/types'
export * from './paper/types'
export * from './purpur/types'
export * from './shared-instances/types'
@@ -21,6 +21,8 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
baseUrl = this.resolveBaseUrl(this.config.labrinthBaseUrl!)
} else if (options.api === 'archon') {
baseUrl = this.resolveBaseUrl(this.config.archonBaseUrl!)
} else if (options.api === 'sharedinstances') {
baseUrl = this.resolveBaseUrl(this.config.sharedInstancesBaseUrl!)
} else {
baseUrl = options.api
}
+6
View File
@@ -50,6 +50,12 @@ export interface ClientConfig {
*/
archonBaseUrl?: BaseUrlConfig
/**
* Base URL for the Shared Instances API
* @default 'https://shared-instances.modrinth.com'
*/
sharedInstancesBaseUrl?: BaseUrlConfig
/**
* Default request timeout in milliseconds
* @default 10000
+1
View File
@@ -11,6 +11,7 @@ export type RequestOptions = {
* API to use for this request
* - 'labrinth': Main Modrinth API (resolves to labrinthBaseUrl)
* - 'archon': Modrinth Hosting API (resolves to archonBaseUrl)
* - 'sharedinstances': Shared Instances API (resolves to sharedInstancesBaseUrl)
* - string: Custom base URL (e.g., 'https://custom-api.com')
*/
api: 'labrinth' | 'archon' | string