Merge branch 'main' into cal/dev-124-project-validation
Signed-off-by: IMB11 <hendersoncal117@gmail.com>
This commit is contained in:
commit
fe3d360215
@ -2,5 +2,8 @@
|
|||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
||||||
|
|
||||||
|
[target.x86_64-pc-windows-msvc]
|
||||||
|
linker = "rust-lld"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
rustflags = ["--cfg", "tokio_unstable"]
|
||||||
|
|||||||
13
.github/workflows/daedalus-docker.yml
vendored
13
.github/workflows/daedalus-docker.yml
vendored
@ -22,23 +22,26 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Fetch docker metadata
|
- name: Fetch docker metadata
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/modrinth/daedalus
|
images: ghcr.io/modrinth/daedalus
|
||||||
- name: Login to GitHub Images
|
- name: Login to GitHub Images
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: docker_build
|
uses: docker/build-push-action@v6
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
with:
|
||||||
file: ./apps/daedalus_client/Dockerfile
|
file: ./apps/daedalus_client/Dockerfile
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=ghcr.io/modrinth/daedalus:main
|
||||||
|
cache-to: type=inline
|
||||||
|
|||||||
13
.github/workflows/labrinth-docker.yml
vendored
13
.github/workflows/labrinth-docker.yml
vendored
@ -20,23 +20,26 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Fetch docker metadata
|
- name: Fetch docker metadata
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/modrinth/labrinth
|
images: ghcr.io/modrinth/labrinth
|
||||||
- name: Login to GitHub Images
|
- name: Login to GitHub Images
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: docker_build
|
uses: docker/build-push-action@v6
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
with:
|
||||||
file: ./apps/labrinth/Dockerfile
|
file: ./apps/labrinth/Dockerfile
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=ghcr.io/modrinth/labrinth:main
|
||||||
|
cache-to: type=inline
|
||||||
|
|||||||
4
.github/workflows/theseus-build.yml
vendored
4
.github/workflows/theseus-build.yml
vendored
@ -75,7 +75,7 @@ jobs:
|
|||||||
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||||
chmod: 0755
|
chmod: 0755
|
||||||
|
|
||||||
- name: ⚙️ Set application version
|
- name: ⚙️ Set application version and environment
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
||||||
@ -84,6 +84,8 @@ jobs:
|
|||||||
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
||||||
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
|
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
|
||||||
|
|
||||||
|
cp packages/app-lib/.env.prod packages/app-lib/.env
|
||||||
|
|
||||||
- name: 💨 Setup Turbo cache
|
- name: 💨 Setup Turbo cache
|
||||||
uses: rharkor/caching-for-turbo@v1.8
|
uses: rharkor/caching-for-turbo@v1.8
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/turbo-ci.yml
vendored
6
.github/workflows/turbo-ci.yml
vendored
@ -52,7 +52,7 @@ jobs:
|
|||||||
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||||
# back to a cached cargo install
|
# back to a cached cargo install
|
||||||
- name: 🧰 Setup cargo-sqlx
|
- name: 🧰 Setup cargo-sqlx
|
||||||
uses: AlexTMjugador/cache-cargo-install-action@feat/features-support
|
uses: taiki-e/cache-cargo-install-action@v2
|
||||||
with:
|
with:
|
||||||
tool: sqlx-cli
|
tool: sqlx-cli
|
||||||
locked: false
|
locked: false
|
||||||
@ -74,6 +74,10 @@ jobs:
|
|||||||
cp .env.local .env
|
cp .env.local .env
|
||||||
sqlx database setup
|
sqlx database setup
|
||||||
|
|
||||||
|
- name: ⚙️ Set app environment
|
||||||
|
working-directory: packages/app-lib
|
||||||
|
run: cp .env.staging .env
|
||||||
|
|
||||||
- name: 🔍 Lint and test
|
- name: 🔍 Lint and test
|
||||||
run: pnpm run ci
|
run: pnpm run ci
|
||||||
|
|
||||||
|
|||||||
47
Cargo.lock
generated
47
Cargo.lock
generated
@ -5731,6 +5731,17 @@ dependencies = [
|
|||||||
"phf_shared 0.11.3",
|
"phf_shared 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros 0.12.1",
|
||||||
|
"phf_shared 0.12.1",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_codegen"
|
name = "phf_codegen"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -5781,6 +5792,16 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand 2.3.0",
|
||||||
|
"phf_shared 0.12.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_macros"
|
name = "phf_macros"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@ -5808,6 +5829,19 @@ dependencies = [
|
|||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.12.1",
|
||||||
|
"phf_shared 0.12.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_shared"
|
name = "phf_shared"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -5835,6 +5869,15 @@ dependencies = [
|
|||||||
"siphasher 1.0.1",
|
"siphasher 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.10"
|
version = "1.1.10"
|
||||||
@ -8930,6 +8973,7 @@ dependencies = [
|
|||||||
"data-url",
|
"data-url",
|
||||||
"dirs",
|
"dirs",
|
||||||
"discord-rich-presence",
|
"discord-rich-presence",
|
||||||
|
"dotenvy",
|
||||||
"dunce",
|
"dunce",
|
||||||
"either",
|
"either",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
@ -8945,6 +8989,7 @@ dependencies = [
|
|||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
"p256",
|
"p256",
|
||||||
"paste",
|
"paste",
|
||||||
|
"phf 0.12.1",
|
||||||
"png",
|
"png",
|
||||||
"quartz_nbt",
|
"quartz_nbt",
|
||||||
"quick-xml 0.37.5",
|
"quick-xml 0.37.5",
|
||||||
@ -8984,6 +9029,8 @@ dependencies = [
|
|||||||
"dashmap",
|
"dashmap",
|
||||||
"either",
|
"either",
|
||||||
"enumset",
|
"enumset",
|
||||||
|
"hyper 1.6.0",
|
||||||
|
"hyper-util",
|
||||||
"native-dialog",
|
"native-dialog",
|
||||||
"paste",
|
"paste",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -67,6 +67,7 @@ heck = "0.5.0"
|
|||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
|
hyper = "1.6.0"
|
||||||
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
||||||
"http1",
|
"http1",
|
||||||
"native-tokio",
|
"native-tokio",
|
||||||
@ -98,6 +99,7 @@ notify = { version = "8.0.0", default-features = false }
|
|||||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
||||||
p256 = "0.13.2"
|
p256 = "0.13.2"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
|
phf = { version = "0.12.1", features = ["macros"] }
|
||||||
png = "0.17.16"
|
png = "0.17.16"
|
||||||
prometheus = "0.14.0"
|
prometheus = "0.14.0"
|
||||||
quartz_nbt = "0.2.9"
|
quartz_nbt = "0.2.9"
|
||||||
|
|||||||
@ -61,9 +61,10 @@ import { renderString } from '@modrinth/utils'
|
|||||||
import { useFetch } from '@/helpers/fetch.js'
|
import { useFetch } from '@/helpers/fetch.js'
|
||||||
import { check } from '@tauri-apps/plugin-updater'
|
import { check } from '@tauri-apps/plugin-updater'
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
||||||
import { get_user } from '@/helpers/cache.js'
|
import { get_user } from '@/helpers/cache.js'
|
||||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||||
|
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
|
||||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||||
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
||||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||||
@ -263,6 +264,8 @@ const incompatibilityWarningModal = ref()
|
|||||||
|
|
||||||
const credentials = ref()
|
const credentials = ref()
|
||||||
|
|
||||||
|
const modrinthLoginFlowWaitModal = ref()
|
||||||
|
|
||||||
async function fetchCredentials() {
|
async function fetchCredentials() {
|
||||||
const creds = await getCreds().catch(handleError)
|
const creds = await getCreds().catch(handleError)
|
||||||
if (creds && creds.user_id) {
|
if (creds && creds.user_id) {
|
||||||
@ -272,8 +275,24 @@ async function fetchCredentials() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function signIn() {
|
async function signIn() {
|
||||||
await login().catch(handleError)
|
modrinthLoginFlowWaitModal.value.show()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login()
|
||||||
await fetchCredentials()
|
await fetchCredentials()
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
typeof error === 'object' &&
|
||||||
|
typeof error['message'] === 'string' &&
|
||||||
|
error.message.includes('Login canceled')
|
||||||
|
) {
|
||||||
|
// Not really an error due to being a result of user interaction, show nothing
|
||||||
|
} else {
|
||||||
|
handleError(error)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modrinthLoginFlowWaitModal.value.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logOut() {
|
async function logOut() {
|
||||||
@ -402,6 +421,9 @@ function handleAuxClick(e) {
|
|||||||
<Suspense>
|
<Suspense>
|
||||||
<AppSettingsModal ref="settingsModal" />
|
<AppSettingsModal ref="settingsModal" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
<Suspense>
|
||||||
|
<AuthGrantFlowWaitModal ref="modrinthLoginFlowWaitModal" @flow-cancel="cancelLogin" />
|
||||||
|
</Suspense>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<InstanceCreationModal ref="installationModal" />
|
<InstanceCreationModal ref="installationModal" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@ -305,12 +305,16 @@ const [
|
|||||||
get_game_versions().then(shallowRef).catch(handleError),
|
get_game_versions().then(shallowRef).catch(handleError),
|
||||||
get_loaders()
|
get_loaders()
|
||||||
.then((value) =>
|
.then((value) =>
|
||||||
|
ref(
|
||||||
value
|
value
|
||||||
.filter((item) => item.supported_project_types.includes('modpack'))
|
.filter((item) => item.supported_project_types.includes('modpack'))
|
||||||
.map((item) => item.name.toLowerCase()),
|
.map((item) => item.name.toLowerCase()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.then(ref)
|
.catch((err) => {
|
||||||
.catch(handleError),
|
handleError(err)
|
||||||
|
return ref([])
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
loaders.value.unshift('vanilla')
|
loaders.value.unshift('vanilla')
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
onFlowCancel: {
|
||||||
|
type: Function,
|
||||||
|
default() {
|
||||||
|
return async () => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const modal = ref()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show, hide })
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ModalWrapper ref="modal" @hide="onFlowCancel">
|
||||||
|
<template #title>
|
||||||
|
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
|
<LogInIcon /> Sign in
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-2">
|
||||||
|
<SpinnerIcon class="w-12 h-12 animate-spin" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
Please sign in at the browser window that just opened to continue.
|
||||||
|
</p>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
|
type ProtocolVersion,
|
||||||
type ServerWorld,
|
type ServerWorld,
|
||||||
type ServerData,
|
type ServerData,
|
||||||
type WorldWithProfile,
|
type WorldWithProfile,
|
||||||
@ -33,7 +34,7 @@ const theme = useTheming()
|
|||||||
|
|
||||||
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
||||||
const serverData = ref<Record<string, ServerData>>({})
|
const serverData = ref<Record<string, ServerData>>({})
|
||||||
const protocolVersions = ref<Record<string, number | null>>({})
|
const protocolVersions = ref<Record<string, ProtocolVersion | null>>({})
|
||||||
|
|
||||||
const MIN_JUMP_BACK_IN = 3
|
const MIN_JUMP_BACK_IN = 3
|
||||||
const MAX_JUMP_BACK_IN = 6
|
const MAX_JUMP_BACK_IN = 6
|
||||||
@ -121,11 +122,8 @@ async function populateJumpBackIn() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// fetch each server's data
|
servers.forEach(({ instancePath, address }) =>
|
||||||
Promise.all(
|
|
||||||
servers.map(({ instancePath, address }) =>
|
|
||||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,8 +148,8 @@ async function populateJumpBackIn() {
|
|||||||
.slice(0, MAX_JUMP_BACK_IN)
|
.slice(0, MAX_JUMP_BACK_IN)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshServer(address: string, instancePath: string) {
|
function refreshServer(address: string, instancePath: string) {
|
||||||
await refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinWorld(world: WorldWithProfile) {
|
async function joinWorld(world: WorldWithProfile) {
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
import type {
|
||||||
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
ProtocolVersion,
|
||||||
|
ServerStatus,
|
||||||
|
ServerWorld,
|
||||||
|
SingleplayerWorld,
|
||||||
|
World,
|
||||||
|
set_world_display_status,
|
||||||
|
getWorldIdentifier,
|
||||||
|
} from '@/helpers/worlds.ts'
|
||||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
import {
|
import {
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
@ -55,7 +62,7 @@ const props = withDefaults(
|
|||||||
playingWorld?: boolean
|
playingWorld?: boolean
|
||||||
startingInstance?: boolean
|
startingInstance?: boolean
|
||||||
supportsQuickPlay?: boolean
|
supportsQuickPlay?: boolean
|
||||||
currentProtocol?: number | null
|
currentProtocol?: ProtocolVersion | null
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
|
|
||||||
// Server only
|
// Server only
|
||||||
@ -102,7 +109,8 @@ const serverIncompatible = computed(
|
|||||||
!!props.serverStatus &&
|
!!props.serverStatus &&
|
||||||
!!props.serverStatus.version?.protocol &&
|
!!props.serverStatus.version?.protocol &&
|
||||||
!!props.currentProtocol &&
|
!!props.currentProtocol &&
|
||||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
(props.serverStatus.version.protocol !== props.currentProtocol.version ||
|
||||||
|
props.serverStatus.version.legacy !== props.currentProtocol.legacy),
|
||||||
)
|
)
|
||||||
|
|
||||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||||
|
|||||||
@ -16,3 +16,7 @@ export async function logout() {
|
|||||||
export async function get() {
|
export async function get() {
|
||||||
return await invoke('plugin:mr-auth|get')
|
return await invoke('plugin:mr-auth|get')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cancelLogin() {
|
||||||
|
return await invoke('plugin:mr-auth|cancel_modrinth_login')
|
||||||
|
}
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export type ServerStatus = {
|
|||||||
version?: {
|
version?: {
|
||||||
name: string
|
name: string
|
||||||
protocol: number
|
protocol: number
|
||||||
|
legacy: boolean
|
||||||
}
|
}
|
||||||
favicon?: string
|
favicon?: string
|
||||||
enforces_secure_chat: boolean
|
enforces_secure_chat: boolean
|
||||||
@ -70,11 +71,17 @@ export interface Chat {
|
|||||||
|
|
||||||
export type ServerData = {
|
export type ServerData = {
|
||||||
refreshing: boolean
|
refreshing: boolean
|
||||||
|
lastSuccessfulRefresh?: number
|
||||||
status?: ServerStatus
|
status?: ServerStatus
|
||||||
rawMotd?: string | Chat
|
rawMotd?: string | Chat
|
||||||
renderedMotd?: string
|
renderedMotd?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProtocolVersion = {
|
||||||
|
version: number
|
||||||
|
legacy: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export async function get_recent_worlds(
|
export async function get_recent_worlds(
|
||||||
limit: number,
|
limit: number,
|
||||||
displayStatuses?: DisplayStatus[],
|
displayStatuses?: DisplayStatus[],
|
||||||
@ -156,13 +163,13 @@ export async function remove_server_from_profile(path: string, index: number): P
|
|||||||
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_profile_protocol_version(path: string): Promise<number | null> {
|
export async function get_profile_protocol_version(path: string): Promise<ProtocolVersion | null> {
|
||||||
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_server_status(
|
export async function get_server_status(
|
||||||
address: string,
|
address: string,
|
||||||
protocolVersion: number | null = null,
|
protocolVersion: ProtocolVersion | null = null,
|
||||||
): Promise<ServerStatus> {
|
): Promise<ServerStatus> {
|
||||||
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
||||||
}
|
}
|
||||||
@ -206,30 +213,39 @@ export function isServerWorld(world: World): world is ServerWorld {
|
|||||||
|
|
||||||
export async function refreshServerData(
|
export async function refreshServerData(
|
||||||
serverData: ServerData,
|
serverData: ServerData,
|
||||||
protocolVersion: number | null,
|
protocolVersion: ProtocolVersion | null,
|
||||||
address: string,
|
address: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const refreshTime = Date.now()
|
||||||
serverData.refreshing = true
|
serverData.refreshing = true
|
||||||
await get_server_status(address, protocolVersion)
|
await get_server_status(address, protocolVersion)
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
|
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
||||||
|
// Don't update if there was a more recent successful refresh
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverData.lastSuccessfulRefresh = Date.now()
|
||||||
serverData.status = status
|
serverData.status = status
|
||||||
if (status.description) {
|
if (status.description) {
|
||||||
serverData.rawMotd = status.description
|
serverData.rawMotd = status.description
|
||||||
serverData.renderedMotd = autoToHTML(status.description)
|
serverData.renderedMotd = autoToHTML(status.description)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
|
||||||
console.error(`Refreshing addr: ${address}`, err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
serverData.refreshing = false
|
serverData.refreshing = false
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
||||||
|
if (!protocolVersion?.legacy) {
|
||||||
|
refreshServerData(serverData, { version: 74, legacy: true }, address)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshServers(
|
export function refreshServers(
|
||||||
worlds: World[],
|
worlds: World[],
|
||||||
serverData: Record<string, ServerData>,
|
serverData: Record<string, ServerData>,
|
||||||
protocolVersion: number | null,
|
protocolVersion: ProtocolVersion | null,
|
||||||
) {
|
) {
|
||||||
const servers = worlds.filter(isServerWorld)
|
const servers = worlds.filter(isServerWorld)
|
||||||
servers.forEach((server) => {
|
servers.forEach((server) => {
|
||||||
@ -243,10 +259,8 @@ export async function refreshServers(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
||||||
Promise.all(
|
Object.keys(serverData).forEach((address) =>
|
||||||
Object.keys(serverData).map((address) =>
|
|
||||||
refreshServerData(serverData[address], protocolVersion, address),
|
refreshServerData(serverData[address], protocolVersion, address),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -134,6 +134,7 @@ import {
|
|||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { PlusIcon, SpinnerIcon, UpdatedIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
import { PlusIcon, SpinnerIcon, UpdatedIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
|
type ProtocolVersion,
|
||||||
type SingleplayerWorld,
|
type SingleplayerWorld,
|
||||||
type World,
|
type World,
|
||||||
type ServerWorld,
|
type ServerWorld,
|
||||||
@ -210,7 +211,9 @@ const worldPlaying = ref<World>()
|
|||||||
const worlds = ref<World[]>([])
|
const worlds = ref<World[]>([])
|
||||||
const serverData = ref<Record<string, ServerData>>({})
|
const serverData = ref<Record<string, ServerData>>({})
|
||||||
|
|
||||||
const protocolVersion = ref<number | null>(await get_profile_protocol_version(instance.value.path))
|
const protocolVersion = ref<ProtocolVersion | null>(
|
||||||
|
await get_profile_protocol_version(instance.value.path),
|
||||||
|
)
|
||||||
|
|
||||||
const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
||||||
if (e.profile_path_id !== instance.value.path) return
|
if (e.profile_path_id !== instance.value.path) return
|
||||||
@ -246,7 +249,7 @@ async function refreshAllWorlds() {
|
|||||||
worlds.value = await refreshWorlds(instance.value.path).finally(
|
worlds.value = await refreshWorlds(instance.value.path).finally(
|
||||||
() => (refreshingAll.value = false),
|
() => (refreshingAll.value = false),
|
||||||
)
|
)
|
||||||
await refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||||
|
|
||||||
const hasNoWorlds = worlds.value.length === 0
|
const hasNoWorlds = worlds.value.length === 0
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
|||||||
println!("A browser window will now open, follow the login flow there.");
|
println!("A browser window will now open, follow the login flow there.");
|
||||||
let login = minecraft_auth::begin_login().await?;
|
let login = minecraft_auth::begin_login().await?;
|
||||||
|
|
||||||
println!("Open URL {} in a browser", login.redirect_uri.as_str());
|
println!("Open URL {} in a browser", login.auth_request_uri.as_str());
|
||||||
|
|
||||||
println!("Please enter URL code: ");
|
println!("Please enter URL code: ");
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
|
|||||||
@ -31,6 +31,8 @@ thiserror.workspace = true
|
|||||||
daedalus.workspace = true
|
daedalus.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
either.workspace = true
|
either.workspace = true
|
||||||
|
hyper = { workspace = true, features = ["server"] }
|
||||||
|
hyper-util.workspace = true
|
||||||
|
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
urlencoding.workspace = true
|
urlencoding.workspace = true
|
||||||
|
|||||||
@ -120,7 +120,12 @@ fn main() {
|
|||||||
.plugin(
|
.plugin(
|
||||||
"mr-auth",
|
"mr-auth",
|
||||||
InlinedPlugin::new()
|
InlinedPlugin::new()
|
||||||
.commands(&["modrinth_login", "logout", "get"])
|
.commands(&[
|
||||||
|
"modrinth_login",
|
||||||
|
"logout",
|
||||||
|
"get",
|
||||||
|
"cancel_modrinth_login",
|
||||||
|
])
|
||||||
.default_permission(
|
.default_permission(
|
||||||
DefaultPermissionRule::AllowAllCommands,
|
DefaultPermissionRule::AllowAllCommands,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -33,7 +33,7 @@ pub async fn login<R: Runtime>(
|
|||||||
let window = tauri::WebviewWindowBuilder::new(
|
let window = tauri::WebviewWindowBuilder::new(
|
||||||
&app,
|
&app,
|
||||||
"signin",
|
"signin",
|
||||||
tauri::WebviewUrl::External(flow.redirect_uri.parse().map_err(
|
tauri::WebviewUrl::External(flow.auth_request_uri.parse().map_err(
|
||||||
|_| {
|
|_| {
|
||||||
theseus::ErrorKind::OtherError(
|
theseus::ErrorKind::OtherError(
|
||||||
"Error parsing auth redirect URL".to_string(),
|
"Error parsing auth redirect URL".to_string(),
|
||||||
@ -77,6 +77,7 @@ pub async fn login<R: Runtime>(
|
|||||||
window.close()?;
|
window.close()?;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn remove_user(user: uuid::Uuid) -> Result<()> {
|
pub async fn remove_user(user: uuid::Uuid) -> Result<()> {
|
||||||
Ok(minecraft_auth::remove_user(user).await?)
|
Ok(minecraft_auth::remove_user(user).await?)
|
||||||
|
|||||||
@ -22,6 +22,8 @@ pub mod cache;
|
|||||||
pub mod friends;
|
pub mod friends;
|
||||||
pub mod worlds;
|
pub mod worlds;
|
||||||
|
|
||||||
|
mod oauth_utils;
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
|
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
|
||||||
|
|
||||||
// // Main returnable Theseus GUI error
|
// // Main returnable Theseus GUI error
|
||||||
|
|||||||
@ -1,79 +1,70 @@
|
|||||||
use crate::api::Result;
|
use crate::api::Result;
|
||||||
use chrono::{Duration, Utc};
|
use crate::api::TheseusSerializableError;
|
||||||
|
use crate::api::oauth_utils;
|
||||||
|
use tauri::Manager;
|
||||||
|
use tauri::Runtime;
|
||||||
use tauri::plugin::TauriPlugin;
|
use tauri::plugin::TauriPlugin;
|
||||||
use tauri::{Manager, Runtime, UserAttentionType};
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||||
tauri::plugin::Builder::new("mr-auth")
|
tauri::plugin::Builder::new("mr-auth")
|
||||||
.invoke_handler(tauri::generate_handler![modrinth_login, logout, get,])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
modrinth_login,
|
||||||
|
logout,
|
||||||
|
get,
|
||||||
|
cancel_modrinth_login,
|
||||||
|
])
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn modrinth_login<R: Runtime>(
|
pub async fn modrinth_login<R: Runtime>(
|
||||||
app: tauri::AppHandle<R>,
|
app: tauri::AppHandle<R>,
|
||||||
) -> Result<Option<ModrinthCredentials>> {
|
) -> Result<ModrinthCredentials> {
|
||||||
let redirect_uri = mr_auth::authenticate_begin_flow();
|
let (auth_code_recv_socket_tx, auth_code_recv_socket) = oneshot::channel();
|
||||||
|
let auth_code = tokio::spawn(oauth_utils::auth_code_reply::listen(
|
||||||
|
auth_code_recv_socket_tx,
|
||||||
|
));
|
||||||
|
|
||||||
let start = Utc::now();
|
let auth_code_recv_socket = auth_code_recv_socket.await.unwrap()?;
|
||||||
|
|
||||||
if let Some(window) = app.get_webview_window("modrinth-signin") {
|
let auth_request_uri = format!(
|
||||||
window.close()?;
|
"{}?launcher=true&ipver={}&port={}",
|
||||||
}
|
mr_auth::authenticate_begin_flow(),
|
||||||
|
if auth_code_recv_socket.is_ipv4() {
|
||||||
let window = tauri::WebviewWindowBuilder::new(
|
"4"
|
||||||
&app,
|
|
||||||
"modrinth-signin",
|
|
||||||
tauri::WebviewUrl::External(redirect_uri.parse().map_err(|_| {
|
|
||||||
theseus::ErrorKind::OtherError(
|
|
||||||
"Error parsing auth redirect URL".to_string(),
|
|
||||||
)
|
|
||||||
.as_error()
|
|
||||||
})?),
|
|
||||||
)
|
|
||||||
.min_inner_size(420.0, 632.0)
|
|
||||||
.inner_size(420.0, 632.0)
|
|
||||||
.max_inner_size(420.0, 632.0)
|
|
||||||
.zoom_hotkeys_enabled(false)
|
|
||||||
.title("Sign into Modrinth")
|
|
||||||
.always_on_top(true)
|
|
||||||
.center()
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
window.request_user_attention(Some(UserAttentionType::Critical))?;
|
|
||||||
|
|
||||||
while (Utc::now() - start) < Duration::minutes(10) {
|
|
||||||
if window.title().is_err() {
|
|
||||||
// user closed window, cancelling flow
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
if window
|
|
||||||
.url()?
|
|
||||||
.as_str()
|
|
||||||
.starts_with("https://launcher-files.modrinth.com")
|
|
||||||
{
|
|
||||||
let url = window.url()?;
|
|
||||||
|
|
||||||
let code = url.query_pairs().find(|(key, _)| key == "code");
|
|
||||||
|
|
||||||
window.close()?;
|
|
||||||
|
|
||||||
return if let Some((_, code)) = code {
|
|
||||||
let val = mr_auth::authenticate_finish_flow(&code).await?;
|
|
||||||
|
|
||||||
Ok(Some(val))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
"6"
|
||||||
|
},
|
||||||
|
auth_code_recv_socket.port()
|
||||||
|
);
|
||||||
|
|
||||||
|
app.opener()
|
||||||
|
.open_url(auth_request_uri, None::<&str>)
|
||||||
|
.map_err(|e| {
|
||||||
|
TheseusSerializableError::Theseus(
|
||||||
|
theseus::ErrorKind::OtherError(format!(
|
||||||
|
"Failed to open auth request URI: {e}"
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(auth_code) = auth_code.await.unwrap()? else {
|
||||||
|
return Err(TheseusSerializableError::Theseus(
|
||||||
|
theseus::ErrorKind::OtherError("Login canceled".into()).into(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let credentials = mr_auth::authenticate_finish_flow(&auth_code).await?;
|
||||||
|
|
||||||
|
if let Some(main_window) = app.get_window("main") {
|
||||||
|
main_window.set_focus().ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
Ok(credentials)
|
||||||
}
|
|
||||||
|
|
||||||
window.close()?;
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -85,3 +76,8 @@ pub async fn logout() -> Result<()> {
|
|||||||
pub async fn get() -> Result<Option<ModrinthCredentials>> {
|
pub async fn get() -> Result<Option<ModrinthCredentials>> {
|
||||||
Ok(theseus::mr_auth::get_credentials().await?)
|
Ok(theseus::mr_auth::get_credentials().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn cancel_modrinth_login() {
|
||||||
|
oauth_utils::auth_code_reply::stop_listeners();
|
||||||
|
}
|
||||||
|
|||||||
159
apps/app/src/api/oauth_utils/auth_code_reply.rs
Normal file
159
apps/app/src/api/oauth_utils/auth_code_reply.rs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
//! A minimal OAuth 2.0 authorization code grant flow redirection/reply loopback URI HTTP
|
||||||
|
//! server implementation, compliant with [RFC 6749]'s authorization code grant flow and
|
||||||
|
//! [RFC 8252]'s best current practices for OAuth 2.0 in native apps.
|
||||||
|
//!
|
||||||
|
//! This server is needed for the step 4 of the OAuth authentication dance represented in
|
||||||
|
//! figure 1 of [RFC 8252].
|
||||||
|
//!
|
||||||
|
//! Further reading: https://www.oauth.com/oauth2-servers/oauth-native-apps/redirect-urls-for-native-apps/
|
||||||
|
//!
|
||||||
|
//! [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
|
||||||
|
//! [RFC 8252]: https://datatracker.ietf.org/doc/html/rfc8252
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||||
|
sync::{LazyLock, Mutex},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use hyper::body::Incoming;
|
||||||
|
use hyper_util::rt::{TokioIo, TokioTimer};
|
||||||
|
use theseus::ErrorKind;
|
||||||
|
use tokio::{
|
||||||
|
net::TcpListener,
|
||||||
|
sync::{broadcast, oneshot},
|
||||||
|
};
|
||||||
|
|
||||||
|
static SERVER_SHUTDOWN: LazyLock<broadcast::Sender<()>> =
|
||||||
|
LazyLock::new(|| broadcast::channel(1024).0);
|
||||||
|
|
||||||
|
/// Starts a temporary HTTP server to receive OAuth 2.0 authorization code grant flow redirects
|
||||||
|
/// on a loopback interface with an ephemeral port. The caller can know the bound socket address
|
||||||
|
/// by listening on the counterpart channel for `listen_socket_tx`.
|
||||||
|
///
|
||||||
|
/// If the server is stopped before receiving an authorization code, `Ok(None)` is returned.
|
||||||
|
pub async fn listen(
|
||||||
|
listen_socket_tx: oneshot::Sender<Result<SocketAddr, theseus::Error>>,
|
||||||
|
) -> Result<Option<String>, theseus::Error> {
|
||||||
|
// IPv4 is tried first for the best compatibility and performance with most systems.
|
||||||
|
// IPv6 is also tried in case IPv4 is not available. Resolving "localhost" is avoided
|
||||||
|
// to prevent failures deriving from improper name resolution setup. Any available
|
||||||
|
// ephemeral port is used to prevent conflicts with other services. This is all as per
|
||||||
|
// RFC 8252's recommendations
|
||||||
|
const ANY_LOOPBACK_SOCKET: &[SocketAddr] = &[
|
||||||
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
||||||
|
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let listener = match TcpListener::bind(ANY_LOOPBACK_SOCKET).await {
|
||||||
|
Ok(listener) => {
|
||||||
|
listen_socket_tx
|
||||||
|
.send(listener.local_addr().map_err(|e| {
|
||||||
|
ErrorKind::OtherError(format!(
|
||||||
|
"Failed to get auth code reply socket address: {e}"
|
||||||
|
))
|
||||||
|
.into()
|
||||||
|
}))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
listener
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let error_msg =
|
||||||
|
format!("Failed to bind auth code reply socket: {e}");
|
||||||
|
|
||||||
|
listen_socket_tx
|
||||||
|
.send(Err(ErrorKind::OtherError(error_msg.clone()).into()))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
return Err(ErrorKind::OtherError(error_msg).into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut auth_code = Mutex::new(None);
|
||||||
|
let mut shutdown_notification = SERVER_SHUTDOWN.subscribe();
|
||||||
|
|
||||||
|
while auth_code.get_mut().unwrap().is_none() {
|
||||||
|
let client_socket = tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = shutdown_notification.recv() => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
conn_accept_result = listener.accept() => {
|
||||||
|
match conn_accept_result {
|
||||||
|
Ok((socket, _)) => socket,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to accept auth code reply: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = hyper::server::conn::http1::Builder::new()
|
||||||
|
.keep_alive(false)
|
||||||
|
.header_read_timeout(Duration::from_secs(5))
|
||||||
|
.timer(TokioTimer::new())
|
||||||
|
.auto_date_header(false)
|
||||||
|
.serve_connection(
|
||||||
|
TokioIo::new(client_socket),
|
||||||
|
hyper::service::service_fn(|req| handle_reply(req, &auth_code)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!("Failed to handle auth code reply: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(auth_code.into_inner().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops any active OAuth 2.0 authorization code grant flow reply listening HTTP servers.
|
||||||
|
pub fn stop_listeners() {
|
||||||
|
SERVER_SHUTDOWN.send(()).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_reply(
|
||||||
|
req: hyper::Request<Incoming>,
|
||||||
|
auth_code_out: &Mutex<Option<String>>,
|
||||||
|
) -> Result<hyper::Response<String>, hyper::http::Error> {
|
||||||
|
if req.method() != hyper::Method::GET {
|
||||||
|
return hyper::Response::builder()
|
||||||
|
.status(hyper::StatusCode::METHOD_NOT_ALLOWED)
|
||||||
|
.header("Allow", "GET")
|
||||||
|
.body("".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The authorization code is guaranteed to be sent as a "code" query parameter
|
||||||
|
// in the request URI query string as per RFC 6749 § 4.1.2
|
||||||
|
let auth_code = req.uri().query().and_then(|query_string| {
|
||||||
|
query_string
|
||||||
|
.split('&')
|
||||||
|
.filter_map(|query_pair| query_pair.split_once('='))
|
||||||
|
.find_map(|(key, value)| (key == "code").then_some(value))
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = if let Some(auth_code) = auth_code {
|
||||||
|
*auth_code_out.lock().unwrap() = Some(auth_code.to_string());
|
||||||
|
|
||||||
|
hyper::Response::builder()
|
||||||
|
.status(hyper::StatusCode::OK)
|
||||||
|
.header("Content-Type", "text/html;charset=utf-8")
|
||||||
|
.body(
|
||||||
|
include_str!("auth_code_reply/page.html")
|
||||||
|
.replace("{{title}}", "Success")
|
||||||
|
.replace("{{message}}", "You have successfully signed in! You can close this page now."),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
hyper::Response::builder()
|
||||||
|
.status(hyper::StatusCode::BAD_REQUEST)
|
||||||
|
.header("Content-Type", "text/html;charset=utf-8")
|
||||||
|
.body(
|
||||||
|
include_str!("auth_code_reply/page.html")
|
||||||
|
.replace("{{title}}", "Error")
|
||||||
|
.replace("{{message}}", "Authorization code not found. Please try signing in again."),
|
||||||
|
)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
1
apps/app/src/api/oauth_utils/auth_code_reply/page.html
Normal file
1
apps/app/src/api/oauth_utils/auth_code_reply/page.html
Normal file
File diff suppressed because one or more lines are too long
3
apps/app/src/api/oauth_utils/mod.rs
Normal file
3
apps/app/src/api/oauth_utils/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
//! Assorted utilities for OAuth 2.0 authorization flows.
|
||||||
|
|
||||||
|
pub mod auth_code_reply;
|
||||||
@ -5,8 +5,8 @@ use tauri::{AppHandle, Manager, Runtime};
|
|||||||
use theseus::prelude::ProcessMetadata;
|
use theseus::prelude::ProcessMetadata;
|
||||||
use theseus::profile::{QuickPlayType, get_full_path};
|
use theseus::profile::{QuickPlayType, get_full_path};
|
||||||
use theseus::worlds::{
|
use theseus::worlds::{
|
||||||
DisplayStatus, ServerPackStatus, ServerStatus, World, WorldType,
|
DisplayStatus, ProtocolVersion, ServerPackStatus, ServerStatus, World,
|
||||||
WorldWithProfile,
|
WorldType, WorldWithProfile,
|
||||||
};
|
};
|
||||||
use theseus::{profile, worlds};
|
use theseus::{profile, worlds};
|
||||||
|
|
||||||
@ -183,14 +183,16 @@ pub async fn remove_server_from_profile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_profile_protocol_version(path: &str) -> Result<Option<i32>> {
|
pub async fn get_profile_protocol_version(
|
||||||
|
path: &str,
|
||||||
|
) -> Result<Option<ProtocolVersion>> {
|
||||||
Ok(worlds::get_profile_protocol_version(path).await?)
|
Ok(worlds::get_profile_protocol_version(path).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_server_status(
|
pub async fn get_server_status(
|
||||||
address: &str,
|
address: &str,
|
||||||
protocol_version: Option<i32>,
|
protocol_version: Option<ProtocolVersion>,
|
||||||
) -> Result<ServerStatus> {
|
) -> Result<ServerStatus> {
|
||||||
Ok(worlds::get_server_status(address, protocol_version).await?)
|
Ok(worlds::get_server_status(address, protocol_version).await?)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,7 @@
|
|||||||
"height": 800,
|
"height": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Modrinth App",
|
"title": "Modrinth App",
|
||||||
|
"label": "main",
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"minHeight": 700,
|
"minHeight": 700,
|
||||||
"minWidth": 1100,
|
"minWidth": 1100,
|
||||||
|
|||||||
@ -1,9 +1,19 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM rust:1.88.0 AS build
|
FROM rust:1.88.0 AS build
|
||||||
|
|
||||||
WORKDIR /usr/src/daedalus
|
WORKDIR /usr/src/daedalus
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN cargo build --release --package daedalus_client
|
RUN --mount=type=cache,target=/usr/src/daedalus/target \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/git/db \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
cargo build --release --package daedalus_client
|
||||||
|
|
||||||
|
FROM build AS artifacts
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/usr/src/daedalus/target \
|
||||||
|
mkdir /daedalus \
|
||||||
|
&& cp /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
@ -11,7 +21,7 @@ RUN apt-get update \
|
|||||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
COPY --from=artifacts /daedalus /daedalus
|
||||||
WORKDIR /daedalus_client
|
|
||||||
|
|
||||||
CMD /daedalus/daedalus_client
|
WORKDIR /daedalus_client
|
||||||
|
CMD ["/daedalus/daedalus_client"]
|
||||||
|
|||||||
@ -1,29 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import { MailIcon, CheckIcon } from "@modrinth/assets";
|
import { MailIcon, CheckIcon } from "@modrinth/assets";
|
||||||
import { ref, watchEffect } from "vue";
|
import { ref } from "vue";
|
||||||
import { useBaseFetch } from "~/composables/fetch.js";
|
import { useBaseFetch } from "~/composables/fetch.js";
|
||||||
|
|
||||||
const auth = await useAuth();
|
const auth = await useAuth();
|
||||||
const showSubscriptionConfirmation = ref(false);
|
const showSubscriptionConfirmation = ref(false);
|
||||||
const subscribed = ref(false);
|
const showSubscribeButton = useAsyncData(
|
||||||
|
async () => {
|
||||||
async function checkSubscribed() {
|
|
||||||
if (auth.value?.user) {
|
if (auth.value?.user) {
|
||||||
try {
|
try {
|
||||||
const { data } = await useBaseFetch("auth/email/subscribe", {
|
const { subscribed } = await useBaseFetch("auth/email/subscribe", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
subscribed.value = data?.subscribed || false;
|
return !subscribed;
|
||||||
} catch {
|
} catch {
|
||||||
subscribed.value = false;
|
return true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ watch: [auth], server: false },
|
||||||
watchEffect(() => {
|
);
|
||||||
checkSubscribed();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function subscribe() {
|
async function subscribe() {
|
||||||
try {
|
try {
|
||||||
@ -35,14 +34,19 @@ async function subscribe() {
|
|||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showSubscriptionConfirmation.value = false;
|
showSubscriptionConfirmation.value = false;
|
||||||
subscribed.value = true;
|
showSubscribeButton.status.value = "success";
|
||||||
|
showSubscribeButton.data.value = false;
|
||||||
}, 2500);
|
}, 2500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ButtonStyled v-if="auth?.user && !subscribed" color="brand" type="outlined">
|
<ButtonStyled
|
||||||
|
v-if="showSubscribeButton.status.value === 'success' && showSubscribeButton.data.value"
|
||||||
|
color="brand"
|
||||||
|
type="outlined"
|
||||||
|
>
|
||||||
<button v-tooltip="`Subscribe to the Modrinth newsletter`" @click="subscribe">
|
<button v-tooltip="`Subscribe to the Modrinth newsletter`" @click="subscribe">
|
||||||
<template v-if="!showSubscriptionConfirmation"> <MailIcon /> Subscribe </template>
|
<template v-if="!showSubscriptionConfirmation"> <MailIcon /> Subscribe </template>
|
||||||
<template v-else> <CheckIcon /> Subscribed! </template>
|
<template v-else> <CheckIcon /> Subscribed! </template>
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<div v-if="!modPackData">Loading data...</div>
|
<div v-if="!modPackData">Loading data...</div>
|
||||||
|
|
||||||
<div v-else-if="modPackData.length === 0">
|
<div v-else-if="modPackData.length === 0">
|
||||||
<p>All permissions obtained. You may skip this step!</p>
|
<p>All permissions already obtained.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="!modPackData[currentIndex]">
|
<div v-else-if="!modPackData[currentIndex]">
|
||||||
@ -157,7 +157,7 @@ import type {
|
|||||||
} from "@modrinth/utils";
|
} from "@modrinth/utils";
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import { ref, computed, watch, onMounted } from "vue";
|
import { ref, computed, watch, onMounted } from "vue";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -182,7 +182,26 @@ const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
|||||||
|
|
||||||
const persistedIndex = useLocalStorage<number>(`modpack-permissions-index-${props.projectId}`, 0);
|
const persistedIndex = useLocalStorage<number>(`modpack-permissions-index-${props.projectId}`, 0);
|
||||||
|
|
||||||
const modPackData = ref<ModerationModpackItem[] | null>(null);
|
const modPackData = useSessionStorage<ModerationModpackItem[] | null>(
|
||||||
|
`modpack-permissions-data-${props.projectId}`,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
serializer: {
|
||||||
|
read: (v: any) => (v ? JSON.parse(v) : null),
|
||||||
|
write: (v: any) => JSON.stringify(v),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
|
||||||
|
`modpack-permissions-permanent-no-${props.projectId}`,
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
serializer: {
|
||||||
|
read: (v: any) => (v ? JSON.parse(v) : []),
|
||||||
|
write: (v: any) => JSON.stringify(v),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
const currentIndex = ref(0);
|
const currentIndex = ref(0);
|
||||||
|
|
||||||
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
||||||
@ -251,7 +270,45 @@ async function fetchModPackData(): Promise<void> {
|
|||||||
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
||||||
internal: true,
|
internal: true,
|
||||||
})) as ModerationModpackResponse;
|
})) as ModerationModpackResponse;
|
||||||
|
|
||||||
|
const permanentNoItems: ModerationModpackItem[] = Object.entries(data.identified || {})
|
||||||
|
.filter(([_, file]) => file.status === "permanent-no")
|
||||||
|
.map(
|
||||||
|
([sha1, file]): ModerationModpackItem => ({
|
||||||
|
sha1,
|
||||||
|
file_name: file.file_name,
|
||||||
|
type: "identified",
|
||||||
|
status: file.status,
|
||||||
|
approved: null,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.file_name.localeCompare(b.file_name));
|
||||||
|
|
||||||
|
permanentNoFiles.value = permanentNoItems;
|
||||||
|
|
||||||
const sortedData: ModerationModpackItem[] = [
|
const sortedData: ModerationModpackItem[] = [
|
||||||
|
...Object.entries(data.identified || {})
|
||||||
|
.filter(
|
||||||
|
([_, file]) =>
|
||||||
|
file.status !== "yes" &&
|
||||||
|
file.status !== "with-attribution-and-source" &&
|
||||||
|
file.status !== "permanent-no",
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
([sha1, file]): ModerationModpackItem => ({
|
||||||
|
sha1,
|
||||||
|
file_name: file.file_name,
|
||||||
|
type: "identified",
|
||||||
|
status: file.status,
|
||||||
|
approved: null,
|
||||||
|
...(file.status === "unidentified" && {
|
||||||
|
proof: "",
|
||||||
|
url: "",
|
||||||
|
title: "",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
||||||
...Object.entries(data.unknown_files || {})
|
...Object.entries(data.unknown_files || {})
|
||||||
.map(
|
.map(
|
||||||
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
||||||
@ -310,6 +367,7 @@ async function fetchModPackData(): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch modpack data:", error);
|
console.error("Failed to fetch modpack data:", error);
|
||||||
modPackData.value = [];
|
modPackData.value = [];
|
||||||
|
permanentNoFiles.value = [];
|
||||||
persistAll();
|
persistAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,6 +379,14 @@ function goToPrevious(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
modPackData,
|
||||||
|
(newValue) => {
|
||||||
|
persistedModPackData.value = newValue;
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
function goToNext(): void {
|
function goToNext(): void {
|
||||||
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
||||||
currentIndex.value++;
|
currentIndex.value++;
|
||||||
@ -396,6 +462,17 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
modPackData,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue && newValue.length === 0) {
|
||||||
|
emit("complete");
|
||||||
|
clearPersistedData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.projectId,
|
() => props.projectId,
|
||||||
() => {
|
() => {
|
||||||
@ -406,6 +483,20 @@ watch(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function getModpackFiles(): {
|
||||||
|
interactive: ModerationModpackItem[];
|
||||||
|
permanentNo: ModerationModpackItem[];
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
interactive: modPackData.value || [],
|
||||||
|
permanentNo: permanentNoFiles.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getModpackFiles,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -240,24 +240,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="generatedMessage" class="flex items-center gap-2">
|
<div v-else-if="generatedMessage" class="flex items-center gap-2">
|
||||||
<OverflowMenu :options="stageOptions" class="bg-transparent p-0">
|
|
||||||
<ButtonStyled circular>
|
|
||||||
<button v-tooltip="`Stages`">
|
|
||||||
<ListBulletedIcon />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
|
|
||||||
<template
|
|
||||||
v-for="opt in stageOptions.filter(
|
|
||||||
(opt) => 'id' in opt && 'text' in opt && 'icon' in opt,
|
|
||||||
)"
|
|
||||||
#[opt.id]
|
|
||||||
:key="opt.id"
|
|
||||||
>
|
|
||||||
<component :is="opt.icon" v-if="opt.icon" class="mr-2" />
|
|
||||||
{{ opt.text }}
|
|
||||||
</template>
|
|
||||||
</OverflowMenu>
|
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="goBackToStages">
|
<button @click="goBackToStages">
|
||||||
<LeftArrowIcon aria-hidden="true" />
|
<LeftArrowIcon aria-hidden="true" />
|
||||||
@ -368,21 +350,26 @@ import {
|
|||||||
DropdownSelect,
|
DropdownSelect,
|
||||||
MarkdownEditor,
|
MarkdownEditor,
|
||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
import { type Project, renderHighlightedString, type ModerationJudgements } from "@modrinth/utils";
|
import {
|
||||||
|
type Project,
|
||||||
|
renderHighlightedString,
|
||||||
|
type ModerationJudgements,
|
||||||
|
type ModerationModpackItem,
|
||||||
|
} from "@modrinth/utils";
|
||||||
import { computedAsync, useLocalStorage } from "@vueuse/core";
|
import { computedAsync, useLocalStorage } from "@vueuse/core";
|
||||||
import type {
|
import {
|
||||||
Action,
|
type Action,
|
||||||
MultiSelectChipsAction,
|
type MultiSelectChipsAction,
|
||||||
DropdownAction,
|
type DropdownAction,
|
||||||
ButtonAction,
|
type ButtonAction,
|
||||||
ToggleAction,
|
type ToggleAction,
|
||||||
ConditionalButtonAction,
|
type ConditionalButtonAction,
|
||||||
Stage,
|
type Stage,
|
||||||
|
finalPermissionMessages,
|
||||||
} from "@modrinth/moderation";
|
} from "@modrinth/moderation";
|
||||||
|
import * as prettier from "prettier";
|
||||||
import ModpackPermissionsFlow from "./ModpackPermissionsFlow.vue";
|
import ModpackPermissionsFlow from "./ModpackPermissionsFlow.vue";
|
||||||
import KeybindsModal from "./ChecklistKeybindsModal.vue";
|
import KeybindsModal from "./ChecklistKeybindsModal.vue";
|
||||||
import { finalPermissionMessages } from "@modrinth/moderation/data/modpack-permissions-stage";
|
|
||||||
import prettier from "prettier";
|
|
||||||
|
|
||||||
const keybindsModal = ref<InstanceType<typeof KeybindsModal>>();
|
const keybindsModal = ref<InstanceType<typeof KeybindsModal>>();
|
||||||
|
|
||||||
@ -419,7 +406,6 @@ const done = ref(false);
|
|||||||
|
|
||||||
function handleModpackPermissionsComplete() {
|
function handleModpackPermissionsComplete() {
|
||||||
modpackPermissionsComplete.value = true;
|
modpackPermissionsComplete.value = true;
|
||||||
nextStage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -823,6 +809,31 @@ const isAnyVisibleInputs = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getModpackFilesFromStorage(): {
|
||||||
|
interactive: ModerationModpackItem[];
|
||||||
|
permanentNo: ModerationModpackItem[];
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
const sessionData = sessionStorage.getItem(`modpack-permissions-data-${props.project.id}`);
|
||||||
|
const interactive = sessionData ? (JSON.parse(sessionData) as ModerationModpackItem[]) : [];
|
||||||
|
|
||||||
|
const permanentNoData = sessionStorage.getItem(
|
||||||
|
`modpack-permissions-permanent-no-${props.project.id}`,
|
||||||
|
);
|
||||||
|
const permanentNo = permanentNoData
|
||||||
|
? (JSON.parse(permanentNoData) as ModerationModpackItem[])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
interactive: interactive || [],
|
||||||
|
permanentNo: permanentNo || [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to parse session storage modpack data:", error);
|
||||||
|
return { interactive: [], permanentNo: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function assembleFullMessage() {
|
async function assembleFullMessage() {
|
||||||
const messageParts: MessagePart[] = [];
|
const messageParts: MessagePart[] = [];
|
||||||
|
|
||||||
@ -1092,15 +1103,16 @@ async function generateMessage() {
|
|||||||
const baseMessage = await assembleFullMessage();
|
const baseMessage = await assembleFullMessage();
|
||||||
let fullMessage = baseMessage;
|
let fullMessage = baseMessage;
|
||||||
|
|
||||||
if (
|
if (props.project.project_type === "modpack") {
|
||||||
props.project.project_type === "modpack" &&
|
const modpackFilesData = getModpackFilesFromStorage();
|
||||||
Object.keys(modpackJudgements.value).length > 0
|
|
||||||
) {
|
if (modpackFilesData.interactive.length > 0 || modpackFilesData.permanentNo.length > 0) {
|
||||||
const modpackMessage = generateModpackMessage(modpackJudgements.value);
|
const modpackMessage = generateModpackMessage(modpackFilesData);
|
||||||
if (modpackMessage) {
|
if (modpackMessage) {
|
||||||
fullMessage = baseMessage ? `${baseMessage}\n\n${modpackMessage}` : modpackMessage;
|
fullMessage = baseMessage ? `${baseMessage}\n\n${modpackMessage}` : modpackMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formattedMessage = await prettier.format(fullMessage, {
|
const formattedMessage = await prettier.format(fullMessage, {
|
||||||
@ -1129,25 +1141,32 @@ async function generateMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateModpackMessage(judgements: ModerationJudgements) {
|
function generateModpackMessage(allFiles: {
|
||||||
|
interactive: ModerationModpackItem[];
|
||||||
|
permanentNo: ModerationModpackItem[];
|
||||||
|
}) {
|
||||||
const issues = [];
|
const issues = [];
|
||||||
|
|
||||||
const attributeMods = [];
|
const attributeMods: string[] = [];
|
||||||
const noMods = [];
|
const noMods: string[] = [];
|
||||||
const permanentNoMods = [];
|
const permanentNoMods: string[] = [];
|
||||||
const unidentifiedMods = [];
|
const unidentifiedMods: string[] = [];
|
||||||
|
|
||||||
for (const [, judgement] of Object.entries(judgements)) {
|
allFiles.interactive.forEach((file) => {
|
||||||
if (judgement.status === "with-attribution") {
|
if (file.status === "unidentified") {
|
||||||
attributeMods.push(judgement.file_name);
|
if (file.approved === "no") {
|
||||||
} else if (judgement.status === "no") {
|
unidentifiedMods.push(file.file_name);
|
||||||
noMods.push(judgement.file_name);
|
|
||||||
} else if (judgement.status === "permanent-no") {
|
|
||||||
permanentNoMods.push(judgement.file_name);
|
|
||||||
} else if (judgement.status === "unidentified") {
|
|
||||||
unidentifiedMods.push(judgement.file_name);
|
|
||||||
}
|
}
|
||||||
|
} else if (file.status === "with-attribution" && file.approved === "no") {
|
||||||
|
attributeMods.push(file.file_name);
|
||||||
|
} else if (file.status === "no" && file.approved === "no") {
|
||||||
|
noMods.push(file.file_name);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
allFiles.permanentNo.forEach((file) => {
|
||||||
|
permanentNoMods.push(file.file_name);
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
attributeMods.length > 0 ||
|
attributeMods.length > 0 ||
|
||||||
@ -1157,6 +1176,12 @@ function generateModpackMessage(judgements: ModerationJudgements) {
|
|||||||
) {
|
) {
|
||||||
issues.push("## Copyrighted content");
|
issues.push("## Copyrighted content");
|
||||||
|
|
||||||
|
if (unidentifiedMods.length > 0) {
|
||||||
|
issues.push(
|
||||||
|
`${finalPermissionMessages.unidentified}\n${unidentifiedMods.map((mod) => `- ${mod}`).join("\n")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (attributeMods.length > 0) {
|
if (attributeMods.length > 0) {
|
||||||
issues.push(
|
issues.push(
|
||||||
`${finalPermissionMessages["with-attribution"]}\n${attributeMods.map((mod) => `- ${mod}`).join("\n")}`,
|
`${finalPermissionMessages["with-attribution"]}\n${attributeMods.map((mod) => `- ${mod}`).join("\n")}`,
|
||||||
@ -1172,12 +1197,6 @@ function generateModpackMessage(judgements: ModerationJudgements) {
|
|||||||
`${finalPermissionMessages["permanent-no"]}\n${permanentNoMods.map((mod) => `- ${mod}`).join("\n")}`,
|
`${finalPermissionMessages["permanent-no"]}\n${permanentNoMods.map((mod) => `- ${mod}`).join("\n")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unidentifiedMods.length > 0) {
|
|
||||||
issues.push(
|
|
||||||
`${finalPermissionMessages["unidentified"]}\n${unidentifiedMods.map((mod) => `- ${mod}`).join("\n")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return issues.join("\n\n");
|
return issues.join("\n\n");
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<template v-if="moderation">
|
||||||
|
<Chips v-model="reasonFilter" :items="reasons" />
|
||||||
|
<p v-if="reports.length === MAX_REPORTS" class="text-red">
|
||||||
|
There are at least {{ MAX_REPORTS }} open reports. This page is at its max reports and will
|
||||||
|
not show any more recent ones.
|
||||||
|
</p>
|
||||||
|
<p v-else-if="reasonFilter === 'All'">There are {{ filteredReports.length }} open reports.</p>
|
||||||
|
<p v-else>
|
||||||
|
There are {{ filteredReports.length }}/{{ reports.length }} open '{{ reasonFilter }}' reports.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<ReportInfo
|
<ReportInfo
|
||||||
v-for="report in reports.filter(
|
v-for="report in filteredReports"
|
||||||
(x) =>
|
|
||||||
(moderation || x.reporterUser.id === auth.user.id) &&
|
|
||||||
(viewMode === 'open' ? x.open : !x.open),
|
|
||||||
)"
|
|
||||||
:key="report.id"
|
:key="report.id"
|
||||||
:report="report"
|
:report="report"
|
||||||
:thread="report.thread"
|
:thread="report.thread"
|
||||||
|
:show-message="false"
|
||||||
:moderation="moderation"
|
:moderation="moderation"
|
||||||
raised
|
raised
|
||||||
:auth="auth"
|
:auth="auth"
|
||||||
@ -16,11 +24,12 @@
|
|||||||
<p v-if="reports.length === 0">You don't have any active reports.</p>
|
<p v-if="reports.length === 0">You don't have any active reports.</p>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { Chips } from "@modrinth/ui";
|
||||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
||||||
import { addReportMessage } from "~/helpers/threads.js";
|
import { addReportMessage } from "~/helpers/threads.js";
|
||||||
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
moderation: {
|
moderation: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@ -32,9 +41,14 @@ defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const viewMode = ref("open");
|
const viewMode = ref("open");
|
||||||
|
const reasonFilter = ref("All");
|
||||||
const reports = ref([]);
|
const reports = ref([]);
|
||||||
|
|
||||||
let { data: rawReports } = await useAsyncData("report", () => useBaseFetch("report?count=1000"));
|
const MAX_REPORTS = 1500;
|
||||||
|
|
||||||
|
let { data: rawReports } = await useAsyncData("report", () =>
|
||||||
|
useBaseFetch(`report?count=${MAX_REPORTS}`),
|
||||||
|
);
|
||||||
|
|
||||||
rawReports = rawReports.value.map((report) => {
|
rawReports = rawReports.value.map((report) => {
|
||||||
report.item_id = report.item_id.replace(/"/g, "");
|
report.item_id = report.item_id.replace(/"/g, "");
|
||||||
@ -51,6 +65,7 @@ const userIds = [...new Set(reporterUsers.concat(reportedUsers))];
|
|||||||
const threadIds = [
|
const threadIds = [
|
||||||
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
|
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
|
||||||
];
|
];
|
||||||
|
const reasons = ["All", ...new Set(rawReports.map((report) => report.report_type))];
|
||||||
|
|
||||||
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||||
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||||
@ -93,4 +108,13 @@ reports.value = rawReports.map((report) => {
|
|||||||
report.open = true;
|
report.open = true;
|
||||||
return report;
|
return report;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filteredReports = computed(() =>
|
||||||
|
reports.value?.filter(
|
||||||
|
(x) =>
|
||||||
|
(props.moderation || x.reporterUser.id === props.auth.user.id) &&
|
||||||
|
(viewMode.value === "open" ? x.open : !x.open) &&
|
||||||
|
(reasonFilter.value === "All" || reasonFilter.value === x.report_type),
|
||||||
|
),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -66,6 +66,27 @@
|
|||||||
<UiServersPanelSpinner />
|
<UiServersPanelSpinner />
|
||||||
Your server's hardware is currently being upgraded and will be back online shortly.
|
Your server's hardware is currently being upgraded and will be back online shortly.
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="status === 'suspended' && suspension_reason === 'cancelled'"
|
||||||
|
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<UiServersIconsPanelErrorIcon class="!size-5" /> Your server has been cancelled. Please
|
||||||
|
update your billing information or contact Modrinth Support for more information.
|
||||||
|
</div>
|
||||||
|
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="status === 'suspended' && suspension_reason"
|
||||||
|
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<UiServersIconsPanelErrorIcon class="!size-5" /> Your server has been suspended:
|
||||||
|
{{ suspension_reason }}. Please update your billing information or contact Modrinth Support
|
||||||
|
for more information.
|
||||||
|
</div>
|
||||||
|
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="status === 'suspended'"
|
v-else-if="status === 'suspended'"
|
||||||
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||||
@ -87,7 +108,8 @@ import { Avatar, CopyCode } from "@modrinth/ui";
|
|||||||
|
|
||||||
const props = defineProps<Partial<Server>>();
|
const props = defineProps<Partial<Server>>();
|
||||||
|
|
||||||
if (props.server_id) {
|
if (props.server_id && props.status === "available") {
|
||||||
|
// Necessary only to get server icon
|
||||||
await useModrinthServers(props.server_id, ["general"]);
|
await useModrinthServers(props.server_id, ["general"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,11 +131,6 @@ if (props.upstream) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
|
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
|
||||||
|
|
||||||
if (import.meta.server && projectData.value?.icon_url) {
|
|
||||||
await useModrinthServers(props.server_id!, ["general"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
|
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
|
||||||
const isConfiguring = computed(() => props.flows?.intro);
|
const isConfiguring = computed(() => props.flows?.intro);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -34,6 +34,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal ref="modalReply" header="Reply to thread">
|
||||||
|
<div class="modal-submit universal-body">
|
||||||
|
<span>
|
||||||
|
Your project is already approved. As such, the moderation team does not actively monitor
|
||||||
|
this thread. However, they may still see your message if there is a problem with your
|
||||||
|
project.
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
If you need to get in contact with the moderation team, please use the
|
||||||
|
<a class="text-link" href="https://support.modrinth.com" target="_blank">
|
||||||
|
Modrinth Help Center
|
||||||
|
</a>
|
||||||
|
and click the green bubble to contact support.
|
||||||
|
</span>
|
||||||
|
<Checkbox
|
||||||
|
v-model="replyConfirmation"
|
||||||
|
description="Confirm moderators do not actively monitor this"
|
||||||
|
>
|
||||||
|
I acknowledge that the moderators do not actively monitor the thread.
|
||||||
|
</Checkbox>
|
||||||
|
<div class="input-group push-right">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="!replyConfirmation"
|
||||||
|
@click="sendReplyFromModal()"
|
||||||
|
>
|
||||||
|
<ReplyIcon aria-hidden="true" />
|
||||||
|
Reply to thread
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
<div v-if="flags.developerMode" class="thread-id">
|
<div v-if="flags.developerMode" class="thread-id">
|
||||||
Thread ID:
|
Thread ID:
|
||||||
<CopyCode :text="thread.id" />
|
<CopyCode :text="thread.id" />
|
||||||
@ -71,12 +103,17 @@
|
|||||||
v-if="sortedMessages.length > 0"
|
v-if="sortedMessages.length > 0"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
:disabled="!replyBody"
|
:disabled="!replyBody"
|
||||||
@click="sendReply()"
|
@click="isApproved(project) && !isStaff(auth.user) ? openReplyModal() : sendReply()"
|
||||||
>
|
>
|
||||||
<ReplyIcon aria-hidden="true" />
|
<ReplyIcon aria-hidden="true" />
|
||||||
Reply
|
Reply
|
||||||
</button>
|
</button>
|
||||||
<button v-else class="btn btn-primary" :disabled="!replyBody" @click="sendReply()">
|
<button
|
||||||
|
v-else
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="!replyBody"
|
||||||
|
@click="isApproved(project) && !isStaff(auth.user) ? openReplyModal() : sendReply()"
|
||||||
|
>
|
||||||
<SendIcon aria-hidden="true" />
|
<SendIcon aria-hidden="true" />
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
@ -289,6 +326,7 @@ const sortedMessages = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const modalSubmit = ref(null);
|
const modalSubmit = ref(null);
|
||||||
|
const modalReply = ref(null);
|
||||||
|
|
||||||
async function updateThreadLocal() {
|
async function updateThreadLocal() {
|
||||||
let threadId = null;
|
let threadId = null;
|
||||||
@ -316,6 +354,11 @@ async function onUploadImage(file) {
|
|||||||
return response.url;
|
return response.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendReplyFromModal(status = null, privateMessage = false) {
|
||||||
|
modalReply.value.hide();
|
||||||
|
await sendReply(status, privateMessage);
|
||||||
|
}
|
||||||
|
|
||||||
async function sendReply(status = null, privateMessage = false) {
|
async function sendReply(status = null, privateMessage = false) {
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
@ -398,6 +441,7 @@ async function reopenReport() {
|
|||||||
|
|
||||||
const replyWithSubmission = ref(false);
|
const replyWithSubmission = ref(false);
|
||||||
const submissionConfirmation = ref(false);
|
const submissionConfirmation = ref(false);
|
||||||
|
const replyConfirmation = ref(false);
|
||||||
|
|
||||||
function openResubmitModal(reply) {
|
function openResubmitModal(reply) {
|
||||||
submissionConfirmation.value = false;
|
submissionConfirmation.value = false;
|
||||||
@ -405,6 +449,11 @@ function openResubmitModal(reply) {
|
|||||||
modalSubmit.value.show();
|
modalSubmit.value.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openReplyModal(reply) {
|
||||||
|
replyConfirmation.value = false;
|
||||||
|
modalReply.value.show();
|
||||||
|
}
|
||||||
|
|
||||||
async function resubmit() {
|
async function resubmit() {
|
||||||
if (replyWithSubmission.value) {
|
if (replyWithSubmission.value) {
|
||||||
await sendReply("processing");
|
await sendReply("processing");
|
||||||
|
|||||||
@ -182,9 +182,6 @@
|
|||||||
"collection.button.unfollow-project": {
|
"collection.button.unfollow-project": {
|
||||||
"message": "Unfollow project"
|
"message": "Unfollow project"
|
||||||
},
|
},
|
||||||
"collection.button.upload-icon": {
|
|
||||||
"message": "Upload icon"
|
|
||||||
},
|
|
||||||
"collection.delete-modal.description": {
|
"collection.delete-modal.description": {
|
||||||
"message": "This will remove this collection forever. This action cannot be undone."
|
"message": "This will remove this collection forever. This action cannot be undone."
|
||||||
},
|
},
|
||||||
|
|||||||
@ -951,14 +951,7 @@ import {
|
|||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
||||||
import {
|
import { formatCategory, formatProjectType, renderString } from "@modrinth/utils";
|
||||||
formatCategory,
|
|
||||||
formatProjectType,
|
|
||||||
isRejected,
|
|
||||||
isStaff,
|
|
||||||
isUnderReview,
|
|
||||||
renderString,
|
|
||||||
} from "@modrinth/utils";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Tooltip } from "floating-vue";
|
import { Tooltip } from "floating-vue";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
@ -1646,9 +1639,7 @@ const navLinks = computed(() => {
|
|||||||
{
|
{
|
||||||
label: formatMessage(messages.moderationTab),
|
label: formatMessage(messages.moderationTab),
|
||||||
href: `${projectUrl}/moderation`,
|
href: `${projectUrl}/moderation`,
|
||||||
shown:
|
shown: !!currentMember.value,
|
||||||
!!currentMember.value &&
|
|
||||||
(isRejected(project.value) || isUnderReview(project.value) || isStaff(auth.value.user)),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@ -76,8 +76,15 @@
|
|||||||
<p>
|
<p>
|
||||||
This is a private conversation thread with the Modrinth moderators. They may message you
|
This is a private conversation thread with the Modrinth moderators. They may message you
|
||||||
with issues concerning this project. This thread is only checked when you submit your
|
with issues concerning this project. This thread is only checked when you submit your
|
||||||
project for review. For additional inquiries, contact
|
project for review. For additional inquiries, please go to the
|
||||||
<a href="https://support.modrinth.com">Modrinth Support</a>.
|
<a class="text-link" href="https://support.modrinth.com" target="_blank">
|
||||||
|
Modrinth Help Center
|
||||||
|
</a>
|
||||||
|
and click the green bubble to contact support.
|
||||||
|
</p>
|
||||||
|
<p v-if="isApproved(project)" class="warning">
|
||||||
|
<IssuesIcon /> The moderators do not actively monitor this chat. However, they may still see
|
||||||
|
messages here if there is a problem with your project.
|
||||||
</p>
|
</p>
|
||||||
<ConversationThread
|
<ConversationThread
|
||||||
v-if="thread"
|
v-if="thread"
|
||||||
|
|||||||
@ -58,6 +58,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NewModal>
|
</NewModal>
|
||||||
|
<NewModal ref="modifyModal">
|
||||||
|
<template #title>
|
||||||
|
<span class="text-lg font-extrabold text-contrast">Modify charge</span>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="cancel" class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-semibold text-contrast">
|
||||||
|
Cancel server
|
||||||
|
<span class="text-brand-red">*</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Whether or not the subscription should be cancelled. Submitting this as "true" will
|
||||||
|
cancel the subscription, while submitting it as "false" will force another charge
|
||||||
|
attempt to be made.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Toggle id="cancel" v-model="cancel" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button :disabled="modifying" @click="modifyCharge">
|
||||||
|
<CheckIcon aria-hidden="true" />
|
||||||
|
Modify charge
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="modifyModal.hide()">
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
<div class="page experimental-styles-within">
|
<div class="page experimental-styles-within">
|
||||||
<div
|
<div
|
||||||
class="mb-4 flex items-center justify-between border-0 border-b border-solid border-divider pb-4"
|
class="mb-4 flex items-center justify-between border-0 border-b border-solid border-divider pb-4"
|
||||||
@ -201,6 +236,12 @@
|
|||||||
Refund options
|
Refund options
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
<ButtonStyled v-else-if="charge.status === 'failed'" color="red" color-fill="text">
|
||||||
|
<button @click="showModifyModal(subscription)">
|
||||||
|
<CurrencyIcon />
|
||||||
|
Modify charge
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -234,7 +275,6 @@ import { products } from "~/generated/state.json";
|
|||||||
import ModrinthServersIcon from "~/components/ui/servers/ModrinthServersIcon.vue";
|
import ModrinthServersIcon from "~/components/ui/servers/ModrinthServersIcon.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const data = useNuxtApp();
|
|
||||||
const vintl = useVIntl();
|
const vintl = useVIntl();
|
||||||
|
|
||||||
const { formatMessage } = vintl;
|
const { formatMessage } = vintl;
|
||||||
@ -304,6 +344,10 @@ const refundTypes = ref(["full", "partial", "none"]);
|
|||||||
const refundAmount = ref(0);
|
const refundAmount = ref(0);
|
||||||
const unprovision = ref(true);
|
const unprovision = ref(true);
|
||||||
|
|
||||||
|
const modifying = ref(false);
|
||||||
|
const modifyModal = ref();
|
||||||
|
const cancel = ref(false);
|
||||||
|
|
||||||
function showRefundModal(charge) {
|
function showRefundModal(charge) {
|
||||||
selectedCharge.value = charge;
|
selectedCharge.value = charge;
|
||||||
refundType.value = "full";
|
refundType.value = "full";
|
||||||
@ -312,6 +356,12 @@ function showRefundModal(charge) {
|
|||||||
refundModal.value.show();
|
refundModal.value.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showModifyModal(charge) {
|
||||||
|
selectedCharge.value = charge;
|
||||||
|
cancel.value = false;
|
||||||
|
modifyModal.value.show();
|
||||||
|
}
|
||||||
|
|
||||||
async function refundCharge() {
|
async function refundCharge() {
|
||||||
refunding.value = true;
|
refunding.value = true;
|
||||||
try {
|
try {
|
||||||
@ -327,8 +377,7 @@ async function refundCharge() {
|
|||||||
await refreshCharges();
|
await refreshCharges();
|
||||||
refundModal.value.hide();
|
refundModal.value.hide();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
data.$notify({
|
addNotification({
|
||||||
group: "main",
|
|
||||||
title: "Error refunding",
|
title: "Error refunding",
|
||||||
text: err.data?.description ?? err,
|
text: err.data?.description ?? err,
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -337,6 +386,32 @@ async function refundCharge() {
|
|||||||
refunding.value = false;
|
refunding.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function modifyCharge() {
|
||||||
|
modifying.value = true;
|
||||||
|
try {
|
||||||
|
await useBaseFetch(`billing/subscription/${selectedCharge.value.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({
|
||||||
|
cancelled: cancel.value,
|
||||||
|
}),
|
||||||
|
internal: true,
|
||||||
|
});
|
||||||
|
addNotification({
|
||||||
|
title: "Resubscription request submitted",
|
||||||
|
text: "If the server is currently suspended, it may take up to 10 minutes for another charge attempt to be made.",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
await refreshCharges();
|
||||||
|
} catch (err) {
|
||||||
|
addNotification({
|
||||||
|
title: "Error reattempting charge",
|
||||||
|
text: err.data?.description ?? err,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
modifying.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
const chargeStatuses = {
|
const chargeStatuses = {
|
||||||
open: {
|
open: {
|
||||||
color: "bg-blue",
|
color: "bg-blue",
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="subtleLauncherRedirectUri">
|
||||||
<template v-if="flow">
|
<iframe
|
||||||
|
:src="subtleLauncherRedirectUri"
|
||||||
|
class="fixed left-0 top-0 z-[9999] m-0 h-full w-full border-0 p-0"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<template v-if="flow && !subtleLauncherRedirectUri">
|
||||||
<label for="two-factor-code">
|
<label for="two-factor-code">
|
||||||
<span class="label__title">{{ formatMessage(messages.twoFactorCodeLabel) }}</span>
|
<span class="label__title">{{ formatMessage(messages.twoFactorCodeLabel) }}</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
@ -189,6 +195,7 @@ const auth = await useAuth();
|
|||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
|
|
||||||
const redirectTarget = route.query.redirect || "";
|
const redirectTarget = route.query.redirect || "";
|
||||||
|
const subtleLauncherRedirectUri = ref();
|
||||||
|
|
||||||
if (route.query.code && !route.fullPath.includes("new_account=true")) {
|
if (route.query.code && !route.fullPath.includes("new_account=true")) {
|
||||||
await finishSignIn();
|
await finishSignIn();
|
||||||
@ -262,7 +269,32 @@ async function begin2FASignIn() {
|
|||||||
|
|
||||||
async function finishSignIn(token) {
|
async function finishSignIn(token) {
|
||||||
if (route.query.launcher) {
|
if (route.query.launcher) {
|
||||||
await navigateTo(`https://launcher-files.modrinth.com/?code=${token}`, { external: true });
|
if (!token) {
|
||||||
|
token = auth.value.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usesLocalhostRedirectionScheme =
|
||||||
|
["4", "6"].includes(route.query.ipver) && Number(route.query.port) < 65536;
|
||||||
|
|
||||||
|
const redirectUrl = usesLocalhostRedirectionScheme
|
||||||
|
? `http://${route.query.ipver === "4" ? "127.0.0.1" : "[::1]"}:${route.query.port}/?code=${token}`
|
||||||
|
: `https://launcher-files.modrinth.com/?code=${token}`;
|
||||||
|
|
||||||
|
if (usesLocalhostRedirectionScheme) {
|
||||||
|
// When using this redirection scheme, the auth token is very visible in the URL to the user.
|
||||||
|
// While we could make it harder to find with a POST request, such is security by obscurity:
|
||||||
|
// the user and other applications would still be able to sniff the token in the request body.
|
||||||
|
// So, to make the UX a little better by not changing the displayed URL, while keeping the
|
||||||
|
// token hidden from very casual observation and keeping the protocol as close to OAuth's
|
||||||
|
// standard flows as possible, let's execute the redirect within an iframe that visually
|
||||||
|
// covers the entire page.
|
||||||
|
subtleLauncherRedirectUri.value = redirectUrl;
|
||||||
|
} else {
|
||||||
|
await navigateTo(redirectUrl, {
|
||||||
|
external: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -247,16 +247,14 @@ async function createAccount() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (route.query.launcher) {
|
|
||||||
await navigateTo(`https://launcher-files.modrinth.com/?code=${res.session}`, {
|
|
||||||
external: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await useAuth(res.session);
|
await useAuth(res.session);
|
||||||
await useUser();
|
await useUser();
|
||||||
|
|
||||||
|
if (route.query.launcher) {
|
||||||
|
await navigateTo({ path: "/auth/sign-in", query: route.query });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (route.query.redirect) {
|
if (route.query.redirect) {
|
||||||
await navigateTo(route.query.redirect);
|
await navigateTo(route.query.redirect);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -40,7 +40,6 @@
|
|||||||
@change="showPreviewImage"
|
@change="showPreviewImage"
|
||||||
>
|
>
|
||||||
<UploadIcon aria-hidden="true" />
|
<UploadIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.uploadIconButton) }}
|
|
||||||
</FileInput>
|
</FileInput>
|
||||||
<Button
|
<Button
|
||||||
v-if="!deletedIcon && (previewImage || collection.icon_url)"
|
v-if="!deletedIcon && (previewImage || collection.icon_url)"
|
||||||
@ -479,10 +478,6 @@ const messages = defineMessages({
|
|||||||
id: "collection.label.updated-at",
|
id: "collection.label.updated-at",
|
||||||
defaultMessage: "Updated {ago}",
|
defaultMessage: "Updated {ago}",
|
||||||
},
|
},
|
||||||
uploadIconButton: {
|
|
||||||
id: "collection.button.upload-icon",
|
|
||||||
defaultMessage: "Upload icon",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = useNuxtApp();
|
const data = useNuxtApp();
|
||||||
|
|||||||
@ -96,16 +96,7 @@
|
|||||||
<UiServersServerListing
|
<UiServersServerListing
|
||||||
v-for="server in filteredData"
|
v-for="server in filteredData"
|
||||||
:key="server.server_id"
|
:key="server.server_id"
|
||||||
:server_id="server.server_id"
|
v-bind="server"
|
||||||
:name="server.name"
|
|
||||||
:status="server.status"
|
|
||||||
:game="server.game"
|
|
||||||
:loader="server.loader"
|
|
||||||
:loader_version="server.loader_version"
|
|
||||||
:mc_version="server.mc_version"
|
|
||||||
:upstream="server.upstream"
|
|
||||||
:net="server.net"
|
|
||||||
:flows="server.flows"
|
|
||||||
/>
|
/>
|
||||||
<LazyUiServersServerListingSkeleton v-if="isPollingForNewServers" />
|
<LazyUiServersServerListingSkeleton v-if="isPollingForNewServers" />
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -208,15 +208,7 @@
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<UiServersServerListing
|
<UiServersServerListing
|
||||||
v-if="subscription.serverInfo"
|
v-if="subscription.serverInfo"
|
||||||
:server_id="subscription.serverInfo.server_id"
|
v-bind="subscription.serverInfo"
|
||||||
:name="subscription.serverInfo.name"
|
|
||||||
:status="subscription.serverInfo.status"
|
|
||||||
:game="subscription.serverInfo.game"
|
|
||||||
:loader="subscription.serverInfo.loader"
|
|
||||||
:loader_version="subscription.serverInfo.loader_version"
|
|
||||||
:mc_version="subscription.serverInfo.mc_version"
|
|
||||||
:upstream="subscription.serverInfo.upstream"
|
|
||||||
:net="subscription.serverInfo.net"
|
|
||||||
/>
|
/>
|
||||||
<div v-else class="w-fit">
|
<div v-else class="w-fit">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT id FROM reports\n WHERE closed = FALSE\n ORDER BY created ASC\n LIMIT $1;\n ",
|
"query": "\n SELECT id FROM reports\n WHERE closed = FALSE\n ORDER BY created ASC\n OFFSET $2\n LIMIT $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -11,6 +11,7 @@
|
|||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
|
"Int8",
|
||||||
"Int8"
|
"Int8"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -18,5 +19,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "29e171bd746ac5dc1fabae4c9f81c3d1df4e69c860b7d0f6a907377664199217"
|
"hash": "1aea0d5e6936b043cb7727b779d60598aa812c8ef0f5895fa740859321092a1c"
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT id FROM reports\n WHERE closed = FALSE AND reporter = $1\n ORDER BY created ASC\n LIMIT $2;\n ",
|
"query": "\n SELECT id FROM reports\n WHERE closed = FALSE AND reporter = $1\n ORDER BY created ASC\n OFFSET $3\n LIMIT $2\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -11,6 +11,7 @@
|
|||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
|
"Int8",
|
||||||
"Int8",
|
"Int8",
|
||||||
"Int8"
|
"Int8"
|
||||||
]
|
]
|
||||||
@ -19,5 +20,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "f17a109913015a7a5ab847bb2e73794d6261a08d450de24b450222755e520881"
|
"hash": "be8a5dd2b71fdc279a6fa68fe5384da31afd91d4b480527e2dd8402aef36f12c"
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT id FROM mods\n WHERE status = $1\n ORDER BY queued ASC\n LIMIT $2;\n ",
|
"query": "\n SELECT id FROM mods\n WHERE status = $1\n ORDER BY queued ASC\n OFFSET $3\n LIMIT $2\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -12,6 +12,7 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Text",
|
"Text",
|
||||||
|
"Int8",
|
||||||
"Int8"
|
"Int8"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -19,5 +20,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "3baabc9f08401801fa290866888c540746fc50c1d79911f08f3322b605ce5c30"
|
"hash": "ccb0315ff52ea4402f53508334a7288fc9f8e77ffd7bce665441ff682384cbf9"
|
||||||
}
|
}
|
||||||
@ -1,8 +1,21 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM rust:1.88.0 AS build
|
FROM rust:1.88.0 AS build
|
||||||
|
|
||||||
WORKDIR /usr/src/labrinth
|
WORKDIR /usr/src/labrinth
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN SQLX_OFFLINE=true cargo build --release --package labrinth
|
RUN --mount=type=cache,target=/usr/src/labrinth/target \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/git/db \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
SQLX_OFFLINE=true cargo build --release --package labrinth
|
||||||
|
|
||||||
|
FROM build AS artifacts
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/usr/src/labrinth/target \
|
||||||
|
mkdir /labrinth \
|
||||||
|
&& cp /usr/src/labrinth/target/release/labrinth /labrinth/labrinth \
|
||||||
|
&& cp -r /usr/src/labrinth/apps/labrinth/migrations /labrinth \
|
||||||
|
&& cp -r /usr/src/labrinth/apps/labrinth/assets /labrinth
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
@ -14,10 +27,8 @@ RUN apt-get update \
|
|||||||
&& apt-get install -y --no-install-recommends ca-certificates dumb-init curl \
|
&& apt-get install -y --no-install-recommends ca-certificates dumb-init curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=build /usr/src/labrinth/target/release/labrinth /labrinth/labrinth
|
COPY --from=artifacts /labrinth /labrinth
|
||||||
COPY --from=build /usr/src/labrinth/apps/labrinth/migrations/* /labrinth/migrations/
|
|
||||||
COPY --from=build /usr/src/labrinth/apps/labrinth/assets /labrinth/assets
|
|
||||||
WORKDIR /labrinth
|
|
||||||
|
|
||||||
|
WORKDIR /labrinth
|
||||||
ENTRYPOINT ["dumb-init", "--"]
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
CMD ["/labrinth/labrinth"]
|
CMD ["/labrinth/labrinth"]
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX reports_closed ON reports (closed);
|
||||||
@ -315,9 +315,13 @@ pub async fn filter_enlisted_version_ids(
|
|||||||
pub async fn is_visible_collection(
|
pub async fn is_visible_collection(
|
||||||
collection_data: &DBCollection,
|
collection_data: &DBCollection,
|
||||||
user_option: &Option<User>,
|
user_option: &Option<User>,
|
||||||
|
hide_unlisted: bool,
|
||||||
) -> Result<bool, ApiError> {
|
) -> Result<bool, ApiError> {
|
||||||
let mut authorized = !collection_data.status.is_hidden()
|
let mut authorized = (if hide_unlisted {
|
||||||
&& !collection_data.projects.is_empty();
|
collection_data.status.is_searchable()
|
||||||
|
} else {
|
||||||
|
!collection_data.status.is_hidden()
|
||||||
|
}) && !collection_data.projects.is_empty();
|
||||||
if let Some(user) = &user_option {
|
if let Some(user) = &user_option {
|
||||||
if !authorized
|
if !authorized
|
||||||
&& (user.role.is_mod() || user.id == collection_data.user_id.into())
|
&& (user.role.is_mod() || user.id == collection_data.user_id.into())
|
||||||
@ -331,12 +335,17 @@ pub async fn is_visible_collection(
|
|||||||
pub async fn filter_visible_collections(
|
pub async fn filter_visible_collections(
|
||||||
collections: Vec<DBCollection>,
|
collections: Vec<DBCollection>,
|
||||||
user_option: &Option<User>,
|
user_option: &Option<User>,
|
||||||
|
hide_unlisted: bool,
|
||||||
) -> Result<Vec<crate::models::collections::Collection>, ApiError> {
|
) -> Result<Vec<crate::models::collections::Collection>, ApiError> {
|
||||||
let mut return_collections = Vec::new();
|
let mut return_collections = Vec::new();
|
||||||
let mut check_collections = Vec::new();
|
let mut check_collections = Vec::new();
|
||||||
|
|
||||||
for collection in collections {
|
for collection in collections {
|
||||||
if (!collection.status.is_hidden() && !collection.projects.is_empty())
|
if ((if hide_unlisted {
|
||||||
|
collection.status.is_searchable()
|
||||||
|
} else {
|
||||||
|
!collection.status.is_hidden()
|
||||||
|
}) && !collection.projects.is_empty())
|
||||||
|| user_option.as_ref().is_some_and(|x| x.role.is_mod())
|
|| user_option.as_ref().is_some_and(|x| x.role.is_mod())
|
||||||
{
|
{
|
||||||
return_collections.push(collection.into());
|
return_collections.push(collection.into());
|
||||||
|
|||||||
@ -92,7 +92,7 @@ impl CollectionStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project pages + info cannot be viewed
|
// Collection pages + info cannot be viewed
|
||||||
pub fn is_hidden(&self) -> bool {
|
pub fn is_hidden(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
CollectionStatus::Rejected => true,
|
CollectionStatus::Rejected => true,
|
||||||
@ -103,6 +103,11 @@ impl CollectionStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collection can be displayed in on user page
|
||||||
|
pub fn is_searchable(&self) -> bool {
|
||||||
|
matches!(self, CollectionStatus::Listed)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_approved(&self) -> bool {
|
pub fn is_approved(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
CollectionStatus::Listed => true,
|
CollectionStatus::Listed => true,
|
||||||
|
|||||||
@ -276,7 +276,11 @@ pub async fn refund_charge(
|
|||||||
subscription_interval: charge.subscription_interval,
|
subscription_interval: charge.subscription_interval,
|
||||||
payment_platform: charge.payment_platform,
|
payment_platform: charge.payment_platform,
|
||||||
payment_platform_id: id,
|
payment_platform_id: id,
|
||||||
parent_charge_id: Some(charge.id),
|
parent_charge_id: if refund_amount != 0 {
|
||||||
|
Some(charge.id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
net,
|
net,
|
||||||
}
|
}
|
||||||
.upsert(&mut transaction)
|
.upsert(&mut transaction)
|
||||||
|
|||||||
@ -18,12 +18,14 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ResultCount {
|
pub struct ProjectsRequestOptions {
|
||||||
#[serde(default = "default_count")]
|
#[serde(default = "default_count")]
|
||||||
pub count: i16,
|
pub count: u16,
|
||||||
|
#[serde(default)]
|
||||||
|
pub offset: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_count() -> i16 {
|
fn default_count() -> u16 {
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +33,7 @@ pub async fn get_projects(
|
|||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
count: web::Query<ResultCount>,
|
request_opts: web::Query<ProjectsRequestOptions>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
check_is_moderator_from_headers(
|
check_is_moderator_from_headers(
|
||||||
@ -50,10 +52,12 @@ pub async fn get_projects(
|
|||||||
SELECT id FROM mods
|
SELECT id FROM mods
|
||||||
WHERE status = $1
|
WHERE status = $1
|
||||||
ORDER BY queued ASC
|
ORDER BY queued ASC
|
||||||
LIMIT $2;
|
OFFSET $3
|
||||||
|
LIMIT $2
|
||||||
",
|
",
|
||||||
ProjectStatus::Processing.as_str(),
|
ProjectStatus::Processing.as_str(),
|
||||||
count.count as i64
|
request_opts.count as i64,
|
||||||
|
request_opts.offset as i64
|
||||||
)
|
)
|
||||||
.fetch(&**pool)
|
.fetch(&**pool)
|
||||||
.map_ok(|m| database::models::DBProjectId(m.id))
|
.map_ok(|m| database::models::DBProjectId(m.id))
|
||||||
|
|||||||
@ -15,10 +15,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ResultCount {
|
pub struct ResultCount {
|
||||||
#[serde(default = "default_count")]
|
#[serde(default = "default_count")]
|
||||||
pub count: i16,
|
pub count: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_count() -> i16 {
|
fn default_count() -> u16 {
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +34,10 @@ pub async fn get_projects(
|
|||||||
req,
|
req,
|
||||||
pool.clone(),
|
pool.clone(),
|
||||||
redis.clone(),
|
redis.clone(),
|
||||||
web::Query(internal::moderation::ResultCount { count: count.count }),
|
web::Query(internal::moderation::ProjectsRequestOptions {
|
||||||
|
count: count.count,
|
||||||
|
offset: 0,
|
||||||
|
}),
|
||||||
session_queue,
|
session_queue,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -43,12 +43,12 @@ pub async fn report_create(
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ReportsRequestOptions {
|
pub struct ReportsRequestOptions {
|
||||||
#[serde(default = "default_count")]
|
#[serde(default = "default_count")]
|
||||||
count: i16,
|
count: u16,
|
||||||
#[serde(default = "default_all")]
|
#[serde(default = "default_all")]
|
||||||
all: bool,
|
all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_count() -> i16 {
|
fn default_count() -> u16 {
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
fn default_all() -> bool {
|
fn default_all() -> bool {
|
||||||
@ -60,7 +60,7 @@ pub async fn reports(
|
|||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
count: web::Query<ReportsRequestOptions>,
|
request_opts: web::Query<ReportsRequestOptions>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let response = v3::reports::reports(
|
let response = v3::reports::reports(
|
||||||
@ -68,8 +68,9 @@ pub async fn reports(
|
|||||||
pool,
|
pool,
|
||||||
redis,
|
redis,
|
||||||
web::Query(v3::reports::ReportsRequestOptions {
|
web::Query(v3::reports::ReportsRequestOptions {
|
||||||
count: count.count,
|
count: request_opts.count,
|
||||||
all: count.all,
|
offset: 0,
|
||||||
|
all: request_opts.all,
|
||||||
}),
|
}),
|
||||||
session_queue,
|
session_queue,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -163,7 +163,8 @@ pub async fn collections_get(
|
|||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let collections =
|
let collections =
|
||||||
filter_visible_collections(collections_data, &user_option).await?;
|
filter_visible_collections(collections_data, &user_option, false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(collections))
|
Ok(HttpResponse::Ok().json(collections))
|
||||||
}
|
}
|
||||||
@ -192,7 +193,7 @@ pub async fn collection_get(
|
|||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
if let Some(data) = collection_data {
|
if let Some(data) = collection_data {
|
||||||
if is_visible_collection(&data, &user_option).await? {
|
if is_visible_collection(&data, &user_option, false).await? {
|
||||||
return Ok(HttpResponse::Ok().json(Collection::from(data)));
|
return Ok(HttpResponse::Ok().json(Collection::from(data)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -222,12 +222,14 @@ pub async fn report_create(
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ReportsRequestOptions {
|
pub struct ReportsRequestOptions {
|
||||||
#[serde(default = "default_count")]
|
#[serde(default = "default_count")]
|
||||||
pub count: i16,
|
pub count: u16,
|
||||||
|
#[serde(default)]
|
||||||
|
pub offset: u32,
|
||||||
#[serde(default = "default_all")]
|
#[serde(default = "default_all")]
|
||||||
pub all: bool,
|
pub all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_count() -> i16 {
|
fn default_count() -> u16 {
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
fn default_all() -> bool {
|
fn default_all() -> bool {
|
||||||
@ -238,7 +240,7 @@ pub async fn reports(
|
|||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
count: web::Query<ReportsRequestOptions>,
|
request_opts: web::Query<ReportsRequestOptions>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user = get_user_from_headers(
|
let user = get_user_from_headers(
|
||||||
@ -253,15 +255,17 @@ pub async fn reports(
|
|||||||
|
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
|
|
||||||
let report_ids = if user.role.is_mod() && count.all {
|
let report_ids = if user.role.is_mod() && request_opts.all {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT id FROM reports
|
SELECT id FROM reports
|
||||||
WHERE closed = FALSE
|
WHERE closed = FALSE
|
||||||
ORDER BY created ASC
|
ORDER BY created ASC
|
||||||
LIMIT $1;
|
OFFSET $2
|
||||||
|
LIMIT $1
|
||||||
",
|
",
|
||||||
count.count as i64
|
request_opts.count as i64,
|
||||||
|
request_opts.offset as i64
|
||||||
)
|
)
|
||||||
.fetch(&**pool)
|
.fetch(&**pool)
|
||||||
.map_ok(|m| crate::database::models::ids::DBReportId(m.id))
|
.map_ok(|m| crate::database::models::ids::DBReportId(m.id))
|
||||||
@ -273,10 +277,12 @@ pub async fn reports(
|
|||||||
SELECT id FROM reports
|
SELECT id FROM reports
|
||||||
WHERE closed = FALSE AND reporter = $1
|
WHERE closed = FALSE AND reporter = $1
|
||||||
ORDER BY created ASC
|
ORDER BY created ASC
|
||||||
LIMIT $2;
|
OFFSET $3
|
||||||
|
LIMIT $2
|
||||||
",
|
",
|
||||||
user.id.0 as i64,
|
user.id.0 as i64,
|
||||||
count.count as i64
|
request_opts.count as i64,
|
||||||
|
request_opts.offset as i64
|
||||||
)
|
)
|
||||||
.fetch(&**pool)
|
.fetch(&**pool)
|
||||||
.map_ok(|m| crate::database::models::ids::DBReportId(m.id))
|
.map_ok(|m| crate::database::models::ids::DBReportId(m.id))
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use super::{ApiError, oauth_clients::get_user_clients};
|
use super::{ApiError, oauth_clients::get_user_clients};
|
||||||
use crate::file_hosting::FileHostPublicity;
|
|
||||||
use crate::util::img::delete_old_images;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{filter_visible_projects, get_user_from_headers},
|
auth::{
|
||||||
|
filter_visible_collections, filter_visible_projects,
|
||||||
|
get_user_from_headers,
|
||||||
|
},
|
||||||
database::{models::DBUser, redis::RedisPool},
|
database::{models::DBUser, redis::RedisPool},
|
||||||
file_hosting::FileHost,
|
file_hosting::{FileHost, FileHostPublicity},
|
||||||
models::{
|
models::{
|
||||||
collections::{Collection, CollectionStatus},
|
|
||||||
notifications::Notification,
|
notifications::Notification,
|
||||||
pats::Scopes,
|
pats::Scopes,
|
||||||
projects::Project,
|
projects::Project,
|
||||||
@ -16,7 +16,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
queue::session::AuthQueue,
|
queue::session::AuthQueue,
|
||||||
util::{
|
util::{
|
||||||
routes::read_limited_from_payload,
|
img::delete_old_images, routes::read_limited_from_payload,
|
||||||
validate::validation_errors_to_string,
|
validate::validation_errors_to_string,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -244,27 +244,19 @@ pub async fn collections_list(
|
|||||||
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
|
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||||
|
|
||||||
if let Some(id) = id_option.map(|x| x.id) {
|
if let Some(id) = id_option.map(|x| x.id) {
|
||||||
let user_id: UserId = id.into();
|
let collection_data = DBUser::get_collections(id, &**pool).await?;
|
||||||
|
|
||||||
let can_view_private =
|
|
||||||
user.is_some_and(|y| y.role.is_mod() || y.id == user_id);
|
|
||||||
|
|
||||||
let project_data = DBUser::get_collections(id, &**pool).await?;
|
|
||||||
|
|
||||||
let response: Vec<_> = crate::database::models::DBCollection::get_many(
|
let response: Vec<_> = crate::database::models::DBCollection::get_many(
|
||||||
&project_data,
|
&collection_data,
|
||||||
&**pool,
|
&**pool,
|
||||||
&redis,
|
&redis,
|
||||||
)
|
)
|
||||||
.await?
|
.await?;
|
||||||
.into_iter()
|
|
||||||
.filter(|x| {
|
|
||||||
can_view_private || matches!(x.status, CollectionStatus::Listed)
|
|
||||||
})
|
|
||||||
.map(Collection::from)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(response))
|
let collections =
|
||||||
|
filter_visible_collections(response, &user, true).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(collections))
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError::NotFound)
|
Err(ApiError::NotFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,10 @@
|
|||||||
# SQLite database file location
|
MODRINTH_URL=http://localhost:3000/
|
||||||
DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
MODRINTH_API_URL=http://127.0.0.1:8000/v2/
|
||||||
|
MODRINTH_API_URL_V3=http://127.0.0.1:8000/v3/
|
||||||
|
MODRINTH_SOCKET_URL=ws://127.0.0.1:8000/
|
||||||
|
MODRINTH_LAUNCHER_META_URL=https://launcher-meta.modrinth.com/
|
||||||
|
|
||||||
|
# SQLite database file used by sqlx for type checking. Uncomment this to a valid path
|
||||||
|
# in your system and run `cargo sqlx database setup` to generate an empty database that
|
||||||
|
# can be used for developing the app DB schema
|
||||||
|
#DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
||||||
|
|||||||
10
packages/app-lib/.env.prod
Normal file
10
packages/app-lib/.env.prod
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
MODRINTH_URL=https://modrinth.com/
|
||||||
|
MODRINTH_API_URL=https://api.modrinth.com/v2/
|
||||||
|
MODRINTH_API_URL_V3=https://api.modrinth.com/v3/
|
||||||
|
MODRINTH_SOCKET_URL=wss://api.modrinth.com/
|
||||||
|
MODRINTH_LAUNCHER_META_URL=https://launcher-meta.modrinth.com/
|
||||||
|
|
||||||
|
# SQLite database file used by sqlx for type checking. Uncomment this to a valid path
|
||||||
|
# in your system and run `cargo sqlx database setup` to generate an empty database that
|
||||||
|
# can be used for developing the app DB schema
|
||||||
|
#DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
||||||
10
packages/app-lib/.env.staging
Normal file
10
packages/app-lib/.env.staging
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
MODRINTH_URL=https://staging.modrinth.com/
|
||||||
|
MODRINTH_API_URL=https://staging-api.modrinth.com/v2/
|
||||||
|
MODRINTH_API_URL_V3=https://staging-api.modrinth.com/v3/
|
||||||
|
MODRINTH_SOCKET_URL=wss://staging-api.modrinth.com/
|
||||||
|
MODRINTH_LAUNCHER_META_URL=https://launcher-meta.modrinth.com/
|
||||||
|
|
||||||
|
# SQLite database file used by sqlx for type checking. Uncomment this to a valid path
|
||||||
|
# in your system and run `cargo sqlx database setup` to generate an empty database that
|
||||||
|
# can be used for developing the app DB schema
|
||||||
|
#DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
||||||
@ -27,6 +27,7 @@ hashlink.workspace = true
|
|||||||
png.workspace = true
|
png.workspace = true
|
||||||
bytemuck.workspace = true
|
bytemuck.workspace = true
|
||||||
rgb.workspace = true
|
rgb.workspace = true
|
||||||
|
phf.workspace = true
|
||||||
|
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
daedalus.workspace = true
|
daedalus.workspace = true
|
||||||
@ -82,6 +83,7 @@ ariadne.workspace = true
|
|||||||
winreg.workspace = true
|
winreg.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
dotenvy.workspace = true
|
||||||
dunce.workspace = true
|
dunce.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@ -4,12 +4,31 @@ use std::process::{Command, exit};
|
|||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
println!("cargo::rerun-if-changed=.env");
|
||||||
println!("cargo::rerun-if-changed=java/gradle");
|
println!("cargo::rerun-if-changed=java/gradle");
|
||||||
println!("cargo::rerun-if-changed=java/src");
|
println!("cargo::rerun-if-changed=java/src");
|
||||||
println!("cargo::rerun-if-changed=java/build.gradle.kts");
|
println!("cargo::rerun-if-changed=java/build.gradle.kts");
|
||||||
println!("cargo::rerun-if-changed=java/settings.gradle.kts");
|
println!("cargo::rerun-if-changed=java/settings.gradle.kts");
|
||||||
println!("cargo::rerun-if-changed=java/gradle.properties");
|
println!("cargo::rerun-if-changed=java/gradle.properties");
|
||||||
|
|
||||||
|
set_env();
|
||||||
|
build_java_jars();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_env() {
|
||||||
|
for (var_name, var_value) in
|
||||||
|
dotenvy::dotenv_iter().into_iter().flatten().flatten()
|
||||||
|
{
|
||||||
|
if var_name == "DATABASE_URL" {
|
||||||
|
// The sqlx database URL is a build-time detail that should not be exposed to the crate
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo::rustc-env={var_name}={var_value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_java_jars() {
|
||||||
let out_dir =
|
let out_dir =
|
||||||
dunce::canonicalize(PathBuf::from(env::var_os("OUT_DIR").unwrap()))
|
dunce::canonicalize(PathBuf::from(env::var_os("OUT_DIR").unwrap()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -37,6 +56,7 @@ fn main() {
|
|||||||
.current_dir(dunce::canonicalize("java").unwrap())
|
.current_dir(dunce::canonicalize("java").unwrap())
|
||||||
.status()
|
.status()
|
||||||
.expect("Failed to wait on Gradle build");
|
.expect("Failed to wait on Gradle build");
|
||||||
|
|
||||||
if !exit_status.success() {
|
if !exit_status.success() {
|
||||||
println!("cargo::error=Gradle build failed with {exit_status}");
|
println!("cargo::error=Gradle build failed with {exit_status}");
|
||||||
exit(exit_status.code().unwrap_or(1));
|
exit(exit_status.code().unwrap_or(1));
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::state::ModrinthCredentials;
|
use crate::state::ModrinthCredentials;
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub fn authenticate_begin_flow() -> String {
|
pub fn authenticate_begin_flow() -> &'static str {
|
||||||
crate::state::get_login_url()
|
crate::state::get_login_url()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ use crate::state::attached_world_data::AttachedWorldData;
|
|||||||
use crate::state::{
|
use crate::state::{
|
||||||
Profile, ProfileInstallStage, attached_world_data, server_join_log,
|
Profile, ProfileInstallStage, attached_world_data, server_join_log,
|
||||||
};
|
};
|
||||||
|
use crate::util::protocol_version::OLD_PROTOCOL_VERSIONS;
|
||||||
|
pub use crate::util::protocol_version::ProtocolVersion;
|
||||||
pub use crate::util::server_ping::{
|
pub use crate::util::server_ping::{
|
||||||
ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion,
|
ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion,
|
||||||
};
|
};
|
||||||
@ -835,7 +837,7 @@ mod servers_data {
|
|||||||
|
|
||||||
pub async fn get_profile_protocol_version(
|
pub async fn get_profile_protocol_version(
|
||||||
profile: &str,
|
profile: &str,
|
||||||
) -> Result<Option<i32>> {
|
) -> Result<Option<ProtocolVersion>> {
|
||||||
let mut profile = super::profile::get(profile).await?.ok_or_else(|| {
|
let mut profile = super::profile::get(profile).await?.ok_or_else(|| {
|
||||||
ErrorKind::UnmanagedProfileError(format!(
|
ErrorKind::UnmanagedProfileError(format!(
|
||||||
"Could not find profile {profile}"
|
"Could not find profile {profile}"
|
||||||
@ -846,7 +848,12 @@ pub async fn get_profile_protocol_version(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(protocol_version) = profile.protocol_version {
|
if let Some(protocol_version) = profile.protocol_version {
|
||||||
return Ok(Some(protocol_version));
|
return Ok(Some(ProtocolVersion::modern(protocol_version)));
|
||||||
|
}
|
||||||
|
if let Some(protocol_version) =
|
||||||
|
OLD_PROTOCOL_VERSIONS.get(&profile.game_version)
|
||||||
|
{
|
||||||
|
return Ok(Some(*protocol_version));
|
||||||
}
|
}
|
||||||
|
|
||||||
let minecraft = crate::api::metadata::get_minecraft_versions().await?;
|
let minecraft = crate::api::metadata::get_minecraft_versions().await?;
|
||||||
@ -854,7 +861,7 @@ pub async fn get_profile_protocol_version(
|
|||||||
.versions
|
.versions
|
||||||
.iter()
|
.iter()
|
||||||
.position(|it| it.id == profile.game_version)
|
.position(|it| it.id == profile.game_version)
|
||||||
.ok_or(crate::ErrorKind::LauncherError(format!(
|
.ok_or(ErrorKind::LauncherError(format!(
|
||||||
"Invalid game version: {}",
|
"Invalid game version: {}",
|
||||||
profile.game_version
|
profile.game_version
|
||||||
)))?;
|
)))?;
|
||||||
@ -890,16 +897,19 @@ pub async fn get_profile_protocol_version(
|
|||||||
profile.protocol_version = version;
|
profile.protocol_version = version;
|
||||||
profile.upsert(&state.pool).await?;
|
profile.upsert(&state.pool).await?;
|
||||||
}
|
}
|
||||||
Ok(version)
|
Ok(version.map(ProtocolVersion::modern))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_server_status(
|
pub async fn get_server_status(
|
||||||
address: &str,
|
address: &str,
|
||||||
protocol_version: Option<i32>,
|
protocol_version: Option<ProtocolVersion>,
|
||||||
) -> Result<ServerStatus> {
|
) -> Result<ServerStatus> {
|
||||||
let (original_host, original_port) = parse_server_address(address)?;
|
let (original_host, original_port) = parse_server_address(address)?;
|
||||||
let (host, port) =
|
let (host, port) =
|
||||||
resolve_server_address(original_host, original_port).await?;
|
resolve_server_address(original_host, original_port).await?;
|
||||||
|
tracing::debug!(
|
||||||
|
"Pinging {address} with protocol version {protocol_version:?}"
|
||||||
|
);
|
||||||
server_ping::get_server_status(
|
server_ping::get_server_status(
|
||||||
&(&host as &str, port),
|
&(&host as &str, port),
|
||||||
(original_host, original_port),
|
(original_host, original_port),
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
//! Configuration structs
|
|
||||||
|
|
||||||
// pub const MODRINTH_URL: &str = "https://staging.modrinth.com/";
|
|
||||||
// pub const MODRINTH_API_URL: &str = "https://staging-api.modrinth.com/v2/";
|
|
||||||
// pub const MODRINTH_API_URL_V3: &str = "https://staging-api.modrinth.com/v3/";
|
|
||||||
|
|
||||||
pub const MODRINTH_URL: &str = "https://modrinth.com/";
|
|
||||||
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
|
|
||||||
pub const MODRINTH_API_URL_V3: &str = "https://api.modrinth.com/v3/";
|
|
||||||
|
|
||||||
pub const MODRINTH_SOCKET_URL: &str = "wss://api.modrinth.com/";
|
|
||||||
|
|
||||||
pub const META_URL: &str = "https://launcher-meta.modrinth.com/";
|
|
||||||
@ -419,7 +419,7 @@ pub async fn install_minecraft(
|
|||||||
|
|
||||||
pub async fn read_protocol_version_from_jar(
|
pub async fn read_protocol_version_from_jar(
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
) -> crate::Result<Option<i32>> {
|
) -> crate::Result<Option<u32>> {
|
||||||
let zip = async_zip::tokio::read::fs::ZipFileReader::new(path).await?;
|
let zip = async_zip::tokio::read::fs::ZipFileReader::new(path).await?;
|
||||||
let Some(entry_index) = zip
|
let Some(entry_index) = zip
|
||||||
.file()
|
.file()
|
||||||
@ -432,7 +432,7 @@ pub async fn read_protocol_version_from_jar(
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct VersionData {
|
struct VersionData {
|
||||||
protocol_version: Option<i32>,
|
protocol_version: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut data = vec![];
|
let mut data = vec![];
|
||||||
|
|||||||
@ -11,7 +11,6 @@ and launching Modrinth mod packs
|
|||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod config;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod event;
|
mod event;
|
||||||
mod launcher;
|
mod launcher;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
use crate::config::{META_URL, MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
|
||||||
use crate::state::ProjectType;
|
use crate::state::ProjectType;
|
||||||
use crate::util::fetch::{FetchSemaphore, fetch_json, sha1_async};
|
use crate::util::fetch::{FetchSemaphore, fetch_json, sha1_async};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
@ -8,6 +7,7 @@ use serde::de::DeserializeOwned;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@ -945,7 +945,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Project => {
|
CacheValueType::Project => {
|
||||||
fetch_original_values!(
|
fetch_original_values!(
|
||||||
Project,
|
Project,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"projects",
|
"projects",
|
||||||
CacheValue::Project
|
CacheValue::Project
|
||||||
)
|
)
|
||||||
@ -953,7 +953,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Version => {
|
CacheValueType::Version => {
|
||||||
fetch_original_values!(
|
fetch_original_values!(
|
||||||
Version,
|
Version,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"versions",
|
"versions",
|
||||||
CacheValue::Version
|
CacheValue::Version
|
||||||
)
|
)
|
||||||
@ -961,7 +961,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::User => {
|
CacheValueType::User => {
|
||||||
fetch_original_values!(
|
fetch_original_values!(
|
||||||
User,
|
User,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"users",
|
"users",
|
||||||
CacheValue::User
|
CacheValue::User
|
||||||
)
|
)
|
||||||
@ -969,7 +969,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Team => {
|
CacheValueType::Team => {
|
||||||
let mut teams = fetch_many_batched::<Vec<TeamMember>>(
|
let mut teams = fetch_many_batched::<Vec<TeamMember>>(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
MODRINTH_API_URL_V3,
|
env!("MODRINTH_API_URL_V3"),
|
||||||
"teams?ids=",
|
"teams?ids=",
|
||||||
&keys,
|
&keys,
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
@ -1008,7 +1008,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Organization => {
|
CacheValueType::Organization => {
|
||||||
let mut orgs = fetch_many_batched::<Organization>(
|
let mut orgs = fetch_many_batched::<Organization>(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
MODRINTH_API_URL_V3,
|
env!("MODRINTH_API_URL_V3"),
|
||||||
"organizations?ids=",
|
"organizations?ids=",
|
||||||
&keys,
|
&keys,
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
@ -1063,7 +1063,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::File => {
|
CacheValueType::File => {
|
||||||
let mut versions = fetch_json::<HashMap<String, Version>>(
|
let mut versions = fetch_json::<HashMap<String, Version>>(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("{MODRINTH_API_URL}version_files"),
|
concat!(env!("MODRINTH_API_URL"), "version_files"),
|
||||||
None,
|
None,
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"algorithm": "sha1",
|
"algorithm": "sha1",
|
||||||
@ -1119,7 +1119,11 @@ impl CachedEntry {
|
|||||||
.map(|x| {
|
.map(|x| {
|
||||||
(
|
(
|
||||||
x.key().to_string(),
|
x.key().to_string(),
|
||||||
format!("{META_URL}{}/v0/manifest.json", x.key()),
|
format!(
|
||||||
|
"{}{}/v0/manifest.json",
|
||||||
|
env!("MODRINTH_LAUNCHER_META_URL"),
|
||||||
|
x.key()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -1154,7 +1158,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::MinecraftManifest => {
|
CacheValueType::MinecraftManifest => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
MinecraftManifest,
|
MinecraftManifest,
|
||||||
META_URL,
|
env!("MODRINTH_LAUNCHER_META_URL"),
|
||||||
format!(
|
format!(
|
||||||
"minecraft/v{}/manifest.json",
|
"minecraft/v{}/manifest.json",
|
||||||
daedalus::minecraft::CURRENT_FORMAT_VERSION
|
daedalus::minecraft::CURRENT_FORMAT_VERSION
|
||||||
@ -1165,7 +1169,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Categories => {
|
CacheValueType::Categories => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
Categories,
|
Categories,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/category",
|
"tag/category",
|
||||||
CacheValue::Categories
|
CacheValue::Categories
|
||||||
)
|
)
|
||||||
@ -1173,7 +1177,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::ReportTypes => {
|
CacheValueType::ReportTypes => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
ReportTypes,
|
ReportTypes,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/report_type",
|
"tag/report_type",
|
||||||
CacheValue::ReportTypes
|
CacheValue::ReportTypes
|
||||||
)
|
)
|
||||||
@ -1181,7 +1185,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Loaders => {
|
CacheValueType::Loaders => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
Loaders,
|
Loaders,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/loader",
|
"tag/loader",
|
||||||
CacheValue::Loaders
|
CacheValue::Loaders
|
||||||
)
|
)
|
||||||
@ -1189,7 +1193,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::GameVersions => {
|
CacheValueType::GameVersions => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
GameVersions,
|
GameVersions,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/game_version",
|
"tag/game_version",
|
||||||
CacheValue::GameVersions
|
CacheValue::GameVersions
|
||||||
)
|
)
|
||||||
@ -1197,7 +1201,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::DonationPlatforms => {
|
CacheValueType::DonationPlatforms => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
DonationPlatforms,
|
DonationPlatforms,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/donation_platform",
|
"tag/donation_platform",
|
||||||
CacheValue::DonationPlatforms
|
CacheValue::DonationPlatforms
|
||||||
)
|
)
|
||||||
@ -1297,14 +1301,12 @@ impl CachedEntry {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let version_update_url =
|
|
||||||
format!("{MODRINTH_API_URL}version_files/update");
|
|
||||||
let variations =
|
let variations =
|
||||||
futures::future::try_join_all(filtered_keys.iter().map(
|
futures::future::try_join_all(filtered_keys.iter().map(
|
||||||
|((loaders_key, game_version), hashes)| {
|
|((loaders_key, game_version), hashes)| {
|
||||||
fetch_json::<HashMap<String, Version>>(
|
fetch_json::<HashMap<String, Version>>(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&version_update_url,
|
concat!(env!("MODRINTH_API_URL"), "version_files/update"),
|
||||||
None,
|
None,
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"algorithm": "sha1",
|
"algorithm": "sha1",
|
||||||
@ -1368,7 +1370,11 @@ impl CachedEntry {
|
|||||||
.map(|x| {
|
.map(|x| {
|
||||||
(
|
(
|
||||||
x.key().to_string(),
|
x.key().to_string(),
|
||||||
format!("{MODRINTH_API_URL}search{}", x.key()),
|
format!(
|
||||||
|
"{}search{}",
|
||||||
|
env!("MODRINTH_API_URL"),
|
||||||
|
x.key()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
use crate::config::{MODRINTH_API_URL_V3, MODRINTH_SOCKET_URL};
|
|
||||||
use crate::data::ModrinthCredentials;
|
use crate::data::ModrinthCredentials;
|
||||||
use crate::event::FriendPayload;
|
use crate::event::FriendPayload;
|
||||||
use crate::event::emit::emit_friend;
|
use crate::event::emit::emit_friend;
|
||||||
@ -77,7 +76,8 @@ impl FriendsSocket {
|
|||||||
|
|
||||||
if let Some(credentials) = credentials {
|
if let Some(credentials) = credentials {
|
||||||
let mut request = format!(
|
let mut request = format!(
|
||||||
"{MODRINTH_SOCKET_URL}_internal/launcher_socket?code={}",
|
"{}_internal/launcher_socket?code={}",
|
||||||
|
env!("MODRINTH_SOCKET_URL"),
|
||||||
credentials.session
|
credentials.session
|
||||||
)
|
)
|
||||||
.into_client_request()?;
|
.into_client_request()?;
|
||||||
@ -303,7 +303,7 @@ impl FriendsSocket {
|
|||||||
) -> crate::Result<Vec<UserFriend>> {
|
) -> crate::Result<Vec<UserFriend>> {
|
||||||
fetch_json(
|
fetch_json(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
&format!("{MODRINTH_API_URL_V3}friends"),
|
concat!(env!("MODRINTH_API_URL_V3"), "friends"),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
@ -328,7 +328,7 @@ impl FriendsSocket {
|
|||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
fetch_advanced(
|
fetch_advanced(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("{MODRINTH_API_URL_V3}friend/{user_id}"),
|
&format!("{}friend/{user_id}", env!("MODRINTH_API_URL_V3")),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@ -349,7 +349,7 @@ impl FriendsSocket {
|
|||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
fetch_advanced(
|
fetch_advanced(
|
||||||
Method::DELETE,
|
Method::DELETE,
|
||||||
&format!("{MODRINTH_API_URL_V3}friend/{user_id}"),
|
&format!("{}friend/{user_id}", env!("MODRINTH_API_URL_V3")),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
|||||||
@ -85,21 +85,18 @@ pub struct MinecraftLoginFlow {
|
|||||||
pub verifier: String,
|
pub verifier: String,
|
||||||
pub challenge: String,
|
pub challenge: String,
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
pub redirect_uri: String,
|
pub auth_request_uri: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn login_begin(
|
pub async fn login_begin(
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||||
) -> crate::Result<MinecraftLoginFlow> {
|
) -> crate::Result<MinecraftLoginFlow> {
|
||||||
let (pair, current_date, valid_date) =
|
let (pair, current_date) =
|
||||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), false, exec)
|
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), exec).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let verifier = generate_oauth_challenge();
|
let verifier = generate_oauth_challenge();
|
||||||
let mut hasher = sha2::Sha256::new();
|
let result = sha2::Sha256::digest(&verifier);
|
||||||
hasher.update(&verifier);
|
|
||||||
let result = hasher.finalize();
|
|
||||||
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
|
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
|
||||||
|
|
||||||
match sisu_authenticate(
|
match sisu_authenticate(
|
||||||
@ -110,46 +107,15 @@ pub async fn login_begin(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((session_id, redirect_uri)) => Ok(MinecraftLoginFlow {
|
Ok((session_id, redirect_uri)) => {
|
||||||
|
return Ok(MinecraftLoginFlow {
|
||||||
verifier,
|
verifier,
|
||||||
challenge,
|
challenge,
|
||||||
session_id,
|
session_id,
|
||||||
redirect_uri: redirect_uri.value.msa_oauth_redirect,
|
auth_request_uri: redirect_uri.value.msa_oauth_redirect,
|
||||||
}),
|
});
|
||||||
Err(err) => {
|
|
||||||
if !valid_date {
|
|
||||||
let (pair, current_date, _) =
|
|
||||||
DeviceTokenPair::refresh_and_get_device_token(
|
|
||||||
Utc::now(),
|
|
||||||
false,
|
|
||||||
exec,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let verifier = generate_oauth_challenge();
|
|
||||||
let mut hasher = sha2::Sha256::new();
|
|
||||||
hasher.update(&verifier);
|
|
||||||
let result = hasher.finalize();
|
|
||||||
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
|
|
||||||
|
|
||||||
let (session_id, redirect_uri) = sisu_authenticate(
|
|
||||||
&pair.token.token,
|
|
||||||
&challenge,
|
|
||||||
&pair.key,
|
|
||||||
current_date,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(MinecraftLoginFlow {
|
|
||||||
verifier,
|
|
||||||
challenge,
|
|
||||||
session_id,
|
|
||||||
redirect_uri: redirect_uri.value.msa_oauth_redirect,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(crate::ErrorKind::from(err).into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Err(err) => return Err(crate::ErrorKind::from(err).into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,9 +125,8 @@ pub async fn login_finish(
|
|||||||
flow: MinecraftLoginFlow,
|
flow: MinecraftLoginFlow,
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||||
) -> crate::Result<Credentials> {
|
) -> crate::Result<Credentials> {
|
||||||
let (pair, _, _) =
|
let (pair, _) =
|
||||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), false, exec)
|
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), exec).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let oauth_token = oauth_token(code, &flow.verifier).await?;
|
let oauth_token = oauth_token(code, &flow.verifier).await?;
|
||||||
let sisu_authorize = sisu_authorize(
|
let sisu_authorize = sisu_authorize(
|
||||||
@ -267,10 +232,9 @@ impl Credentials {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let oauth_token = oauth_refresh(&self.refresh_token).await?;
|
let oauth_token = oauth_refresh(&self.refresh_token).await?;
|
||||||
let (pair, current_date, _) =
|
let (pair, current_date) =
|
||||||
DeviceTokenPair::refresh_and_get_device_token(
|
DeviceTokenPair::refresh_and_get_device_token(
|
||||||
oauth_token.date,
|
oauth_token.date,
|
||||||
false,
|
|
||||||
exec,
|
exec,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -633,21 +597,20 @@ impl DeviceTokenPair {
|
|||||||
#[tracing::instrument(skip(exec))]
|
#[tracing::instrument(skip(exec))]
|
||||||
async fn refresh_and_get_device_token(
|
async fn refresh_and_get_device_token(
|
||||||
current_date: DateTime<Utc>,
|
current_date: DateTime<Utc>,
|
||||||
force_generate: bool,
|
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||||
) -> crate::Result<(Self, DateTime<Utc>, bool)> {
|
) -> crate::Result<(Self, DateTime<Utc>)> {
|
||||||
let pair = Self::get(exec).await?;
|
let pair = Self::get(exec).await?;
|
||||||
|
|
||||||
if let Some(mut pair) = pair {
|
if let Some(mut pair) = pair {
|
||||||
if pair.token.not_after > Utc::now() && !force_generate {
|
if pair.token.not_after > current_date {
|
||||||
Ok((pair, current_date, false))
|
Ok((pair, current_date))
|
||||||
} else {
|
} else {
|
||||||
let res = device_token(&pair.key, current_date).await?;
|
let res = device_token(&pair.key, current_date).await?;
|
||||||
|
|
||||||
pair.token = res.value;
|
pair.token = res.value;
|
||||||
pair.upsert(exec).await?;
|
pair.upsert(exec).await?;
|
||||||
|
|
||||||
Ok((pair, res.date, true))
|
Ok((pair, res.date))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let key = generate_key()?;
|
let key = generate_key()?;
|
||||||
@ -660,7 +623,7 @@ impl DeviceTokenPair {
|
|||||||
|
|
||||||
pair.upsert(exec).await?;
|
pair.upsert(exec).await?;
|
||||||
|
|
||||||
Ok((pair, res.date, true))
|
Ok((pair, res.date))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -758,8 +721,8 @@ impl DeviceTokenPair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
|
const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
|
||||||
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
const AUTH_REPLY_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||||
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
const REQUESTED_SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||||
|
|
||||||
struct RequestWithDate<T> {
|
struct RequestWithDate<T> {
|
||||||
pub date: DateTime<Utc>,
|
pub date: DateTime<Utc>,
|
||||||
@ -838,7 +801,7 @@ async fn sisu_authenticate(
|
|||||||
"AppId": MICROSOFT_CLIENT_ID,
|
"AppId": MICROSOFT_CLIENT_ID,
|
||||||
"DeviceToken": token,
|
"DeviceToken": token,
|
||||||
"Offers": [
|
"Offers": [
|
||||||
REQUESTED_SCOPES
|
REQUESTED_SCOPE
|
||||||
],
|
],
|
||||||
"Query": {
|
"Query": {
|
||||||
"code_challenge": challenge,
|
"code_challenge": challenge,
|
||||||
@ -846,7 +809,7 @@ async fn sisu_authenticate(
|
|||||||
"state": generate_oauth_challenge(),
|
"state": generate_oauth_challenge(),
|
||||||
"prompt": "select_account"
|
"prompt": "select_account"
|
||||||
},
|
},
|
||||||
"RedirectUri": REDIRECT_URL,
|
"RedirectUri": AUTH_REPLY_URL,
|
||||||
"Sandbox": "RETAIL",
|
"Sandbox": "RETAIL",
|
||||||
"TokenType": "code",
|
"TokenType": "code",
|
||||||
"TitleId": "1794566092",
|
"TitleId": "1794566092",
|
||||||
@ -890,12 +853,12 @@ async fn oauth_token(
|
|||||||
verifier: &str,
|
verifier: &str,
|
||||||
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
|
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
|
||||||
let mut query = HashMap::new();
|
let mut query = HashMap::new();
|
||||||
query.insert("client_id", "00000000402b5328");
|
query.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||||
query.insert("code", code);
|
query.insert("code", code);
|
||||||
query.insert("code_verifier", verifier);
|
query.insert("code_verifier", verifier);
|
||||||
query.insert("grant_type", "authorization_code");
|
query.insert("grant_type", "authorization_code");
|
||||||
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
|
query.insert("redirect_uri", AUTH_REPLY_URL);
|
||||||
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
|
query.insert("scope", REQUESTED_SCOPE);
|
||||||
|
|
||||||
let res = auth_retry(|| {
|
let res = auth_retry(|| {
|
||||||
REQWEST_CLIENT
|
REQWEST_CLIENT
|
||||||
@ -939,11 +902,11 @@ async fn oauth_refresh(
|
|||||||
refresh_token: &str,
|
refresh_token: &str,
|
||||||
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
|
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
|
||||||
let mut query = HashMap::new();
|
let mut query = HashMap::new();
|
||||||
query.insert("client_id", "00000000402b5328");
|
query.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||||
query.insert("refresh_token", refresh_token);
|
query.insert("refresh_token", refresh_token);
|
||||||
query.insert("grant_type", "refresh_token");
|
query.insert("grant_type", "refresh_token");
|
||||||
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
|
query.insert("redirect_uri", AUTH_REPLY_URL);
|
||||||
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
|
query.insert("scope", REQUESTED_SCOPE);
|
||||||
|
|
||||||
let res = auth_retry(|| {
|
let res = auth_retry(|| {
|
||||||
REQWEST_CLIENT
|
REQWEST_CLIENT
|
||||||
@ -1007,7 +970,7 @@ async fn sisu_authorize(
|
|||||||
"/authorize",
|
"/authorize",
|
||||||
json!({
|
json!({
|
||||||
"AccessToken": format!("t={access_token}"),
|
"AccessToken": format!("t={access_token}"),
|
||||||
"AppId": "00000000402b5328",
|
"AppId": MICROSOFT_CLIENT_ID,
|
||||||
"DeviceToken": device_token,
|
"DeviceToken": device_token,
|
||||||
"ProofKey": {
|
"ProofKey": {
|
||||||
"kty": "EC",
|
"kty": "EC",
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
use crate::config::{MODRINTH_API_URL, MODRINTH_URL};
|
|
||||||
use crate::state::{CacheBehaviour, CachedEntry};
|
use crate::state::{CacheBehaviour, CachedEntry};
|
||||||
use crate::util::fetch::{FetchSemaphore, fetch_advanced};
|
use crate::util::fetch::{FetchSemaphore, fetch_advanced};
|
||||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||||
@ -31,7 +30,7 @@ impl ModrinthCredentials {
|
|||||||
|
|
||||||
let resp = fetch_advanced(
|
let resp = fetch_advanced(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("{MODRINTH_API_URL}session/refresh"),
|
concat!(env!("MODRINTH_API_URL"), "session/refresh"),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(("Authorization", &*creds.session)),
|
Some(("Authorization", &*creds.session)),
|
||||||
@ -190,8 +189,8 @@ impl ModrinthCredentials {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_login_url() -> String {
|
pub const fn get_login_url() -> &'static str {
|
||||||
format!("{MODRINTH_URL}auth/sign-in?launcher=true")
|
concat!(env!("MODRINTH_URL"), "auth/sign-in")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn finish_login_flow(
|
pub async fn finish_login_flow(
|
||||||
@ -199,6 +198,12 @@ pub async fn finish_login_flow(
|
|||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||||
) -> crate::Result<ModrinthCredentials> {
|
) -> crate::Result<ModrinthCredentials> {
|
||||||
|
// The authorization code actually is the access token, since Labrinth doesn't
|
||||||
|
// issue separate authorization codes. Therefore, this is equivalent to an
|
||||||
|
// implicit OAuth grant flow, and no additional exchanging or finalization is
|
||||||
|
// needed. TODO not do this for the reasons outlined at
|
||||||
|
// https://oauth.net/2/grant-types/implicit/
|
||||||
|
|
||||||
let info = fetch_info(code, semaphore, exec).await?;
|
let info = fetch_info(code, semaphore, exec).await?;
|
||||||
|
|
||||||
Ok(ModrinthCredentials {
|
Ok(ModrinthCredentials {
|
||||||
@ -216,7 +221,7 @@ async fn fetch_info(
|
|||||||
) -> crate::Result<crate::state::cache::User> {
|
) -> crate::Result<crate::state::cache::User> {
|
||||||
let result = fetch_advanced(
|
let result = fetch_advanced(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
&format!("{MODRINTH_API_URL}user"),
|
concat!(env!("MODRINTH_API_URL"), "user"),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(("Authorization", token)),
|
Some(("Authorization", token)),
|
||||||
|
|||||||
@ -32,7 +32,7 @@ pub struct Profile {
|
|||||||
pub icon_path: Option<String>,
|
pub icon_path: Option<String>,
|
||||||
|
|
||||||
pub game_version: String,
|
pub game_version: String,
|
||||||
pub protocol_version: Option<i32>,
|
pub protocol_version: Option<u32>,
|
||||||
pub loader: ModLoader,
|
pub loader: ModLoader,
|
||||||
pub loader_version: Option<String>,
|
pub loader_version: Option<String>,
|
||||||
|
|
||||||
@ -320,7 +320,7 @@ impl TryFrom<ProfileQueryResult> for Profile {
|
|||||||
name: x.name,
|
name: x.name,
|
||||||
icon_path: x.icon_path,
|
icon_path: x.icon_path,
|
||||||
game_version: x.game_version,
|
game_version: x.game_version,
|
||||||
protocol_version: x.protocol_version.map(|x| x as i32),
|
protocol_version: x.protocol_version.map(|x| x as u32),
|
||||||
loader: ModLoader::from_string(&x.mod_loader),
|
loader: ModLoader::from_string(&x.mod_loader),
|
||||||
loader_version: x.mod_loader_version,
|
loader_version: x.mod_loader_version,
|
||||||
groups: serde_json::from_value(x.groups).unwrap_or_default(),
|
groups: serde_json::from_value(x.groups).unwrap_or_default(),
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
//! Functions for fetching information from the Internet
|
//! Functions for fetching information from the Internet
|
||||||
use super::io::{self, IOError};
|
use super::io::{self, IOError};
|
||||||
use crate::config::{MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
|
||||||
use crate::event::LoadingBarId;
|
use crate::event::LoadingBarId;
|
||||||
use crate::event::emit::emit_loading;
|
use crate::event::emit::emit_loading;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
@ -84,8 +83,8 @@ pub async fn fetch_advanced(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_none_or(|x| &*x.0.to_lowercase() != "authorization")
|
.is_none_or(|x| &*x.0.to_lowercase() != "authorization")
|
||||||
&& (url.starts_with("https://cdn.modrinth.com")
|
&& (url.starts_with("https://cdn.modrinth.com")
|
||||||
|| url.starts_with(MODRINTH_API_URL)
|
|| url.starts_with(env!("MODRINTH_API_URL"))
|
||||||
|| url.starts_with(MODRINTH_API_URL_V3))
|
|| url.starts_with(env!("MODRINTH_API_URL_V3")))
|
||||||
{
|
{
|
||||||
crate::state::ModrinthCredentials::get_active(exec).await?
|
crate::state::ModrinthCredentials::get_active(exec).await?
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -3,4 +3,5 @@ pub mod fetch;
|
|||||||
pub mod io;
|
pub mod io;
|
||||||
pub mod jre;
|
pub mod jre;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
|
pub mod protocol_version;
|
||||||
pub mod server_ping;
|
pub mod server_ping;
|
||||||
|
|||||||
478
packages/app-lib/src/util/protocol_version.rs
Normal file
478
packages/app-lib/src/util/protocol_version.rs
Normal file
@ -0,0 +1,478 @@
|
|||||||
|
use phf::phf_map;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Copy, Clone)]
|
||||||
|
pub struct ProtocolVersion {
|
||||||
|
pub version: u32,
|
||||||
|
pub legacy: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolVersion {
|
||||||
|
pub const fn modern(version: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
version,
|
||||||
|
legacy: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn legacy(version: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
version,
|
||||||
|
legacy: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The map of protocol versions from before version.json was added. For newer versions, the
|
||||||
|
/// protocol version can just be read from version.json.
|
||||||
|
pub const OLD_PROTOCOL_VERSIONS: phf::Map<&str, ProtocolVersion> = phf_map! {
|
||||||
|
// April fools versions
|
||||||
|
"1.RV-Pre1" => ProtocolVersion::modern(108),
|
||||||
|
"15w14a" => ProtocolVersion::modern(48),
|
||||||
|
"2point0_purple" => ProtocolVersion::legacy(92),
|
||||||
|
"2point0_red" => ProtocolVersion::legacy(91),
|
||||||
|
"2point0_blue" => ProtocolVersion::legacy(90),
|
||||||
|
|
||||||
|
// Normal versions
|
||||||
|
"18w47a" => ProtocolVersion::modern(446),
|
||||||
|
"18w46a" => ProtocolVersion::modern(445),
|
||||||
|
"18w45a" => ProtocolVersion::modern(444),
|
||||||
|
"18w44a" => ProtocolVersion::modern(443),
|
||||||
|
"18w43c" => ProtocolVersion::modern(442),
|
||||||
|
"18w43b" => ProtocolVersion::modern(441),
|
||||||
|
"18w43a" => ProtocolVersion::modern(440),
|
||||||
|
"1.13.2" => ProtocolVersion::modern(404),
|
||||||
|
"1.13.2-pre2" => ProtocolVersion::modern(403),
|
||||||
|
"1.13.2-pre1" => ProtocolVersion::modern(402),
|
||||||
|
"1.13.1" => ProtocolVersion::modern(401),
|
||||||
|
"1.13.1-pre2" => ProtocolVersion::modern(400),
|
||||||
|
"1.13.1-pre1" => ProtocolVersion::modern(399),
|
||||||
|
"18w33a" => ProtocolVersion::modern(398),
|
||||||
|
"18w32a" => ProtocolVersion::modern(397),
|
||||||
|
"18w31a" => ProtocolVersion::modern(396),
|
||||||
|
"18w30b" => ProtocolVersion::modern(395),
|
||||||
|
"18w30a" => ProtocolVersion::modern(394),
|
||||||
|
"1.13" => ProtocolVersion::modern(393),
|
||||||
|
"1.13-pre10" => ProtocolVersion::modern(392),
|
||||||
|
"1.13-pre9" => ProtocolVersion::modern(391),
|
||||||
|
"1.13-pre8" => ProtocolVersion::modern(390),
|
||||||
|
"1.13-pre7" => ProtocolVersion::modern(389),
|
||||||
|
"1.13-pre6" => ProtocolVersion::modern(388),
|
||||||
|
"1.13-pre5" => ProtocolVersion::modern(387),
|
||||||
|
"1.13-pre4" => ProtocolVersion::modern(386),
|
||||||
|
"1.13-pre3" => ProtocolVersion::modern(385),
|
||||||
|
"1.13-pre2" => ProtocolVersion::modern(384),
|
||||||
|
"1.13-pre1" => ProtocolVersion::modern(383),
|
||||||
|
"18w22c" => ProtocolVersion::modern(382),
|
||||||
|
"18w22b" => ProtocolVersion::modern(381),
|
||||||
|
"18w22a" => ProtocolVersion::modern(380),
|
||||||
|
"18w21b" => ProtocolVersion::modern(379),
|
||||||
|
"18w21a" => ProtocolVersion::modern(378),
|
||||||
|
"18w20c" => ProtocolVersion::modern(377),
|
||||||
|
"18w20b" => ProtocolVersion::modern(376),
|
||||||
|
"18w20a" => ProtocolVersion::modern(375),
|
||||||
|
"18w19b" => ProtocolVersion::modern(374),
|
||||||
|
"18w19a" => ProtocolVersion::modern(373),
|
||||||
|
"18w16a" => ProtocolVersion::modern(372),
|
||||||
|
"18w15a" => ProtocolVersion::modern(371),
|
||||||
|
"18w14b" => ProtocolVersion::modern(370),
|
||||||
|
"18w14a" => ProtocolVersion::modern(369),
|
||||||
|
"18w11a" => ProtocolVersion::modern(368),
|
||||||
|
"18w10d" => ProtocolVersion::modern(367),
|
||||||
|
"18w10c" => ProtocolVersion::modern(366),
|
||||||
|
"18w10b" => ProtocolVersion::modern(365),
|
||||||
|
"18w10a" => ProtocolVersion::modern(364),
|
||||||
|
"18w09a" => ProtocolVersion::modern(363),
|
||||||
|
"18w08b" => ProtocolVersion::modern(362),
|
||||||
|
"18w08a" => ProtocolVersion::modern(361),
|
||||||
|
"18w07c" => ProtocolVersion::modern(360),
|
||||||
|
"18w07b" => ProtocolVersion::modern(359),
|
||||||
|
"18w07a" => ProtocolVersion::modern(358),
|
||||||
|
"18w06a" => ProtocolVersion::modern(357),
|
||||||
|
"18w05a" => ProtocolVersion::modern(356),
|
||||||
|
"18w03b" => ProtocolVersion::modern(355),
|
||||||
|
"18w03a" => ProtocolVersion::modern(354),
|
||||||
|
"18w02a" => ProtocolVersion::modern(353),
|
||||||
|
"18w01a" => ProtocolVersion::modern(352),
|
||||||
|
"17w50a" => ProtocolVersion::modern(351),
|
||||||
|
"17w49b" => ProtocolVersion::modern(350),
|
||||||
|
"17w49a" => ProtocolVersion::modern(349),
|
||||||
|
"17w48a" => ProtocolVersion::modern(348),
|
||||||
|
"17w47b" => ProtocolVersion::modern(347),
|
||||||
|
"17w47a" => ProtocolVersion::modern(346),
|
||||||
|
"17w46a" => ProtocolVersion::modern(345),
|
||||||
|
"17w45b" => ProtocolVersion::modern(344),
|
||||||
|
"17w45a" => ProtocolVersion::modern(343),
|
||||||
|
"17w43b" => ProtocolVersion::modern(342),
|
||||||
|
"17w43a" => ProtocolVersion::modern(341),
|
||||||
|
"1.12.2" => ProtocolVersion::modern(340),
|
||||||
|
"1.21.2-pre2" => ProtocolVersion::modern(339),
|
||||||
|
"1.21.2-pre1" => ProtocolVersion::modern(339),
|
||||||
|
"1.12.1" => ProtocolVersion::modern(338),
|
||||||
|
"1.12.1-pre1" => ProtocolVersion::modern(337),
|
||||||
|
"17w31a" => ProtocolVersion::modern(336),
|
||||||
|
"1.12" => ProtocolVersion::modern(335),
|
||||||
|
"1.12-pre7" => ProtocolVersion::modern(334),
|
||||||
|
"1.12-pre6" => ProtocolVersion::modern(333),
|
||||||
|
"1.12-pre5" => ProtocolVersion::modern(332),
|
||||||
|
"1.12-pre4" => ProtocolVersion::modern(331),
|
||||||
|
"1.12-pre3" => ProtocolVersion::modern(330),
|
||||||
|
"1.12-pre2" => ProtocolVersion::modern(329),
|
||||||
|
"1.12-pre1" => ProtocolVersion::modern(328),
|
||||||
|
"17w18b" => ProtocolVersion::modern(327),
|
||||||
|
"17w18a" => ProtocolVersion::modern(326),
|
||||||
|
"17w17b" => ProtocolVersion::modern(325),
|
||||||
|
"17w17a" => ProtocolVersion::modern(324),
|
||||||
|
"17w16b" => ProtocolVersion::modern(323),
|
||||||
|
"17w16a" => ProtocolVersion::modern(322),
|
||||||
|
"17w15a" => ProtocolVersion::modern(321),
|
||||||
|
"17w14a" => ProtocolVersion::modern(320),
|
||||||
|
"17w13b" => ProtocolVersion::modern(319),
|
||||||
|
"17w13a" => ProtocolVersion::modern(318),
|
||||||
|
"17w06a" => ProtocolVersion::modern(317),
|
||||||
|
"1.11.2" => ProtocolVersion::modern(316),
|
||||||
|
"1.11.1" => ProtocolVersion::modern(316),
|
||||||
|
"16w50a" => ProtocolVersion::modern(316),
|
||||||
|
"1.11" => ProtocolVersion::modern(315),
|
||||||
|
"1.11-pre1" => ProtocolVersion::modern(314),
|
||||||
|
"16w44a" => ProtocolVersion::modern(313),
|
||||||
|
"16w43a" => ProtocolVersion::modern(313),
|
||||||
|
"16w42a" => ProtocolVersion::modern(312),
|
||||||
|
"16w41a" => ProtocolVersion::modern(311),
|
||||||
|
"16w40a" => ProtocolVersion::modern(310),
|
||||||
|
"16w39c" => ProtocolVersion::modern(309),
|
||||||
|
"16w39b" => ProtocolVersion::modern(308),
|
||||||
|
"16w39a" => ProtocolVersion::modern(307),
|
||||||
|
"16w38a" => ProtocolVersion::modern(306),
|
||||||
|
"16w36a" => ProtocolVersion::modern(305),
|
||||||
|
"16w35a" => ProtocolVersion::modern(304),
|
||||||
|
"16w33a" => ProtocolVersion::modern(303),
|
||||||
|
"16w32b" => ProtocolVersion::modern(302),
|
||||||
|
"16w32a" => ProtocolVersion::modern(301),
|
||||||
|
"1.10.2" => ProtocolVersion::modern(210),
|
||||||
|
"1.10.1" => ProtocolVersion::modern(210),
|
||||||
|
"1.10" => ProtocolVersion::modern(210),
|
||||||
|
"1.10-pre2" => ProtocolVersion::modern(205),
|
||||||
|
"1.10-pre1" => ProtocolVersion::modern(204),
|
||||||
|
"16w21b" => ProtocolVersion::modern(203),
|
||||||
|
"16w21a" => ProtocolVersion::modern(202),
|
||||||
|
"16w20a" => ProtocolVersion::modern(201),
|
||||||
|
"1.9.4" => ProtocolVersion::modern(110),
|
||||||
|
"1.9.3" => ProtocolVersion::modern(110),
|
||||||
|
"1.9.3-pre3" => ProtocolVersion::modern(110),
|
||||||
|
"1.9.3-pre2" => ProtocolVersion::modern(110),
|
||||||
|
"1.9.3-pre1" => ProtocolVersion::modern(109),
|
||||||
|
"16w15b" => ProtocolVersion::modern(109),
|
||||||
|
"16w15a" => ProtocolVersion::modern(109),
|
||||||
|
"16w14a" => ProtocolVersion::modern(109),
|
||||||
|
"1.9.2" => ProtocolVersion::modern(109),
|
||||||
|
"1.9.1" => ProtocolVersion::modern(108),
|
||||||
|
"1.9.1-pre3" => ProtocolVersion::modern(108),
|
||||||
|
"1.9.1-pre2" => ProtocolVersion::modern(108),
|
||||||
|
"1.9.1-pre1" => ProtocolVersion::modern(107),
|
||||||
|
"1.9" => ProtocolVersion::modern(107),
|
||||||
|
"1.9-pre4" => ProtocolVersion::modern(106),
|
||||||
|
"1.9-pre3" => ProtocolVersion::modern(105),
|
||||||
|
"1.9-pre2" => ProtocolVersion::modern(104),
|
||||||
|
"1.9-pre1" => ProtocolVersion::modern(103),
|
||||||
|
"16w07b" => ProtocolVersion::modern(102),
|
||||||
|
"16w07a" => ProtocolVersion::modern(101),
|
||||||
|
"16w06a" => ProtocolVersion::modern(100),
|
||||||
|
"16w05b" => ProtocolVersion::modern(99),
|
||||||
|
"16w05a" => ProtocolVersion::modern(98),
|
||||||
|
"16w04a" => ProtocolVersion::modern(97),
|
||||||
|
"16w03a" => ProtocolVersion::modern(96),
|
||||||
|
"16w02a" => ProtocolVersion::modern(95),
|
||||||
|
"15w51b" => ProtocolVersion::modern(94),
|
||||||
|
"15w51a" => ProtocolVersion::modern(93),
|
||||||
|
"15w50a" => ProtocolVersion::modern(92),
|
||||||
|
"15w49b" => ProtocolVersion::modern(91),
|
||||||
|
"15w49a" => ProtocolVersion::modern(90),
|
||||||
|
"15w47c" => ProtocolVersion::modern(89),
|
||||||
|
"15w47b" => ProtocolVersion::modern(88),
|
||||||
|
"15w47a" => ProtocolVersion::modern(87),
|
||||||
|
"15w46a" => ProtocolVersion::modern(86),
|
||||||
|
"15w45a" => ProtocolVersion::modern(85),
|
||||||
|
"15w44b" => ProtocolVersion::modern(84),
|
||||||
|
"15w44a" => ProtocolVersion::modern(83),
|
||||||
|
"15w43c" => ProtocolVersion::modern(82),
|
||||||
|
"15w43b" => ProtocolVersion::modern(81),
|
||||||
|
"15w43a" => ProtocolVersion::modern(80),
|
||||||
|
"15w42a" => ProtocolVersion::modern(79),
|
||||||
|
"15w41b" => ProtocolVersion::modern(78),
|
||||||
|
"15w41a" => ProtocolVersion::modern(77),
|
||||||
|
"15w40b" => ProtocolVersion::modern(76),
|
||||||
|
"15w40a" => ProtocolVersion::modern(75),
|
||||||
|
"15w39c" => ProtocolVersion::modern(74),
|
||||||
|
"15w39b" => ProtocolVersion::modern(74),
|
||||||
|
"15w39a" => ProtocolVersion::modern(74),
|
||||||
|
"15w38b" => ProtocolVersion::modern(73),
|
||||||
|
"15w38a" => ProtocolVersion::modern(72),
|
||||||
|
"15w37a" => ProtocolVersion::modern(71),
|
||||||
|
"15w36d" => ProtocolVersion::modern(70),
|
||||||
|
"15w36c" => ProtocolVersion::modern(69),
|
||||||
|
"15w36b" => ProtocolVersion::modern(68),
|
||||||
|
"15w36a" => ProtocolVersion::modern(67),
|
||||||
|
"15w35e" => ProtocolVersion::modern(66),
|
||||||
|
"15w35d" => ProtocolVersion::modern(65),
|
||||||
|
"15w35c" => ProtocolVersion::modern(64),
|
||||||
|
"15w35b" => ProtocolVersion::modern(63),
|
||||||
|
"15w35a" => ProtocolVersion::modern(62),
|
||||||
|
"15w34d" => ProtocolVersion::modern(61),
|
||||||
|
"15w34c" => ProtocolVersion::modern(60),
|
||||||
|
"15w34b" => ProtocolVersion::modern(59),
|
||||||
|
"15w34a" => ProtocolVersion::modern(58),
|
||||||
|
"15w33c" => ProtocolVersion::modern(57),
|
||||||
|
"15w33b" => ProtocolVersion::modern(56),
|
||||||
|
"15w33a" => ProtocolVersion::modern(55),
|
||||||
|
"15w32c" => ProtocolVersion::modern(54),
|
||||||
|
"15w32b" => ProtocolVersion::modern(53),
|
||||||
|
"15w32a" => ProtocolVersion::modern(52),
|
||||||
|
"15w31c" => ProtocolVersion::modern(51),
|
||||||
|
"15w31b" => ProtocolVersion::modern(50),
|
||||||
|
"15w31a" => ProtocolVersion::modern(49),
|
||||||
|
"1.8.9" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.8" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.7" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.6" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.5" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.4" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.3" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.2" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.2-pre7" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.2-pre6" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.2-pre5" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.2-pre4" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.2-pre3" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.2-pre2" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.2-pre1" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.1" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.1-pre5" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.1-pre4" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.1-pre3" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.1-pre2" => ProtocolVersion::modern(47),
|
||||||
|
"1.8.1-pre1" => ProtocolVersion::modern(47),
|
||||||
|
"1.8" => ProtocolVersion::modern(47),
|
||||||
|
"1.8-pre3" => ProtocolVersion::modern(46),
|
||||||
|
"1.8-pre2" => ProtocolVersion::modern(45),
|
||||||
|
"1.8-pre1" => ProtocolVersion::modern(44),
|
||||||
|
"14w34d" => ProtocolVersion::modern(43),
|
||||||
|
"14w34c" => ProtocolVersion::modern(42),
|
||||||
|
"14w34b" => ProtocolVersion::modern(41),
|
||||||
|
"14w34a" => ProtocolVersion::modern(40),
|
||||||
|
"14w33c" => ProtocolVersion::modern(39),
|
||||||
|
"14w33b" => ProtocolVersion::modern(38),
|
||||||
|
"14w33a" => ProtocolVersion::modern(37),
|
||||||
|
"14w32d" => ProtocolVersion::modern(36),
|
||||||
|
"14w32c" => ProtocolVersion::modern(35),
|
||||||
|
"14w32b" => ProtocolVersion::modern(34),
|
||||||
|
"14w32a" => ProtocolVersion::modern(33),
|
||||||
|
"14w31a" => ProtocolVersion::modern(32),
|
||||||
|
"14w30c" => ProtocolVersion::modern(31),
|
||||||
|
"14w30b" => ProtocolVersion::modern(30),
|
||||||
|
"14w30a" => ProtocolVersion::modern(30),
|
||||||
|
"14w29b" => ProtocolVersion::modern(29),
|
||||||
|
"14w29a" => ProtocolVersion::modern(29),
|
||||||
|
"14w28b" => ProtocolVersion::modern(28),
|
||||||
|
"14w28a" => ProtocolVersion::modern(27),
|
||||||
|
"14w27b" => ProtocolVersion::modern(26),
|
||||||
|
"14w27a" => ProtocolVersion::modern(26),
|
||||||
|
"14w26c" => ProtocolVersion::modern(25),
|
||||||
|
"14w26b" => ProtocolVersion::modern(24),
|
||||||
|
"14w26a" => ProtocolVersion::modern(23),
|
||||||
|
"14w25b" => ProtocolVersion::modern(22),
|
||||||
|
"14w25a" => ProtocolVersion::modern(21),
|
||||||
|
"14w21b" => ProtocolVersion::modern(20),
|
||||||
|
"14w21a" => ProtocolVersion::modern(19),
|
||||||
|
"14w20b" => ProtocolVersion::modern(18),
|
||||||
|
"14w20a" => ProtocolVersion::modern(18),
|
||||||
|
"14w19a" => ProtocolVersion::modern(17),
|
||||||
|
"14w18b" => ProtocolVersion::modern(16),
|
||||||
|
"14w18a" => ProtocolVersion::modern(16),
|
||||||
|
"14w17a" => ProtocolVersion::modern(15),
|
||||||
|
"14w11b" => ProtocolVersion::modern(14),
|
||||||
|
"14w11a" => ProtocolVersion::modern(14),
|
||||||
|
"14w10c" => ProtocolVersion::modern(13),
|
||||||
|
"14w10b" => ProtocolVersion::modern(13),
|
||||||
|
"14w10a" => ProtocolVersion::modern(13),
|
||||||
|
"14w08a" => ProtocolVersion::modern(12),
|
||||||
|
"14w07a" => ProtocolVersion::modern(11),
|
||||||
|
"14w06b" => ProtocolVersion::modern(10),
|
||||||
|
"14w06a" => ProtocolVersion::modern(10),
|
||||||
|
"14w05b" => ProtocolVersion::modern(9),
|
||||||
|
"14w05a" => ProtocolVersion::modern(9),
|
||||||
|
"14w04b" => ProtocolVersion::modern(8),
|
||||||
|
"14w04a" => ProtocolVersion::modern(7),
|
||||||
|
"14w03b" => ProtocolVersion::modern(6),
|
||||||
|
"14w03a" => ProtocolVersion::modern(6),
|
||||||
|
"14w02c" => ProtocolVersion::modern(5),
|
||||||
|
"14w02b" => ProtocolVersion::modern(5),
|
||||||
|
"14w02a" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.10" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.10-pre4" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.10-pre3" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.10-pre2" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.10-pre1" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.9" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.8" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.7" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.6" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.6-pre2" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.6-pre1" => ProtocolVersion::modern(5),
|
||||||
|
"1.7.5" => ProtocolVersion::modern(4),
|
||||||
|
"1.7.4" => ProtocolVersion::modern(4),
|
||||||
|
"1.7.3" => ProtocolVersion::modern(4),
|
||||||
|
"13w49a" => ProtocolVersion::modern(4),
|
||||||
|
"13w48b" => ProtocolVersion::modern(4),
|
||||||
|
"13w48a" => ProtocolVersion::modern(4),
|
||||||
|
"13w47e" => ProtocolVersion::modern(4),
|
||||||
|
"13w47d" => ProtocolVersion::modern(4),
|
||||||
|
"13w47c" => ProtocolVersion::modern(4),
|
||||||
|
"13w47b" => ProtocolVersion::modern(4),
|
||||||
|
"13w47a" => ProtocolVersion::modern(4),
|
||||||
|
"1.7.2" => ProtocolVersion::modern(4),
|
||||||
|
"1.7.1" => ProtocolVersion::modern(3),
|
||||||
|
"1.7" => ProtocolVersion::modern(3),
|
||||||
|
"13w43a" => ProtocolVersion::modern(2),
|
||||||
|
"13w42b" => ProtocolVersion::modern(1),
|
||||||
|
"13w42a" => ProtocolVersion::modern(1),
|
||||||
|
"13w41b" => ProtocolVersion::modern(0),
|
||||||
|
"13w41a" => ProtocolVersion::modern(0),
|
||||||
|
"13w39b" => ProtocolVersion::legacy(80),
|
||||||
|
"13w39a" => ProtocolVersion::legacy(80),
|
||||||
|
"13w38c" => ProtocolVersion::legacy(79),
|
||||||
|
"13w38b" => ProtocolVersion::legacy(79),
|
||||||
|
"13w38a" => ProtocolVersion::legacy(79),
|
||||||
|
"1.6.4" => ProtocolVersion::legacy(78),
|
||||||
|
"1.6.3" => ProtocolVersion::legacy(77),
|
||||||
|
"13w37b" => ProtocolVersion::legacy(76),
|
||||||
|
"13w37a" => ProtocolVersion::legacy(76),
|
||||||
|
"13w36b" => ProtocolVersion::legacy(75),
|
||||||
|
"13w36a" => ProtocolVersion::legacy(75),
|
||||||
|
"1.6.2" => ProtocolVersion::legacy(74),
|
||||||
|
"1.6.1" => ProtocolVersion::legacy(73),
|
||||||
|
"1.6" => ProtocolVersion::legacy(72),
|
||||||
|
"13w26a" => ProtocolVersion::legacy(72),
|
||||||
|
"13w25c" => ProtocolVersion::legacy(71),
|
||||||
|
"13w25b" => ProtocolVersion::legacy(71),
|
||||||
|
"13w25a" => ProtocolVersion::legacy(71),
|
||||||
|
"13w24b" => ProtocolVersion::legacy(70),
|
||||||
|
"13w24a" => ProtocolVersion::legacy(69),
|
||||||
|
"13w23b" => ProtocolVersion::legacy(68),
|
||||||
|
"13w23a" => ProtocolVersion::legacy(67),
|
||||||
|
"13w22a" => ProtocolVersion::legacy(67),
|
||||||
|
"13w21b" => ProtocolVersion::legacy(67),
|
||||||
|
"13w21a" => ProtocolVersion::legacy(67),
|
||||||
|
"13w19a" => ProtocolVersion::legacy(66),
|
||||||
|
"13w18c" => ProtocolVersion::legacy(65),
|
||||||
|
"13w18b" => ProtocolVersion::legacy(65),
|
||||||
|
"13w18a" => ProtocolVersion::legacy(65),
|
||||||
|
"13w17a" => ProtocolVersion::legacy(64),
|
||||||
|
"13w16b" => ProtocolVersion::legacy(63),
|
||||||
|
"13w16a" => ProtocolVersion::legacy(62),
|
||||||
|
"1.5.2" => ProtocolVersion::legacy(61),
|
||||||
|
"1.5.1" => ProtocolVersion::legacy(60),
|
||||||
|
"13w12~" => ProtocolVersion::legacy(60),
|
||||||
|
"13w11a" => ProtocolVersion::legacy(60),
|
||||||
|
"1.5" => ProtocolVersion::legacy(60),
|
||||||
|
"13w10b" => ProtocolVersion::legacy(60),
|
||||||
|
"13w10a" => ProtocolVersion::legacy(60),
|
||||||
|
"13w09c" => ProtocolVersion::legacy(60),
|
||||||
|
"13w09b" => ProtocolVersion::legacy(59),
|
||||||
|
"13w09a" => ProtocolVersion::legacy(59),
|
||||||
|
"13w07a" => ProtocolVersion::legacy(58),
|
||||||
|
"13w06a" => ProtocolVersion::legacy(58),
|
||||||
|
"13w05b" => ProtocolVersion::legacy(56),
|
||||||
|
"13w05a" => ProtocolVersion::legacy(56),
|
||||||
|
"13w04a" => ProtocolVersion::legacy(55),
|
||||||
|
"13w03a" => ProtocolVersion::legacy(54),
|
||||||
|
"13w02b" => ProtocolVersion::legacy(53),
|
||||||
|
"13w02a" => ProtocolVersion::legacy(53),
|
||||||
|
"13w01b" => ProtocolVersion::legacy(52),
|
||||||
|
"13w01a" => ProtocolVersion::legacy(52),
|
||||||
|
"1.4.7" => ProtocolVersion::legacy(51),
|
||||||
|
"1.4.6" => ProtocolVersion::legacy(51),
|
||||||
|
"12w50b" => ProtocolVersion::legacy(51),
|
||||||
|
"12w50a" => ProtocolVersion::legacy(51),
|
||||||
|
"12w49a" => ProtocolVersion::legacy(50),
|
||||||
|
"1.4.5" => ProtocolVersion::legacy(49),
|
||||||
|
"1.4.4" => ProtocolVersion::legacy(49),
|
||||||
|
"1.4.3" => ProtocolVersion::legacy(48),
|
||||||
|
"1.4.2" => ProtocolVersion::legacy(47),
|
||||||
|
"1.4.1" => ProtocolVersion::legacy(47),
|
||||||
|
"1.4" => ProtocolVersion::legacy(47),
|
||||||
|
"12w42b" => ProtocolVersion::legacy(47),
|
||||||
|
"12w42a" => ProtocolVersion::legacy(46),
|
||||||
|
"12w41b" => ProtocolVersion::legacy(46),
|
||||||
|
"12w41a" => ProtocolVersion::legacy(46),
|
||||||
|
"12w40b" => ProtocolVersion::legacy(45),
|
||||||
|
"12w40a" => ProtocolVersion::legacy(44),
|
||||||
|
"12w39b" => ProtocolVersion::legacy(43),
|
||||||
|
"12w39a" => ProtocolVersion::legacy(43),
|
||||||
|
"12w38b" => ProtocolVersion::legacy(43),
|
||||||
|
"12w38a" => ProtocolVersion::legacy(43),
|
||||||
|
"12w37a" => ProtocolVersion::legacy(42),
|
||||||
|
"12w36a" => ProtocolVersion::legacy(42),
|
||||||
|
"12w34b" => ProtocolVersion::legacy(42),
|
||||||
|
"12w34a" => ProtocolVersion::legacy(41),
|
||||||
|
"12w32a" => ProtocolVersion::legacy(40),
|
||||||
|
"1.3.2" => ProtocolVersion::legacy(39),
|
||||||
|
"1.3.1" => ProtocolVersion::legacy(39),
|
||||||
|
"1.3" => ProtocolVersion::legacy(39),
|
||||||
|
"12w30e" => ProtocolVersion::legacy(39),
|
||||||
|
"12w30d" => ProtocolVersion::legacy(39),
|
||||||
|
"12w30c" => ProtocolVersion::legacy(39),
|
||||||
|
"12w30b" => ProtocolVersion::legacy(38),
|
||||||
|
"12w30a" => ProtocolVersion::legacy(38),
|
||||||
|
"12w27a" => ProtocolVersion::legacy(38),
|
||||||
|
"12w26a" => ProtocolVersion::legacy(37),
|
||||||
|
"12w25a" => ProtocolVersion::legacy(37),
|
||||||
|
"12w24a" => ProtocolVersion::legacy(36),
|
||||||
|
"12w23b" => ProtocolVersion::legacy(35),
|
||||||
|
"12w23a" => ProtocolVersion::legacy(35),
|
||||||
|
"12w22a" => ProtocolVersion::legacy(34),
|
||||||
|
"12w21b" => ProtocolVersion::legacy(33),
|
||||||
|
"12w21a" => ProtocolVersion::legacy(33),
|
||||||
|
"12w19a" => ProtocolVersion::legacy(32),
|
||||||
|
"12w18a" => ProtocolVersion::legacy(32),
|
||||||
|
"12w17a" => ProtocolVersion::legacy(31),
|
||||||
|
"12w16a" => ProtocolVersion::legacy(30),
|
||||||
|
"12w15a" => ProtocolVersion::legacy(29),
|
||||||
|
"1.2.5" => ProtocolVersion::legacy(29),
|
||||||
|
"1.2.4" => ProtocolVersion::legacy(29),
|
||||||
|
"1.2.3" => ProtocolVersion::legacy(28),
|
||||||
|
"1.2.2" => ProtocolVersion::legacy(28),
|
||||||
|
"1.2.1" => ProtocolVersion::legacy(28),
|
||||||
|
"1.2" => ProtocolVersion::legacy(28),
|
||||||
|
"12w08a" => ProtocolVersion::legacy(28),
|
||||||
|
"12w07b" => ProtocolVersion::legacy(27),
|
||||||
|
"12w07a" => ProtocolVersion::legacy(27),
|
||||||
|
"12w06a" => ProtocolVersion::legacy(25),
|
||||||
|
"12w05b" => ProtocolVersion::legacy(24),
|
||||||
|
"12w05a" => ProtocolVersion::legacy(24),
|
||||||
|
"12w04a" => ProtocolVersion::legacy(24),
|
||||||
|
"12w03a" => ProtocolVersion::legacy(24),
|
||||||
|
"1.1" => ProtocolVersion::legacy(23),
|
||||||
|
"12w01a" => ProtocolVersion::legacy(23),
|
||||||
|
"11w50a" => ProtocolVersion::legacy(22),
|
||||||
|
"11w49a" => ProtocolVersion::legacy(22),
|
||||||
|
"11w48a" => ProtocolVersion::legacy(22),
|
||||||
|
"11w47a" => ProtocolVersion::legacy(22),
|
||||||
|
"1.0.1" => ProtocolVersion::legacy(22),
|
||||||
|
"1.0.0" => ProtocolVersion::legacy(22),
|
||||||
|
"1.0.0-rc2-1" => ProtocolVersion::legacy(22),
|
||||||
|
"1.0.0-rc2-2" => ProtocolVersion::legacy(22),
|
||||||
|
"1.0.0-rc2-3" => ProtocolVersion::legacy(22),
|
||||||
|
"1.0.0-rc1" => ProtocolVersion::legacy(22),
|
||||||
|
"b1.9-pre6" => ProtocolVersion::legacy(22),
|
||||||
|
"b1.9-pre5" => ProtocolVersion::legacy(21),
|
||||||
|
"b1.9-pre4" => ProtocolVersion::legacy(20),
|
||||||
|
"b1.9-pre3" => ProtocolVersion::legacy(19),
|
||||||
|
"b1.9-pre2" => ProtocolVersion::legacy(19),
|
||||||
|
"b1.9-pre1" => ProtocolVersion::legacy(18),
|
||||||
|
"b1.8.1" => ProtocolVersion::legacy(17),
|
||||||
|
"b1.8" => ProtocolVersion::legacy(17),
|
||||||
|
"b1.8-pre2" => ProtocolVersion::legacy(16),
|
||||||
|
"b1.8-pre1-1" => ProtocolVersion::legacy(15),
|
||||||
|
"b1.8-pre1-2" => ProtocolVersion::legacy(15),
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
use crate::ErrorKind;
|
use crate::ErrorKind;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::util::protocol_version::ProtocolVersion;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::value::RawValue;
|
use serde_json::value::RawValue;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@ -42,16 +43,23 @@ pub struct ServerGameProfile {
|
|||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
pub struct ServerVersion {
|
pub struct ServerVersion {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub protocol: i32,
|
pub protocol: u32,
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub legacy: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_server_status(
|
pub async fn get_server_status(
|
||||||
address: &impl ToSocketAddrs,
|
address: &impl ToSocketAddrs,
|
||||||
original_address: (&str, u16),
|
original_address: (&str, u16),
|
||||||
protocol_version: Option<i32>,
|
protocol_version: Option<ProtocolVersion>,
|
||||||
) -> Result<ServerStatus> {
|
) -> Result<ServerStatus> {
|
||||||
select! {
|
select! {
|
||||||
res = modern::status(address, original_address, protocol_version) => res,
|
res = async {
|
||||||
|
match protocol_version {
|
||||||
|
Some(ProtocolVersion { legacy: true, version }) => legacy::status(address, original_address, Some(version as u8)).await,
|
||||||
|
protocol => modern::status(address, original_address, protocol.map(|v| v.version)).await,
|
||||||
|
}
|
||||||
|
} => res,
|
||||||
_ = tokio::time::sleep(Duration::from_secs(30)) => Err(ErrorKind::OtherError(
|
_ = tokio::time::sleep(Duration::from_secs(30)) => Err(ErrorKind::OtherError(
|
||||||
format!("Ping of {}:{} timed out", original_address.0, original_address.1)
|
format!("Ping of {}:{} timed out", original_address.0, original_address.1)
|
||||||
).into())
|
).into())
|
||||||
@ -68,7 +76,7 @@ mod modern {
|
|||||||
pub async fn status(
|
pub async fn status(
|
||||||
address: &impl ToSocketAddrs,
|
address: &impl ToSocketAddrs,
|
||||||
original_address: (&str, u16),
|
original_address: (&str, u16),
|
||||||
protocol_version: Option<i32>,
|
protocol_version: Option<u32>,
|
||||||
) -> crate::Result<ServerStatus> {
|
) -> crate::Result<ServerStatus> {
|
||||||
let mut stream = TcpStream::connect(address).await?;
|
let mut stream = TcpStream::connect(address).await?;
|
||||||
handshake(&mut stream, original_address, protocol_version).await?;
|
handshake(&mut stream, original_address, protocol_version).await?;
|
||||||
@ -80,10 +88,10 @@ mod modern {
|
|||||||
async fn handshake(
|
async fn handshake(
|
||||||
stream: &mut TcpStream,
|
stream: &mut TcpStream,
|
||||||
original_address: (&str, u16),
|
original_address: (&str, u16),
|
||||||
protocol_version: Option<i32>,
|
protocol_version: Option<u32>,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let (host, port) = original_address;
|
let (host, port) = original_address;
|
||||||
let protocol_version = protocol_version.unwrap_or(-1);
|
let protocol_version = protocol_version.map_or(-1, |x| x as i32);
|
||||||
|
|
||||||
const PACKET_ID: i32 = 0;
|
const PACKET_ID: i32 = 0;
|
||||||
const NEXT_STATE: i32 = 1;
|
const NEXT_STATE: i32 = 1;
|
||||||
@ -221,3 +229,95 @@ mod modern {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod legacy {
|
||||||
|
use super::ServerStatus;
|
||||||
|
use crate::worlds::{ServerPlayers, ServerVersion};
|
||||||
|
use crate::{Error, ErrorKind};
|
||||||
|
use serde_json::value::to_raw_value;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||||
|
|
||||||
|
pub async fn status(
|
||||||
|
address: &impl ToSocketAddrs,
|
||||||
|
original_address: (&str, u16),
|
||||||
|
protocol_version: Option<u8>,
|
||||||
|
) -> crate::Result<ServerStatus> {
|
||||||
|
let protocol_version = protocol_version.unwrap_or(74);
|
||||||
|
|
||||||
|
let mut packet = vec![0xfe];
|
||||||
|
if protocol_version >= 47 {
|
||||||
|
packet.push(0x01);
|
||||||
|
}
|
||||||
|
if protocol_version >= 73 {
|
||||||
|
packet.push(0xfa);
|
||||||
|
write_legacy(&mut packet, "MC|PingHost");
|
||||||
|
|
||||||
|
let (host, port) = original_address;
|
||||||
|
let len_index = packet.len();
|
||||||
|
packet.push(protocol_version);
|
||||||
|
write_legacy(&mut packet, host);
|
||||||
|
packet.extend_from_slice(&(port as u32).to_be_bytes());
|
||||||
|
packet.splice(
|
||||||
|
len_index..len_index,
|
||||||
|
((packet.len() - len_index) as u16).to_be_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stream = TcpStream::connect(address).await?;
|
||||||
|
stream.write_all(&packet).await?;
|
||||||
|
stream.flush().await?;
|
||||||
|
|
||||||
|
let packet_id = stream.read_u8().await?;
|
||||||
|
if packet_id != 0xff {
|
||||||
|
return Err(Error::from(ErrorKind::InputError(
|
||||||
|
"Unexpected legacy status response".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let data_length = stream.read_u16().await?;
|
||||||
|
let mut data = vec![0u8; data_length as usize * 2];
|
||||||
|
stream.read_exact(&mut data).await?;
|
||||||
|
|
||||||
|
drop(stream);
|
||||||
|
|
||||||
|
let data = String::from_utf16_lossy(
|
||||||
|
&data
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|a| u16::from_be_bytes([a[0], a[1]]))
|
||||||
|
.collect::<Vec<u16>>(),
|
||||||
|
);
|
||||||
|
let mut ancient_server = false;
|
||||||
|
let mut parts = data.split('\0');
|
||||||
|
if parts.next() != Some("§1") {
|
||||||
|
ancient_server = true;
|
||||||
|
parts = data.split('§');
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ServerStatus {
|
||||||
|
version: (!ancient_server).then(|| ServerVersion {
|
||||||
|
protocol: parts
|
||||||
|
.next()
|
||||||
|
.and_then(|x| x.parse().ok())
|
||||||
|
.unwrap_or(0),
|
||||||
|
name: parts.next().unwrap_or("").to_owned(),
|
||||||
|
legacy: true,
|
||||||
|
}),
|
||||||
|
description: parts.next().and_then(|x| to_raw_value(x).ok()),
|
||||||
|
players: Some(ServerPlayers {
|
||||||
|
online: parts.next().and_then(|x| x.parse().ok()).unwrap_or(-1),
|
||||||
|
max: parts.next().and_then(|x| x.parse().ok()).unwrap_or(-1),
|
||||||
|
sample: vec![],
|
||||||
|
}),
|
||||||
|
favicon: None,
|
||||||
|
enforces_secure_chat: false,
|
||||||
|
ping: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_legacy(out: &mut Vec<u8>, text: &str) {
|
||||||
|
let encoded = text.encode_utf16().collect::<Vec<_>>();
|
||||||
|
out.extend_from_slice(&(encoded.len() as u16).to_be_bytes());
|
||||||
|
out.extend(encoded.into_iter().flat_map(u16::to_be_bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
**Client:** `%PROJECT_CLIENT_SIDE%` \
|
||||||
|
**Server:** `%PROJECT_SERVER_SIDE%`
|
||||||
@ -1,3 +1,5 @@
|
|||||||
## Misuse of Slug
|
## Misuse of custom URL
|
||||||
|
|
||||||
Per section 5.2 of %RULES% must accurately represent your project.
|
We ask that you ensure your project's %PROJECT_SLUG_FLINK% accurately represents your project.
|
||||||
|
Your current slug of `%PROJECT_SLUG%` may not accurately match your project's Name or contain excess information.
|
||||||
|
A mismatched URL may make it more difficult for users to find your content. Abbreviations or similar are fine to use if applicable. If your preferred URL is not available, and you cannot find a matching public project, let us know in this moderation thread when you resubmit your project, and our moderation team may be able to free it up for your project.
|
||||||
|
|||||||
@ -3,6 +3,6 @@
|
|||||||
## Private Use
|
## Private Use
|
||||||
|
|
||||||
Under normal circumstances, your project would be rejected due to the issues listed above.
|
Under normal circumstances, your project would be rejected due to the issues listed above.
|
||||||
However, since your project is not intended for for public use, these requirements will be waived and your project will be unlisted. This means it will remain accessible through a direct link without appearing in public search results, allowing you to share it privately.
|
However, since your project is not intended for public use, these requirements will be waived and your project will be unlisted. This means it will remain accessible through a direct link without appearing in public search results, allowing you to share it privately.
|
||||||
If you're okay with this, or submitted your project to be unlisted already, than no further action is necessary.
|
If you're okay with this, or submitted your project to be unlisted already, than no further action is necessary.
|
||||||
If you would like to publish your project publicly, please address all moderation concerns before resubmitting this project.
|
If you would like to publish your project publicly, please address all moderation concerns before resubmitting this project.
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
## Unsupported Project
|
||||||
|
|
||||||
|
Unfortunately, Modrinth does not currently support the upload of %INVALID_TYPE%.
|
||||||
|
|
||||||
|
If you would like to publish this project in the future and help Modrinth grow, consider creating an [issue](https://github.com/modrinth/code/issues) suggesting support for this type of content.
|
||||||
|
|
||||||
|
We appreciate your understanding and look forward to hosting your other creations.
|
||||||
@ -31,7 +31,9 @@ const categories: Stage = {
|
|||||||
weight: 701,
|
weight: 701,
|
||||||
suggestedStatus: 'flagged',
|
suggestedStatus: 'flagged',
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
shouldShow: (project) => project.categories.includes('optimization'),
|
shouldShow: (project) =>
|
||||||
|
project.categories.includes('optimization') ||
|
||||||
|
project.additional_categories.includes('optimization'),
|
||||||
message: async () =>
|
message: async () =>
|
||||||
(await import('../messages/categories/inaccurate.md?raw')).default +
|
(await import('../messages/categories/inaccurate.md?raw')).default +
|
||||||
(await import('../messages/categories/optimization_misused.md?raw')).default,
|
(await import('../messages/categories/optimization_misused.md?raw')).default,
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const gallery: Stage = {
|
|||||||
weight: 901,
|
weight: 901,
|
||||||
suggestedStatus: 'flagged',
|
suggestedStatus: 'flagged',
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
|
shouldShow: (project) => project.gallery && project.gallery.length > 0,
|
||||||
message: async () => (await import('../messages/gallery/not-relevant.md?raw')).default,
|
message: async () => (await import('../messages/gallery/not-relevant.md?raw')).default,
|
||||||
} as ButtonAction,
|
} as ButtonAction,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -7,7 +7,8 @@ const sideTypes: Stage = {
|
|||||||
id: 'environment',
|
id: 'environment',
|
||||||
icon: GlobeIcon,
|
icon: GlobeIcon,
|
||||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||||
navigate: '/settings#side-types',
|
navigate: '/settings',
|
||||||
|
text: async () => (await import('../messages/checklist-text/side_types.md?raw')).default,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
id: 'side_types_inaccurate_modpack',
|
id: 'side_types_inaccurate_modpack',
|
||||||
|
|||||||
@ -150,8 +150,25 @@ const versions: Stage = {
|
|||||||
severity: `medium`,
|
severity: `medium`,
|
||||||
weight: 1004,
|
weight: 1004,
|
||||||
message: async () => (await import('../messages/versions/broken_version.md?raw')).default,
|
message: async () => (await import('../messages/versions/broken_version.md?raw')).default,
|
||||||
|
} as ButtonAction,
|
||||||
|
{
|
||||||
|
id: 'unsupported_project_type',
|
||||||
|
type: 'button',
|
||||||
|
label: `Unsupported`,
|
||||||
|
suggestedStatus: `rejected`,
|
||||||
|
severity: `medium`,
|
||||||
|
weight: 1005,
|
||||||
|
message: async () =>
|
||||||
|
(await import('../messages/versions/unsupported_project.md?raw')).default,
|
||||||
|
relevantExtraInput: [
|
||||||
|
{
|
||||||
|
label: 'Project Type',
|
||||||
|
required: true,
|
||||||
|
variable: 'INVALID_TYPE',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
} as ButtonAction,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default versions
|
export default versions
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export * from './types/stage'
|
|||||||
export * from './types/keybinds'
|
export * from './types/keybinds'
|
||||||
export * from './types/nags'
|
export * from './types/nags'
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
|
export { finalPermissionMessages } from './data/modpack-permissions-stage'
|
||||||
|
|
||||||
export * from './data/nags/index'
|
export * from './data/nags/index'
|
||||||
export { default as checklist } from './data/checklist'
|
export { default as checklist } from './data/checklist'
|
||||||
|
|||||||
@ -317,6 +317,11 @@ export function flattenProjectVariables(project: Project): Record<string, string
|
|||||||
vars[`PROJECT_PERMANENT_LINK`] = `https://modrinth.com/project/${project.id}`
|
vars[`PROJECT_PERMANENT_LINK`] = `https://modrinth.com/project/${project.id}`
|
||||||
vars[`PROJECT_SETTINGS_LINK`] = `https://modrinth.com/project/${project.id}/settings`
|
vars[`PROJECT_SETTINGS_LINK`] = `https://modrinth.com/project/${project.id}/settings`
|
||||||
vars[`PROJECT_SETTINGS_FLINK`] = `[Settings](https://modrinth.com/project/${project.id}/settings)`
|
vars[`PROJECT_SETTINGS_FLINK`] = `[Settings](https://modrinth.com/project/${project.id}/settings)`
|
||||||
|
vars[`PROJECT_TITLE_FLINK`] = `[Name](https://modrinth.com/project/${project.id}/settings)`
|
||||||
|
vars[`PROJECT_SLUG_FLINK`] = `[URL](https://modrinth.com/project/${project.id}/settings)`
|
||||||
|
vars[`PROJECT_SUMMARY_FLINK`] = `[Summary](https://modrinth.com/project/${project.id}/settings)`
|
||||||
|
vars[`PROJECT_ENVIRONMENT_FLINK`] =
|
||||||
|
`[Environment Information](https://modrinth.com/project/${project.id}/settings)`
|
||||||
vars[`PROJECT_TAGS_LINK`] = `https://modrinth.com/project/${project.id}/settings/tags`
|
vars[`PROJECT_TAGS_LINK`] = `https://modrinth.com/project/${project.id}/settings/tags`
|
||||||
vars[`PROJECT_TAGS_FLINK`] = `[Tags](https://modrinth.com/project/${project.id}/settings/tags)`
|
vars[`PROJECT_TAGS_FLINK`] = `[Tags](https://modrinth.com/project/${project.id}/settings/tags)`
|
||||||
vars[`PROJECT_DESCRIPTION_LINK`] =
|
vars[`PROJECT_DESCRIPTION_LINK`] =
|
||||||
|
|||||||
@ -315,7 +315,7 @@ export interface ModerationPermissionType {
|
|||||||
export interface ModerationBaseModpackItem {
|
export interface ModerationBaseModpackItem {
|
||||||
sha1: string
|
sha1: string
|
||||||
file_name: string
|
file_name: string
|
||||||
type: 'unknown' | 'flame'
|
type: 'unknown' | 'flame' | 'identified'
|
||||||
status: ModerationModpackPermissionApprovalType['id'] | null
|
status: ModerationModpackPermissionApprovalType['id'] | null
|
||||||
approved: ModerationPermissionType['id'] | null
|
approved: ModerationPermissionType['id'] | null
|
||||||
}
|
}
|
||||||
@ -334,9 +334,26 @@ export interface ModerationFlameModpackItem extends ModerationBaseModpackItem {
|
|||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModerationModpackItem = ModerationUnknownModpackItem | ModerationFlameModpackItem
|
export interface ModerationIdentifiedModpackItem extends ModerationBaseModpackItem {
|
||||||
|
type: 'identified'
|
||||||
|
proof?: string
|
||||||
|
url?: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModerationModpackItem =
|
||||||
|
| ModerationUnknownModpackItem
|
||||||
|
| ModerationFlameModpackItem
|
||||||
|
| ModerationIdentifiedModpackItem
|
||||||
|
|
||||||
export interface ModerationModpackResponse {
|
export interface ModerationModpackResponse {
|
||||||
|
identified?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
file_name: string
|
||||||
|
status: ModerationModpackPermissionApprovalType['id']
|
||||||
|
}
|
||||||
|
>
|
||||||
unknown_files?: Record<string, string>
|
unknown_files?: Record<string, string>
|
||||||
flame_files?: Record<
|
flame_files?: Record<
|
||||||
string,
|
string,
|
||||||
@ -350,8 +367,8 @@ export interface ModerationModpackResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ModerationJudgement {
|
export interface ModerationJudgement {
|
||||||
type: 'flame' | 'unknown'
|
type: 'flame' | 'unknown' | 'identified'
|
||||||
status: string
|
status: string | null
|
||||||
id?: string
|
id?: string
|
||||||
link?: string
|
link?: string
|
||||||
title?: string
|
title?: string
|
||||||
|
|||||||
50
pnpm-lock.yaml
generated
50
pnpm-lock.yaml
generated
@ -5420,6 +5420,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jiti@2.5.1:
|
||||||
|
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
js-levenshtein@1.1.6:
|
js-levenshtein@1.1.6:
|
||||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -6372,6 +6376,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
picomatch@4.0.3:
|
||||||
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
pify@2.3.0:
|
pify@2.3.0:
|
||||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -7761,8 +7769,8 @@ packages:
|
|||||||
unimport@3.14.4:
|
unimport@3.14.4:
|
||||||
resolution: {integrity: sha512-90jQsiS2D0vIrWg4U58do7B5Hr4q0qt9o/rS0TrDMzrvNuAQ7XF1sQ47Pe2zjVlvFWNkoPBb/2l2GJFy5XjqDg==}
|
resolution: {integrity: sha512-90jQsiS2D0vIrWg4U58do7B5Hr4q0qt9o/rS0TrDMzrvNuAQ7XF1sQ47Pe2zjVlvFWNkoPBb/2l2GJFy5XjqDg==}
|
||||||
|
|
||||||
unimport@5.1.0:
|
unimport@5.2.0:
|
||||||
resolution: {integrity: sha512-wMmuG+wkzeHh2KCE6yiDlHmKelN8iE/maxkUYMbmrS6iV8+n6eP1TH3yKKlepuF4hrkepinEGmBXdfo9XZUvAw==}
|
resolution: {integrity: sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
|
|
||||||
unist-util-find-after@5.0.0:
|
unist-util-find-after@5.0.0:
|
||||||
@ -9366,7 +9374,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@9.4.0)
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
globals: 13.24.0
|
globals: 13.24.0
|
||||||
ignore: 5.3.1
|
ignore: 5.3.1
|
||||||
@ -9559,7 +9567,7 @@ snapshots:
|
|||||||
'@humanwhocodes/config-array@0.11.14':
|
'@humanwhocodes/config-array@0.11.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@9.4.0)
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -9957,7 +9965,7 @@ snapshots:
|
|||||||
errx: 0.1.0
|
errx: 0.1.0
|
||||||
exsolve: 1.0.7
|
exsolve: 1.0.7
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
jiti: 2.4.2
|
jiti: 2.5.1
|
||||||
klona: 2.0.6
|
klona: 2.0.6
|
||||||
knitwork: 1.2.0
|
knitwork: 1.2.0
|
||||||
mlly: 1.7.4
|
mlly: 1.7.4
|
||||||
@ -9970,7 +9978,7 @@ snapshots:
|
|||||||
tinyglobby: 0.2.14
|
tinyglobby: 0.2.14
|
||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
unctx: 2.4.1
|
unctx: 2.4.1
|
||||||
unimport: 5.1.0
|
unimport: 5.2.0
|
||||||
untyped: 2.0.0
|
untyped: 2.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
@ -12089,7 +12097,7 @@ snapshots:
|
|||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
exsolve: 1.0.7
|
exsolve: 1.0.7
|
||||||
giget: 2.0.0
|
giget: 2.0.0
|
||||||
jiti: 2.4.2
|
jiti: 2.5.1
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
perfect-debounce: 1.0.0
|
perfect-debounce: 1.0.0
|
||||||
@ -12475,10 +12483,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
debug@4.4.0:
|
|
||||||
dependencies:
|
|
||||||
ms: 2.1.3
|
|
||||||
|
|
||||||
debug@4.4.0(supports-color@9.4.0):
|
debug@4.4.0(supports-color@9.4.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@ -13241,7 +13245,7 @@ snapshots:
|
|||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.3
|
cross-spawn: 7.0.3
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@9.4.0)
|
||||||
doctrine: 3.0.0
|
doctrine: 3.0.0
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 7.2.2
|
eslint-scope: 7.2.2
|
||||||
@ -13476,9 +13480,9 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.2
|
||||||
|
|
||||||
fdir@6.4.6(picomatch@4.0.2):
|
fdir@6.4.6(picomatch@4.0.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
fflate@0.4.8: {}
|
fflate@0.4.8: {}
|
||||||
@ -14360,6 +14364,9 @@ snapshots:
|
|||||||
|
|
||||||
jiti@2.4.2: {}
|
jiti@2.4.2: {}
|
||||||
|
|
||||||
|
jiti@2.5.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
js-levenshtein@1.1.6: {}
|
js-levenshtein@1.1.6: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
@ -15778,6 +15785,9 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@4.0.2: {}
|
picomatch@4.0.2: {}
|
||||||
|
|
||||||
|
picomatch@4.0.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
pify@2.3.0: {}
|
pify@2.3.0: {}
|
||||||
|
|
||||||
pify@4.0.1: {}
|
pify@4.0.1: {}
|
||||||
@ -17147,8 +17157,8 @@ snapshots:
|
|||||||
|
|
||||||
tinyglobby@0.2.14:
|
tinyglobby@0.2.14:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.4.6(picomatch@4.0.2)
|
fdir: 6.4.6(picomatch@4.0.3)
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
@ -17355,7 +17365,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- rollup
|
- rollup
|
||||||
|
|
||||||
unimport@5.1.0:
|
unimport@5.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
escape-string-regexp: 5.0.0
|
escape-string-regexp: 5.0.0
|
||||||
@ -17424,7 +17434,7 @@ snapshots:
|
|||||||
unplugin-utils@0.2.4:
|
unplugin-utils@0.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
unplugin-vue-router@0.10.9(rollup@4.28.1)(vue-router@4.5.0(vue@3.5.13(typescript@5.5.4)))(vue@3.5.13(typescript@5.5.4)):
|
unplugin-vue-router@0.10.9(rollup@4.28.1)(vue-router@4.5.0(vue@3.5.13(typescript@5.5.4)))(vue@3.5.13(typescript@5.5.4)):
|
||||||
@ -17462,7 +17472,7 @@ snapshots:
|
|||||||
unplugin@2.3.5:
|
unplugin@2.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.3
|
||||||
webpack-virtual-modules: 0.6.2
|
webpack-virtual-modules: 0.6.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -17516,7 +17526,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
citty: 0.1.6
|
citty: 0.1.6
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
jiti: 2.4.2
|
jiti: 2.5.1
|
||||||
knitwork: 1.2.0
|
knitwork: 1.2.0
|
||||||
scule: 1.3.0
|
scule: 1.3.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user