mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user