feature-elyby-account #9

Merged
didirus merged 24 commits from feature-elyby-account into beta 2025-07-20 21:49:31 +00:00
127 changed files with 1923 additions and 624 deletions

View File

@@ -1,17 +1,9 @@
<template>
<div
v-if="mode !== 'isolated'"
ref="button"
<div v-if="mode !== 'isolated'" ref="button"
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
:class="{ expanded: mode === 'expanded' }"
@click="toggleMenu"
>
<Avatar
size="36px"
:src="
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
"
/>
:class="{ expanded: mode === 'expanded' }" @click="toggleMenu">
<Avatar size="36px" :src="selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
" />
<div class="flex flex-col w-full">
<span>
<component :is="getAccountType(selectedAccount)" v-if="selectedAccount" class="vector-icon" />
@@ -32,30 +24,23 @@
</h4>
<p>Selected</p>
</div>
<Button
v-tooltip="'Log out'"
icon-only
color="raised"
@click="logout(selectedAccount.profile.id)"
>
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.profile.id)">
<TrashIcon />
</Button>
</div>
<div v-else class="login-section account">
<h4>Not signed in</h4>
<Button
v-tooltip="'Log in'"
:disabled="loginDisabled"
icon-only
color="primary"
@click="login()"
>
<MicrosoftIcon v-if="!loginDisabled"/>
<Button v-tooltip="'Log via Microsoft'" :disabled="microsoftLoginDisabled" icon-only @click="login()">
<MicrosoftIcon v-if="!microsoftLoginDisabled" />
<SpinnerIcon v-else class="animate-spin" />
</Button>
<Button v-tooltip="'Add offline'" icon-only @click="tryOfflineLogin()">
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
<PirateIcon />
</Button>
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
<ElyByIcon v-if="!elybyLoginDisabled" />
<SpinnerIcon v-else class="animate-spin" />
</Button>
</div>
<div v-if="displayAccounts.length > 0" class="account-group">
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
@@ -72,53 +57,135 @@
</div>
</div>
<div v-if="accounts.length > 0" class="login-section account centered">
<Button v-tooltip="'Log in'" icon-only @click="login()">
<MicrosoftIcon />
<Button v-tooltip="'Log via Microsoft'" icon-only @click="login()">
<MicrosoftIcon v-if="!microsoftLoginDisabled" />
<SpinnerIcon v-else class="animate-spin" />
</Button>
<Button v-tooltip="'Add offline'" icon-only @click="tryOfflineLogin()">
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
<PirateIcon />
</Button>
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
<ElyByIcon v-if="!elybyLoginDisabled" />
<SpinnerIcon v-else class="animate-spin" />
</Button>
</div>
</Card>
</transition>
<ModalWrapper ref="loginOfflineModal" class="modal" header="Add new offline account">
<div class="modal-body">
<div class="label">Enter offline username</div>
<input type="text" v-model="playerName" placeholder="Provide offline player name" />
<Button icon-only color="secondary" @click="offlineLoginFinally()">
Continue
</Button>
<ModalWrapper ref="addElybyModal" class="modal" header="Authenticate with Ely.by">
<ModalWrapper ref="requestElybyTwoFactorCodeModal" class="modal"
header="Ely.by requested 2FA code for authentication">
<div class="flex flex-col gap-4 px-6 py-5">
<label class="label">Enter your 2FA code</label>
<input v-model="elybyTwoFactorCode" type="text" placeholder="Your 2FA code here..." class="input" />
<div class="mt-6 ml-auto">
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
Continue
</Button>
</div>
</div>
</ModalWrapper>
<div class="flex flex-col gap-4 px-6 py-5">
<label class="label">Enter your player name or email (preferred)</label>
<input v-model="elybyLogin" type="text" placeholder="Your player name or email here..." class="input" />
<label class="label">Enter your password</label>
<input v-model="elybyPassword" type="password" placeholder="Your password here..." class="input" />
<div class="mt-6 ml-auto">
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
Login
</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="loginErrorModal" class="modal" header="Error while proceed">
<div class="modal-body">
<div class="label">Error occurred while adding offline account</div>
<Button color="primary" @click="retryOfflineLogin()">
Try again
</Button>
<ModalWrapper ref="addOfflineModal" class="modal" header="Add new offline account">
<div class="flex flex-col gap-4 px-6 py-5">
<label class="label">Enter your player name</label>
<input v-model="offlinePlayerName" type="text" placeholder="Your player name here..." class="input" />
<div class="mt-6 ml-auto">
<Button icon-only color="primary" class="continue-button" @click="addOfflineProfile()">
Login
</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="unexpectedErrorModal" class="modal" header="Ошибка">
<ModalWrapper
ref="authenticationElybyErrorModal"
class="modal"
header="Error while proceeding authentication event with Ely.by">
<div class="flex flex-col gap-4 px-6 py-5">
<label class="text-base font-medium text-red-700">
An error occurred while logging in.
</label>
<div class="mt-6 ml-auto">
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
Try again
</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="inputElybyErrorModal" class="modal" header="Error while proceeding input event with Ely.by">
<div class="flex flex-col gap-4 px-6 py-5">
<label class="text-base font-medium text-red-700">
An error occurred while adding the Ely.by account. Please follow the instructions below.
</label>
<ul class="list-disc list-inside text-sm space-y-1">
<li>Check that you have entered the correct player name or email.</li>
<li>Check that you have entered the correct password.</li>
</ul>
<div class="mt-6 ml-auto">
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
Try again
</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="inputErrorModal" class="modal" header="Error while proceeding input event with offline account">
<div class="flex flex-col gap-4 px-6 py-5">
<label class="text-base font-medium text-red-700">
An error occurred while adding the offline account. Please follow the instructions below.
</label>
<ul class="list-disc list-inside text-sm space-y-1">
<li>Check that you have entered the correct player name.</li>
<li>
Player name must be at least {{ minOfflinePlayerNameLength }} characters long and no more than
{{ maxOfflinePlayerNameLength }} characters.
</li>
</ul>
<div class="mt-6 ml-auto">
<Button color="primary" class="retry-button" @click="retryAddOfflineProfile">
Try again
</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="exceptionErrorModal" class="modal" header="Unexpected error occurred">
<div class="modal-body">
<div class="label">Unexcepted error</div>
<label class="label">An unexpected error has occurred. Please try again later.</label>
</div>
</ModalWrapper>
</template>
<script setup>
import {
import {
DropdownIcon,
PlusIcon,
TrashIcon,
LogInIcon,
PirateIcon as Offline,
MicrosoftIcon as License,
ElyByIcon as Elyby,
MicrosoftIcon,
PirateIcon,
SpinnerIcon } from '@modrinth/assets'
ElyByIcon,
SpinnerIcon
} from '@modrinth/assets'
import { Avatar, Button, Card } from '@modrinth/ui'
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
import {
elyby_auth_authenticate,
elyby_login,
offline_login,
users,
remove_user,
@@ -145,48 +212,180 @@ defineProps({
const emit = defineEmits(['change'])
const accounts = ref({})
const loginDisabled = ref(false)
const microsoftLoginDisabled = ref(false)
const elybyLoginDisabled = ref(false)
const defaultUser = ref()
const loginOfflineModal = ref(null)
const loginErrorModal = ref(null)
const unexpectedErrorModal = ref(null)
const playerName = ref('')
async function tryOfflineLogin() { // [AR] Feature
loginOfflineModal.value.show()
// [AR] Feature
const clientToken = "astralrinth"
const addOfflineModal = ref(null)
const addElybyModal = ref(null)
const requestElybyTwoFactorCodeModal = ref(null)
const authenticationElybyErrorModal = ref(null)
const inputElybyErrorModal = ref(null)
const inputErrorModal = ref(null)
const exceptionErrorModal = ref(null)
const offlinePlayerName = ref('')
const elybyLogin = ref('')
const elybyPassword = ref('')
const elybyTwoFactorCode = ref('')
const minOfflinePlayerNameLength = 2
const maxOfflinePlayerNameLength = 20
// [AR] • Feature
function getAccountType(account) {
switch (account.account_type) {
case 'microsoft':
return License
case 'pirate':
return Offline
case 'elyby':
return Elyby
}
}
async function offlineLoginFinally() { // [AR] Feature
const name = playerName.value
if (name.length > 1 && name.length < 20 && name !== '') {
const loggedIn = await offline_login(name).catch(handleError)
loginOfflineModal.value.hide()
if (loggedIn) {
await setAccount(loggedIn)
// [AR] Feature
function showOfflineLoginModal() {
addOfflineModal.value?.show()
}
// [AR] • Feature
function showElybyLoginModal() {
addElybyModal.value?.show()
}
// [AR] • Feature
function retryAddOfflineProfile() {
inputErrorModal.value?.hide()
clearOfflineFields()
showOfflineLoginModal()
}
// [AR] • Feature
function retryAddElybyProfile() {
authenticationElybyErrorModal.value?.hide()
inputElybyErrorModal.value?.hide()
clearElybyFields()
showElybyLoginModal()
}
// [AR] • Feature
function clearElybyFields() {
elybyLogin.value = ''
elybyPassword.value = ''
elybyTwoFactorCode.value = ''
}
// [AR] • Feature
function clearOfflineFields() {
offlinePlayerName.value = ''
}
// [AR] • Feature
async function addOfflineProfile() {
const name = offlinePlayerName.value.trim()
const isValidName = name.length >= minOfflinePlayerNameLength && name.length <= maxOfflinePlayerNameLength
if (!isValidName) {
addOfflineModal.value?.hide()
inputErrorModal.value?.show()
clearOfflineFields()
return
}
try {
const result = await offline_login(name)
addOfflineModal.value?.hide()
if (result) {
await setAccount(result)
await refreshValues()
} else {
unexpectedErrorModal.value.show()
exceptionErrorModal.value?.show()
}
playerName.value = ''
} else {
playerName.value = ''
loginOfflineModal.value.hide()
loginErrorModal.value.show()
} catch (error) {
handleError(error)
exceptionErrorModal.value?.show()
} finally {
clearOfflineFields()
}
}
function retryOfflineLogin() { // [AR] Feature
loginErrorModal.value.hide()
tryOfflineLogin()
}
// [AR] Feature
async function addElybyProfile() {
if (!elybyLogin.value || !elybyPassword.value) {
addElybyModal.value?.hide()
inputElybyErrorModal.value?.show()
clearElybyFields()
return
}
elybyLoginDisabled.value = true
function getAccountType(account) { // [AR] Feature
if (account.access_token != "null" && account.access_token != null && account.access_token != "") {
return License
} else {
return Offline
const login = elybyLogin.value.trim()
let password = elybyPassword.value.trim()
const twoFactorCode = elybyTwoFactorCode.value.trim()
if (password && twoFactorCode) {
password = `${password}:${twoFactorCode}`
}
try {
const raw_result = await elyby_auth_authenticate(
login,
password,
clientToken
)
const json_data = JSON.parse(raw_result)
console.log(json_data?.error)
console.log(json_data?.errorMessage)
if (!json_data.accessToken) {
if (
json_data.error === 'ForbiddenOperationException' &&
json_data.errorMessage?.includes('two factor')
) {
requestElybyTwoFactorCodeModal.value?.show()
return
}
addElybyModal.value?.hide()
requestElybyTwoFactorCodeModal.value?.hide()
authenticationElybyErrorModal.value?.show()
return
}
const accessToken = json_data.accessToken
const selectedProfileId = convertRawStringToUUIDv4(json_data.selectedProfile.id)
const selectedProfileName = json_data.selectedProfile.name
const result = await elyby_login(selectedProfileId, selectedProfileName, accessToken)
addElybyModal.value?.hide()
requestElybyTwoFactorCodeModal.value?.hide()
clearElybyFields()
await setAccount(result)
await refreshValues()
} catch (err) {
handleError(err)
exceptionErrorModal.value?.show()
} finally {
elybyLoginDisabled.value = false
}
}
// [AR] • Feature
function convertRawStringToUUIDv4(rawId) {
if (rawId.length !== 32) {
console.warn('Invalid UUID string:', rawId)
return rawId
}
return `${rawId.slice(0, 8)}-${rawId.slice(8, 12)}-${rawId.slice(12, 16)}-${rawId.slice(16, 20)}-${rawId.slice(20)}`
}
const equippedSkin = ref(null)
const headUrlCache = ref(new Map())
@@ -212,13 +411,13 @@ async function refreshValues() {
}
function setLoginDisabled(value) {
loginDisabled.value = value
microsoftLoginDisabled.value = value
}
defineExpose({
refreshValues,
setLoginDisabled,
loginDisabled,
loginDisabled: microsoftLoginDisabled,
})
await refreshValues()
@@ -264,7 +463,7 @@ async function setAccount(account) {
}
async function login() {
loginDisabled.value = true
microsoftLoginDisabled.value = true
const loggedIn = await login_flow().catch(handleSevereError)
if (loggedIn) {
@@ -273,7 +472,7 @@ async function login() {
}
trackEvent('AccountLogIn')
loginDisabled.value = false
microsoftLoginDisabled.value = false
}
const logout = async (id) => {

View File

@@ -17,6 +17,24 @@ export async function offline_login(name) {
return await invoke('plugin:auth|offline_login', { name: name })
}
// [AR] • Feature
export async function elyby_login(uuid, login, accessToken) {
return await invoke('plugin:auth|elyby_login', {
uuid,
login,
accessToken
})
}
// [AR] • Feature
export async function elyby_auth_authenticate(login, password, clientToken) {
return await invoke('plugin:auth|elyby_auth_authenticate', {
login,
password,
clientToken,
})
}
/**
* Authenticate a user with Hydra - part 1.
* This begins the authentication flow quasi-synchronously.

View File

@@ -67,9 +67,8 @@ export async function determineModelType(texture: string): Promise<'SLIM' | 'CLA
const armWidth = 2
const armHeight = 12
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
for (let index = 1; index <= imageData.length; index++) {
//every fourth value in RGBA is the alpha channel
if (index % 4 == 0 && imageData[index - 1] !== 0) {
for (let alphaIndex = 3; alphaIndex < imageData.length; alphaIndex += 4) {
if (imageData[alphaIndex] !== 0) {
resolve('CLASSIC')
return
}

View File

@@ -10,20 +10,20 @@ export async function getOS() {
return await invoke('plugin:utils|get_os')
}
// [AR] Feature
export async function initUpdateLauncher(downloadurl, filename, ostype, autoupdatesupported) {
console.log('Downloading build', downloadurl, filename, ostype, autoupdatesupported)
return await invoke('plugin:utils|init_update_launcher', { downloadurl, filename, ostype, autoupdatesupported })
// [AR] Feature. Updater
export async function initUpdateLauncher(downloadUrl, filename, osType, autoUpdateSupported) {
console.log('Downloading build', downloadUrl, filename, osType, autoUpdateSupported)
return await invoke('plugin:utils|init_update_launcher', { downloadUrl, filename, osType, autoUpdateSupported })
}
// [AR] Patch fix
// [AR] Migration. Patch
export async function applyMigrationFix(eol) {
return await invoke('plugin:utils|apply_migration_fix', { eol })
}
// [AR] Feature
export async function initAuthlibPatching(minecraftversion, ismojang) {
return await invoke('plugin:utils|init_authlib_patching', { minecraftversion, ismojang })
// [AR] Feature. Ely.by
export async function initAuthlibPatching(minecraftVersion, isMojang) {
return await invoke('plugin:utils|init_authlib_patching', { minecraftVersion, isMojang })
}
export async function openPath(path) {

View File

@@ -220,7 +220,7 @@ async function refreshSearch() {
}
}
results.value = rawResults.result
currentPage.value = Math.max(1, Math.min(pageCount.value, currentPage.value))
currentPage.value = 1
const persistentParams: LocationQuery = {}
@@ -266,6 +266,7 @@ async function onSearchChangeToTop() {
function clearSearch() {
query.value = ''
currentPage.value = 1
}
watch(

View File

@@ -13,6 +13,8 @@ fn main() {
InlinedPlugin::new()
.commands(&[
"offline_login",
"elyby_login",
"elyby_auth_authenticate",
"login",
"remove_user",
"get_default_user",

View File

@@ -2,12 +2,15 @@ use crate::api::Result;
use chrono::{Duration, Utc};
use tauri::plugin::TauriPlugin;
use tauri::{Manager, Runtime, UserAttentionType};
use tauri_plugin_http::reqwest::Client;
use theseus::prelude::*;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::<R>::new("auth")
.invoke_handler(tauri::generate_handler![
offline_login,
elyby_login,
elyby_auth_authenticate,
login,
remove_user,
get_default_user,
@@ -17,14 +20,65 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.build()
}
/// ### AR • Feature
/// Create new offline user
/// This is custom function from Astralium Org.
#[tauri::command]
pub async fn offline_login(name: &str) -> Result<Credentials> {
let credentials = minecraft_auth::offline_auth(name).await?;
Ok(credentials)
}
/// ### AR • Feature
/// Create new Ely.by user
#[tauri::command]
pub async fn elyby_login(
uuid: uuid::Uuid,
login: &str,
access_token: &str
) -> Result<Credentials> {
let credentials = minecraft_auth::elyby_auth(uuid, login, access_token).await?;
Ok(credentials)
}
/// ### AR • Feature
/// Authenticate Ely.by user
#[tauri::command]
pub async fn elyby_auth_authenticate(
login: &str,
password: &str,
client_token: &str,
) -> Result<String> {
let client = Client::new();
let auth_body = serde_json::json!({
"username": login,
"password": password,
"clientToken": client_token,
});
let response = match client
.post("https://authserver.ely.by/auth/authenticate")
.header("Content-Type", "application/json")
.json(&auth_body)
.send()
.await
{
Ok(resp) => resp,
Err(e) => {
tracing::error!("[AR] • Failed to send request: {}", e);
return Ok("".to_string());
}
};
let text = match response.text().await {
Ok(body) => body,
Err(e) => {
tracing::error!("[AR] • Failed to read response text: {}", e);
return Ok("".to_string());
}
};
Ok(text)
}
/// Authenticate a user with Hydra - part 1
/// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at)
#[tauri::command]

View File

@@ -30,37 +30,37 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
.build()
}
/// [AR] Feature
/// [AR] Feature. Ely.by
#[tauri::command]
pub async fn init_authlib_patching(
minecraftversion: &str,
ismojang: bool,
minecraft_version: &str,
is_mojang: bool,
) -> Result<bool> {
let result =
utils::init_authlib_patching(minecraftversion, ismojang).await?;
utils::init_authlib_patching(minecraft_version, is_mojang).await?;
Ok(result)
}
/// [AR] Patch fix
/// [AR] Migration. Patch
#[tauri::command]
pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
let result = utils::apply_migration_fix(eol).await?;
Ok(result)
}
/// [AR] Feature
/// [AR] Feature. Updater
#[tauri::command]
pub async fn init_update_launcher(
downloadurl: &str,
download_url: &str,
filename: &str,
ostype: &str,
autoupdatesupported: bool,
os_type: &str,
auto_update_supported: bool,
) -> Result<()> {
let _ = utils::init_update_launcher(
downloadurl,
download_url,
filename,
ostype,
autoupdatesupported,
os_type,
auto_update_supported,
)
.await;
Ok(())

View File

@@ -41,7 +41,7 @@
]
},
"productName": "AstralRinth App",
"version": "0.10.304",
"version": "0.10.305",
"mainBinaryName": "AstralRinth App",
"identifier": "AstralRinthApp",
"plugins": {
@@ -86,7 +86,7 @@
"capabilities": ["core", "plugins"],
"csp": {
"default-src": "'self' customprotocol: asset:",
"connect-src": "ipc: https://git.astralium.su http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs 'self' data: blob:",
"connect-src": "ipc: https://git.astralium.su https://authserver.ely.by http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs 'self' data: blob:",
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
"style-src": "'unsafe-inline' 'self'",

View File

@@ -464,7 +464,7 @@ const stageTextExpanded = computedAsync(async () => {
const stage = checklist[stageIndex];
if (stage.text) {
return renderHighlightedString(
expandVariables(await stage.text(), props.project, variables.value),
expandVariables(await stage.text(props.project), props.project, variables.value),
);
}
return null;
@@ -980,6 +980,18 @@ async function processAction(
}
function shouldShowStage(stage: Stage): boolean {
let hasVisibleActions = false;
for (const a of stage.actions) {
if (shouldShowAction(a)) {
hasVisibleActions = true;
}
}
if (!hasVisibleActions) {
return false;
}
if (typeof stage.shouldShow === "function") {
return stage.shouldShow(props.project);
}

View File

@@ -172,6 +172,7 @@ const flags = useFeatureFlags();
.markdown-body {
grid-area: body;
max-width: 100%;
}
.reporter-info {

View File

@@ -194,13 +194,12 @@ export class ModrinthServer {
}
async testNodeReachability(): Promise<boolean> {
if (!this.general?.datacenter) {
console.warn("No datacenter info available for ping test");
if (!this.general?.node?.instance) {
console.warn("No node instance available for ping test");
return false;
}
const datacenter = this.general.datacenter;
const wsUrl = `wss://${datacenter}.nodes.modrinth.com/pingtest`;
const wsUrl = `wss://${this.general.node.instance}/pingtest`;
try {
return await new Promise((resolve) => {

View File

@@ -700,7 +700,6 @@ import {
PackageOpenIcon,
DiscordIcon,
BlueskyIcon,
TumblrIcon,
TwitterIcon,
MastodonIcon,
GithubIcon,
@@ -1185,13 +1184,6 @@ const socialLinks = [
icon: MastodonIcon,
rel: "me",
},
{
label: formatMessage(
defineMessage({ id: "layout.footer.social.tumblr", defaultMessage: "Tumblr" }),
),
href: "https://tumblr.com/modrinth",
icon: TumblrIcon,
},
{
label: formatMessage(defineMessage({ id: "layout.footer.social.x", defaultMessage: "X" })),
href: "https://x.com/modrinth",

View File

@@ -461,9 +461,6 @@
"layout.footer.social.mastodon": {
"message": "Mastodon"
},
"layout.footer.social.tumblr": {
"message": "Tumblr"
},
"layout.footer.social.x": {
"message": "X"
},

View File

@@ -45,8 +45,9 @@
<h2
class="relative m-0 max-w-2xl text-base font-normal leading-[155%] text-secondary md:text-[1.2rem]"
>
Modrinth Servers is the easiest way to host your own Minecraft server. Seamlessly install
and play your favorite mods and modpacks, all within the Modrinth platform.
Modrinth Servers is the easiest way to host your own Minecraft: Java Edition server.
Seamlessly install and play your favorite mods and modpacks, all within the Modrinth
platform.
</h2>
<div class="relative flex w-full flex-wrap items-center gap-8 align-middle sm:w-fit">
<div
@@ -459,7 +460,7 @@
</p>
</details>
<details pyro-hash="players" class="group" :open="$route.hash === '#players'">
<details pyro-hash="performance" class="group" :open="$route.hash === '#performance'">
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
<RightArrowIcon />
@@ -480,7 +481,7 @@
</p>
</details>
<details pyro-hash="players" class="group" :open="$route.hash === '#prices'">
<details pyro-hash="prices" class="group" :open="$route.hash === '#prices'">
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
<RightArrowIcon />
@@ -491,6 +492,24 @@
All prices are listed in United States Dollars (USD).
</p>
</details>
<details pyro-hash="versions" class="group" :open="$route.hash === '#versions'">
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
<RightArrowIcon />
</span>
What Minecraft versions and loaders can be used?
</summary>
<p class="m-0 ml-6 leading-[160%]">
Modrinth Servers can run any version of Minecraft: Java Edition going all the way
back to version 1.2.5, including snapshot versions.
</p>
<p class="m-0 ml-6 mt-3 leading-[160%]">
We also support a wide range of mod and plugin loaders, including Fabric, Quilt,
Forge, and NeoForge for mods, as well as Paper and Purpur for plugins. Availability
depends on whether the mod or plugin loader supports the selected Minecraft version.
</p>
</details>
</div>
</div>
</div>

View File

@@ -98,13 +98,6 @@
"date": "2023-02-01T20:00:00.000Z",
"link": "https://modrinth.com/news/article/accelerating-development"
},
{
"title": "Two years of Modrinth: a retrospective",
"summary": "The history of Modrinth as we know it from December 2020 to December 2022.",
"thumbnail": "https://modrinth.com/news/default.webp",
"date": "2023-01-07T00:00:00.000Z",
"link": "https://modrinth.com/news/article/two-years-of-modrinth-history"
},
{
"title": "Modrinth's Anniversary Update",
"summary": "Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.",
@@ -112,6 +105,13 @@
"date": "2023-01-07T00:00:00.000Z",
"link": "https://modrinth.com/news/article/two-years-of-modrinth"
},
{
"title": "Two years of Modrinth: a retrospective",
"summary": "The history of Modrinth as we know it from December 2020 to December 2022.",
"thumbnail": "https://modrinth.com/news/default.webp",
"date": "2023-01-07T00:00:00.000Z",
"link": "https://modrinth.com/news/article/two-years-of-modrinth-history"
},
{
"title": "Creators can now make money on Modrinth!",
"summary": "Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.",

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')",
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled' OR status = 'failed')",
"describe": {
"columns": [
{
@@ -102,5 +102,5 @@
true
]
},
"hash": "99cca53fd3f35325e2da3b671532bf98b8c7ad8e7cb9158e4eb9c5bac66d20b2"
"hash": "cf020daa52a1316e5f60d197196b880b72c0b2a576e470d9fd7182558103d055"
}

View File

@@ -43,7 +43,9 @@ pub enum AuthenticationError {
InvalidAuthMethod,
#[error("GitHub Token from incorrect Client ID")]
InvalidClientId,
#[error("User email/account is already registered on Modrinth")]
#[error(
"User email is already registered on Modrinth. Try 'Forgot password' to access your account."
)]
DuplicateUser,
#[error("Invalid state sent, you probably need to get a new websocket")]
SocketError,

View File

@@ -197,7 +197,7 @@ impl DBCharge {
) -> Result<Option<DBCharge>, DatabaseError> {
let user_subscription_id = user_subscription_id.0;
let res = select_charges_with_predicate!(
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')",
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled' OR status = 'failed')",
user_subscription_id
)
.fetch_optional(exec)

View File

@@ -223,8 +223,8 @@ impl TempUser {
stripe_customer_id: None,
totp_secret: None,
username,
email: self.email,
email_verified: true,
email: self.email.clone(),
email_verified: self.email.is_some(),
avatar_url,
raw_avatar_url,
bio: self.bio,
@@ -1419,15 +1419,15 @@ pub async fn create_account_with_password(
.hash_password(new_account.password.as_bytes(), &salt)?
.to_string();
if crate::database::models::DBUser::get_by_email(
if !crate::database::models::DBUser::get_by_case_insensitive_email(
&new_account.email,
&**pool,
)
.await?
.is_some()
.is_empty()
{
return Err(ApiError::InvalidInput(
"Email is already registered on Modrinth!".to_string(),
"Email is already registered on Modrinth! Try 'Forgot password' to access your account.".to_string(),
));
}
@@ -2220,6 +2220,18 @@ pub async fn set_email(
.await?
.1;
if !crate::database::models::DBUser::get_by_case_insensitive_email(
&email.email,
&**pool,
)
.await?
.is_empty()
{
return Err(ApiError::InvalidInput(
"Email is already registered on Modrinth! Try 'Forgot password' in incognito to access and delete your other account.".to_string(),
));
}
let mut transaction = pool.begin().await?;
sqlx::query!(

View File

@@ -13,7 +13,9 @@
"app:build": "turbo run build --filter=@modrinth/app",
"app:fix": "turbo run fix --filter=@modrinth/app",
"app:intl:extract": "pnpm run --filter=@modrinth/app-frontend intl:extract",
"blog:fix": "turbo run fix --filter=@modrinth/blog",
"pages:build": "NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
"moderation:fix": "turbo run fix --filter=@modrinth/moderation",
"build": "turbo run build --continue",
"lint": "turbo run lint --continue",
"test": "turbo run test --continue",

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires\n FROM minecraft_users\n WHERE active = TRUE\n ",
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires, account_type\n FROM minecraft_users\n WHERE active = TRUE\n ",
"describe": {
"columns": [
{
@@ -32,6 +32,11 @@
"name": "expires",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "account_type",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
@@ -43,8 +48,9 @@
false,
false,
false,
false,
false
]
},
"hash": "bf7d47350092d87c478009adaab131168e87bb37aa65c2156ad2cb6198426d8c"
"hash": "57214178fb3a0ccd8f67457e9732a706cbc4a4f5190c9320d1ad6111b9711d63"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires\n FROM minecraft_users\n ",
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires, account_type\n FROM minecraft_users\n ",
"describe": {
"columns": [
{
@@ -32,6 +32,11 @@
"name": "expires",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "account_type",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
@@ -43,8 +48,9 @@
false,
false,
false,
false,
false
]
},
"hash": "727e3e1bc8625bbcb833920059bb8cea926ac6c65d613904eff1d740df30acda"
"hash": "5c803f3d90c147210e8e7a7a6d7234d3801bc38c23e1e02fbd8fa08ae51e8f08"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires, account_type)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (uuid) DO UPDATE SET\n active = $2,\n username = $3,\n access_token = $4,\n refresh_token = $5,\n expires = $6,\n account_type = $7\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "8f7d4406ddae4a158eabb20fc6a8ffb21c4e22c7ff33459df5049ee441fa0467"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (uuid) DO UPDATE SET\n active = $2,\n username = $3,\n access_token = $4,\n refresh_token = $5,\n expires = $6\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "d719cf2f6f87c5ea7ea6ace2d6a1828ee58a724f06a91633b8a40b4e04d0b9a0"
}

View File

@@ -0,0 +1,5 @@
-- [AR] - SQL Migration
ALTER TABLE minecraft_users ADD COLUMN account_type varchar(32) NOT NULL DEFAULT 'unknown';
UPDATE minecraft_users SET account_type = 'microsoft' WHERE access_token != 'null';
UPDATE minecraft_users SET account_type = 'pirate' WHERE access_token == 'null';

View File

@@ -28,6 +28,16 @@ pub async fn offline_auth(
crate::state::offline_auth(name, &state.pool).await
}
#[tracing::instrument]
pub async fn elyby_auth(
uuid: uuid::Uuid,
login: &str,
access_token: &str
) -> crate::Result<Credentials> {
let state = State::get().await?;
crate::state::elyby_auth(uuid, login, access_token, &state.pool).await
}
#[tracing::instrument]
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
let state = State::get().await?;

View File

@@ -284,6 +284,12 @@ async fn import_mmc_unmanaged(
component.version.clone().unwrap_or_default(),
));
}
if component.uid.starts_with("net.neoforged") {
return Some((
PackDependency::NeoForge,
component.version.clone().unwrap_or_default(),
));
}
if component.uid.starts_with("org.quiltmc.quilt-loader") {
return Some((
PackDependency::QuiltLoader,

View File

@@ -6,9 +6,9 @@ use crate::launcher::download::download_log_config;
use crate::launcher::io::IOError;
use crate::profile::QuickPlayType;
use crate::state::{
Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
AccountType, Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
};
use crate::util::io;
use crate::util::{io, utils};
use crate::{State, get_resource_file, process, state as st};
use chrono::Utc;
use daedalus as d;
@@ -633,18 +633,32 @@ pub async fn launch_minecraft(
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
}
// FIXME: Fix ElyBy integration with this patch.
// [AR] Patch
if credentials.access_token == "null" && credentials.refresh_token == "null" {
if credentials.account_type == AccountType::Pirate.as_lowercase_str() {
if version_jar == "1.16.4" || version_jar == "1.16.5" {
let invalid_url = "https://invalid.invalid";
tracing::info!("✅ JVM args is patched by AstralRinth for MC {}", version_jar);
tracing::info!(
"[AR] • The launcher detected the launch of {} on the offline account. Applying offline multiplayer fixes.",
version_jar
);
command.arg("-Dminecraft.api.env=custom");
command.arg(format!("-Dminecraft.api.auth.host={}", invalid_url));
command.arg(format!("-Dminecraft.api.account.host={}", invalid_url));
command.arg(format!("-Dminecraft.api.session.host={}", invalid_url));
command.arg(format!("-Dminecraft.api.services.host={}", invalid_url));
command
.arg(format!("-Dminecraft.api.account.host={}", invalid_url));
command
.arg(format!("-Dminecraft.api.session.host={}", invalid_url));
command
.arg(format!("-Dminecraft.api.services.host={}", invalid_url));
}
} else if credentials.account_type == AccountType::ElyBy.as_lowercase_str()
{
tracing::info!(
"[AR] • The launcher detected the launch of {} on the Ely.by account. Applying Ely.by Java Injector.",
version_jar
);
let path_buf = utils::get_or_download_elyby_injector().await?;
let path = path_buf.to_str().unwrap();
command.arg(format!("-javaagent:{}=ely.by", path));
}
command
@@ -746,10 +760,10 @@ pub async fn launch_minecraft(
// [AR] Feature
let selected_phrase = ACTIVE_STATE.choose(&mut rand::thread_rng()).unwrap();
let _ = state
.discord_rpc
.set_activity(&format!("{} {}", selected_phrase, profile.name), true)
.await;
let _ = state
.discord_rpc
.set_activity(&format!("{} {}", selected_phrase, profile.name), true)
.await;
let _ = state
.friends_socket

View File

@@ -131,6 +131,7 @@ where
expires: legacy_credentials.expires,
active: minecraft_auth.default_user == Some(uuid)
|| minecraft_users_len == 1,
account_type: legacy_credentials.account_type,
}
.upsert(exec)
.await?;
@@ -518,6 +519,7 @@ struct LegacyCredentials {
pub access_token: String,
pub refresh_token: String,
pub expires: DateTime<Utc>,
pub account_type: String,
}
#[derive(Deserialize, Debug)]

View File

@@ -191,6 +191,7 @@ pub async fn login_finish(
expires: oauth_token.date
+ Duration::seconds(oauth_token.value.expires_in as i64),
active: true,
account_type: AccountType::Microsoft.as_lowercase_str(),
};
// During login, we need to fetch the online profile at least once to get the
@@ -229,6 +230,7 @@ pub async fn offline_auth(
refresh_token: refresh_token,
expires: Utc::now() + Duration::days(365 * 99),
active: true,
account_type: AccountType::Pirate.as_lowercase_str(),
};
credentials.offline_profile = MinecraftProfile {
@@ -242,6 +244,58 @@ pub async fn offline_auth(
Ok(credentials)
}
// [AR] Feature
#[tracing::instrument]
pub async fn elyby_auth(
uuid: Uuid,
username: &str,
access_token: &str,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
) -> crate::Result<Credentials> {
let mut credentials = Credentials {
offline_profile: MinecraftProfile::default(),
access_token: access_token.to_string(),
refresh_token: "null".to_string(),
expires: Utc::now() + Duration::days(365 * 99),
active: true,
account_type: AccountType::ElyBy.as_lowercase_str(),
};
credentials.offline_profile = MinecraftProfile {
id: uuid,
name: username.to_string(),
..credentials.offline_profile
};
credentials.upsert(exec).await?;
Ok(credentials)
}
/// [AR] • Feature
#[derive(Deserialize, Debug)]
pub enum AccountType {
Unknown,
Microsoft,
Pirate,
ElyBy,
}
impl AccountType {
fn as_str(&self) -> &'static str {
match self {
AccountType::Unknown => "Unknown",
AccountType::Microsoft => "Microsoft",
AccountType::Pirate => "Pirate",
AccountType::ElyBy => "ElyBy",
}
}
pub(crate) fn as_lowercase_str(&self) -> String {
self.as_str().to_lowercase()
}
}
#[derive(Deserialize, Debug)]
pub struct Credentials {
/// The offline profile of the user these credentials are for.
@@ -255,6 +309,7 @@ pub struct Credentials {
pub refresh_token: String,
pub expires: DateTime<Utc>,
pub active: bool,
pub account_type: String,
}
/// An entry in the player profile cache, keyed by player UUID.
@@ -480,7 +535,7 @@ impl Credentials {
let res = sqlx::query!(
"
SELECT
uuid, active, username, access_token, refresh_token, expires
uuid, active, username, access_token, refresh_token, expires, account_type
FROM minecraft_users
WHERE active = TRUE
"
@@ -503,6 +558,7 @@ impl Credentials {
.single()
.unwrap_or_else(Utc::now),
active: x.active == 1,
account_type: x.account_type,
};
credentials.refresh(exec).await.ok();
Some(credentials)
@@ -517,7 +573,7 @@ impl Credentials {
let res = sqlx::query!(
"
SELECT
uuid, active, username, access_token, refresh_token, expires
uuid, active, username, access_token, refresh_token, expires, account_type
FROM minecraft_users
"
)
@@ -537,6 +593,7 @@ impl Credentials {
.single()
.unwrap_or_else(Utc::now),
active: x.active == 1,
account_type: x.account_type,
};
async move {
@@ -572,14 +629,15 @@ impl Credentials {
sqlx::query!(
"
INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires)
VALUES ($1, $2, $3, $4, $5, $6)
INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires, account_type)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (uuid) DO UPDATE SET
active = $2,
username = $3,
access_token = $4,
refresh_token = $5,
expires = $6
expires = $6,
account_type = $7
",
uuid,
self.active,
@@ -587,6 +645,7 @@ impl Credentials {
self.access_token,
self.refresh_token,
expires,
self.account_type,
)
.execute(exec)
.await?;
@@ -649,6 +708,7 @@ impl Serialize for Credentials {
ser.serialize_field("refresh_token", &self.refresh_token)?;
ser.serialize_field("expires", &self.expires)?;
ser.serialize_field("active", &self.active)?;
ser.serialize_field("account_type", &self.account_type)?;
ser.end()
}
}

View File

@@ -5,7 +5,9 @@ use crate::state::db;
///
use crate::{Result, State};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::process;
use std::time::SystemTime;
use tokio::{fs, io};
const PACKAGE_JSON_CONTENT: &str =
@@ -46,6 +48,194 @@ pub fn read_package_json() -> io::Result<Launcher> {
Ok(launcher)
}
/// ### AR • Ely.by Injector
/// Returns the PathBuf to the Ely.by AuthLib Injector
/// If resource doesn't exist or outdated, it will be downloaded from Git Astralium.
pub async fn get_or_download_elyby_injector() -> Result<PathBuf> {
tracing::info!("[AR] • Attempting to get local authlib-injector file or download latest AuthLib Injector from remote repository.");
let state = State::get().await?;
let libraries_dir = state.directories.libraries_dir();
// Stores the local authlib injectors from `libraries/astralrinth/authlib_injectors/` directory.
let mut local_authlib_injectors = Vec::new();
validate_astralrinth_library_dir(&libraries_dir, "authlib_injector/").await?;
let astralrinth_dir = libraries_dir.join("astralrinth/");
let authlib_injector_dir = astralrinth_dir.join("authlib_injector/");
let mut authlib_injector_dir_data = fs::read_dir(&authlib_injector_dir).await?;
// Get all local authlib injectors
while let Some(entry) = authlib_injector_dir_data.next_entry().await? {
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
if file_name.starts_with("authlib-injector") {
let metadata = entry.metadata().await?;
let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
local_authlib_injectors.push((path.clone(), modified));
}
}
}
local_authlib_injectors.sort_by(|a, b| b.1.cmp(&a.1));
if !local_authlib_injectors.is_empty() {
tracing::info!("[AR] • Found local AuthLib Injector(s):");
for (path, time) in &local_authlib_injectors {
tracing::info!("• {:?} (modified: {:?})", path.file_name().unwrap(), time);
}
} else {
tracing::info!("[AR] • No local AuthLib Injector found.");
}
let latest_local_authlib_injector = local_authlib_injectors
.first()
.map(|(p, _)| p.clone());
let latest_local_authlib_injector_full_path_buf = authlib_injector_dir.join(latest_local_authlib_injector.unwrap());
// Get information about latest authlib injector from remote repository
// Return latest local installed authlib injector, if remote repository unreachable.
let (asset_name, download_url) = match extract_elyby_authlib_metadata("authlib-injector").await {
Ok(data) => data,
Err(_err) => {
tracing::warn!("[AR] • Failed to get latest AuthLib Injector from Git Astralium, using local version {}", latest_local_authlib_injector_full_path_buf.display());
return Ok(latest_local_authlib_injector_full_path_buf)
}
};
tracing::info!("[AR] • Asset name: {}", asset_name);
tracing::info!("[AR] • Download URL: {}", download_url);
tracing::info!("[AR] • Latest local AuthLib Injector: {}", latest_local_authlib_injector_full_path_buf.file_name().unwrap().display());
let remote_authlib_injector = authlib_injector_dir
.join(format!("{}", asset_name));
tracing::info!("[AR] • Comparing local version {} with remote version {}", latest_local_authlib_injector_full_path_buf.display(), remote_authlib_injector.display());
if remote_authlib_injector.eq(&latest_local_authlib_injector_full_path_buf) {
tracing::info!("[AR] • Remote version is the same as local version, still using local version {}", latest_local_authlib_injector_full_path_buf.display());
return Ok(latest_local_authlib_injector_full_path_buf)
} else {
tracing::info!(
"[AR] • Doesn't exist or outdated, attempting to download latest AuthLib Injector from URL: {}",
download_url
);
let bytes = fetch_bytes_from_url(download_url.as_str()).await?;
write_file_to_libraries(&remote_authlib_injector.to_str().unwrap(), &bytes).await?;
tracing::info!("[AR] • Successfully saved AuthLib Injector to {}", remote_authlib_injector.display());
}
Ok(remote_authlib_injector)
}
/// ### AR • Migration. Patch
/// Applying migration fix for SQLite database.
pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
tracing::info!("[AR] • Attempting to apply migration fix");
let patched = db::apply_migration_fix(eol).await?;
if patched {
tracing::info!("[AR] • Successfully applied migration fix");
} else {
tracing::error!("[AR] • Failed to apply migration fix");
}
Ok(patched)
}
/// ### AR • Feature. Updater
/// Initialize the update launcher.
pub async fn init_update_launcher(
download_url: &str,
local_filename: &str,
os_type: &str,
auto_update_supported: bool,
) -> Result<()> {
tracing::info!("[AR] • Initialize downloading from • {:?}", download_url);
tracing::info!("[AR] • Save local file name • {:?}", local_filename);
tracing::info!("[AR] • OS type • {}", os_type);
tracing::info!("[AR] • Auto update supported • {}", auto_update_supported);
if let Err(e) = update::get_resource(
download_url,
local_filename,
os_type,
auto_update_supported,
)
.await
{
eprintln!(
"[AR] • An error occurred! Failed to download the file: {}",
e
);
} else {
println!("[AR] • Code finishes without errors.");
process::exit(0)
}
Ok(())
}
/// ### AR • AuthLib (Ely.by)
/// Initializes the AuthLib patching process.
///
/// Returns `true` if the authlib patched successfully.
pub async fn init_authlib_patching(
minecraft_version: &str,
is_mojang: bool,
) -> Result<bool> {
let minecraft_library_metadata =
get_minecraft_library_metadata(minecraft_version).await?;
// Parses the AuthLib version from string
// Example output: "com.mojang:authlib:6.0.58" -> "6.0.58"
let authlib_version = minecraft_library_metadata
.name
.split(':')
.nth(2)
.unwrap_or("unknown");
let authlib_fullname_string = format!("authlib-{}.jar", authlib_version);
let authlib_fullname_str = authlib_fullname_string.as_str();
tracing::info!(
"[AR] • Attempting to download AuthLib -> {}.",
authlib_fullname_string
);
download_authlib(
&minecraft_library_metadata,
authlib_fullname_str,
minecraft_version,
is_mojang,
)
.await
}
/// ### AR • Universal Write (IO) Function.
/// Validating the `astralrinth/{target_directory}/` directory exists inside the libraries/astralrinth directory.
async fn validate_astralrinth_library_dir(
libraries_dir: &PathBuf,
validation_directory: &str
) -> Result<()> {
let astralrinth_path = libraries_dir.join(format!("astralrinth/{}", validation_directory));
if !astralrinth_path.exists() {
tokio::fs::create_dir_all(&astralrinth_path)
.await
.map_err(|e| {
tracing::error!(
"[AR] • Failed to create {} directory: {:?}",
astralrinth_path.display(),
e
);
crate::ErrorKind::IOErrorOccurred {
error: format!(
"Failed to create {} directory: {}",
astralrinth_path.display(),
e
),
}
.as_error()
})?;
tracing::info!(
"[AR] • Created missing {} directory",
astralrinth_path.display()
);
}
Ok(())
}
/// ### AR • Universal Write (IO) Function
/// Saves the downloaded bytes to the `libraries` directory using the given relative path.
async fn write_file_to_libraries(
@@ -64,72 +254,52 @@ async fn write_file_to_libraries(
})
}
/// ### AR • AuthLib (Ely By)
/// Initializes the AuthLib patching process.
///
/// Returns `true` if the authlib patched successfully.
pub async fn init_authlib_patching(
minecraft_version: &str,
is_mojang: bool,
) -> Result<bool> {
let minecraft_library_metadata = get_minecraft_library_metadata(minecraft_version).await?;
// Parses the AuthLib version from string
// Example output: "com.mojang:authlib:6.0.58" -> "6.0.58"
let authlib_version = minecraft_library_metadata.name.split(':').nth(2).unwrap_or("unknown");
tracing::info!(
"[AR] • Attempting to download AuthLib {}.",
authlib_version
);
download_authlib(
&minecraft_library_metadata,
authlib_version,
minecraft_version,
is_mojang,
)
.await
}
/// ### AR • AuthLib (Ely By)
/// ### AR • AuthLib (Ely.by)
/// Downloads the AuthLib file from Mojang libraries or Git Astralium services.
async fn download_authlib(
minecraft_library_metadata: &Library,
authlib_version: &str,
authlib_fullname: &str,
minecraft_version: &str,
is_mojang: bool,
) -> Result<bool> {
let state = State::get().await?;
let (url, path) = extract_download_info(minecraft_library_metadata, minecraft_version)?;
let mut download_url = url.to_string();
let (mut url, path) = extract_minecraft_local_download_info(
minecraft_library_metadata,
minecraft_version,
)?;
let full_path = state.directories.libraries_dir().join(path);
if !is_mojang {
tracing::info!(
"[AR] • Attempting to download AuthLib from Git Astralium"
);
download_url = extract_ely_authlib_url(authlib_version).await?;
(_, url) = extract_elyby_authlib_metadata(authlib_fullname).await?;
}
tracing::info!("[AR] • Downloading AuthLib from URL: {}", download_url);
let bytes = fetch_bytes_from_url(&download_url).await?;
tracing::info!("[AR] • Downloading AuthLib from URL: {}", url);
let bytes = fetch_bytes_from_url(&url).await?;
tracing::info!("[AR] • Will save to path: {}", full_path.to_str().unwrap());
write_file_to_libraries(full_path.to_str().unwrap(), &bytes).await?;
tracing::info!("[AR] • Successfully saved AuthLib to {:?}", full_path);
Ok(true)
}
/// ### AR • AuthLib (Ely By)
/// ### AR • AuthLib (Ely.by)
/// Parses the ElyIntegration release JSON and returns the download URL for the given AuthLib version.
async fn extract_ely_authlib_url(authlib_version: &str) -> Result<String> {
let url = "https://git.astralium.su/api/v1/repos/didirus/ElyIntegration/releases/latest";
async fn extract_elyby_authlib_metadata(
authlib_fullname: &str,
) -> Result<(String, String)> {
const URL: &str = "https://git.astralium.su/api/v1/repos/didirus/ElyIntegration/releases/latest";
let response = reqwest::get(url).await.map_err(|e| {
let response = reqwest::get(URL).await.map_err(|e| {
tracing::error!(
"[AR] • Failed to fetch ElyIntegration release JSON: {:?}",
e
);
crate::ErrorKind::NetworkErrorOccurred {
error: format!("Failed to fetch ElyIntegration release JSON: {}", e),
error: format!(
"Failed to fetch ElyIntegration release JSON: {}",
e
),
}
.as_error()
})?;
@@ -157,14 +327,14 @@ async fn extract_ely_authlib_url(authlib_version: &str) -> Result<String> {
.find(|a| {
a.get("name")
.and_then(|n| n.as_str())
.map(|n| n.contains(authlib_version))
.map(|n| n.contains(authlib_fullname))
.unwrap_or(false)
})
.ok_or_else(|| {
crate::ErrorKind::ParseError {
reason: format!(
"No matching asset for authlib-{}.jar",
authlib_version
"No matching asset for {} in ElyIntegration JSON response.",
authlib_fullname
),
}
.as_error()
@@ -178,20 +348,32 @@ async fn extract_ely_authlib_url(authlib_version: &str) -> Result<String> {
reason: "Missing 'browser_download_url'".into(),
}
.as_error()
})?;
})?
.to_string();
Ok(download_url.to_string())
let asset_name = asset
.get("name")
.and_then(|n| n.as_str())
.ok_or_else(|| {
crate::ErrorKind::ParseError {
reason: "Missing 'name'".into(),
}
.as_error()
})?
.to_string();
Ok((asset_name, download_url))
}
/// ### AR • AuthLib (Ely By)
/// ### AR • AuthLib (Ely.by)
/// Extracts the artifact URL and Path from the library structure.
///
/// Returns a tuple of references to the URL and path strings,
/// or an error if the required metadata is missing.
fn extract_download_info<'a>(
minecraft_library_metadata: &'a Library,
fn extract_minecraft_local_download_info(
minecraft_library_metadata: &Library,
minecraft_version: &str,
) -> Result<(&'a str, &'a str)> {
) -> Result<(String, String)> {
let artifact = minecraft_library_metadata
.downloads
.as_ref()
@@ -203,14 +385,14 @@ fn extract_download_info<'a>(
.as_error()
})?;
let url = artifact.url.as_deref().ok_or_else(|| {
let url = artifact.url.clone().ok_or_else(|| {
crate::ErrorKind::MinecraftMetadataNotFound {
minecraft_version: minecraft_version.to_string(),
}
.as_error()
})?;
let path = artifact.path.as_deref().ok_or_else(|| {
let path = artifact.path.clone().ok_or_else(|| {
crate::ErrorKind::MinecraftMetadataNotFound {
minecraft_version: minecraft_version.to_string(),
}
@@ -220,7 +402,7 @@ fn extract_download_info<'a>(
Ok((url, path))
}
/// ### AR • AuthLib (Ely By)
/// ### AR • Universal Fetch Bytes (IO)
/// Downloads bytes from the provided URL with a 15 second timeout.
async fn fetch_bytes_from_url(url: &str) -> Result<bytes::Bytes> {
// Create client instance with request timeout.
@@ -233,9 +415,15 @@ async fn fetch_bytes_from_url(url: &str) -> Result<bytes::Bytes> {
)
.await
.map_err(|_| {
tracing::error!("[AR] • Download timed out after {} seconds", TIMEOUT_SECONDS);
tracing::error!(
"[AR] • Download timed out after {} seconds",
TIMEOUT_SECONDS
);
crate::ErrorKind::NetworkErrorOccurred {
error: format!("Download timed out after {TIMEOUT_SECONDS} seconds").to_string(),
error: format!(
"Download timed out after {TIMEOUT_SECONDS} seconds"
)
.to_string(),
}
.as_error()
})?
@@ -249,9 +437,9 @@ async fn fetch_bytes_from_url(url: &str) -> Result<bytes::Bytes> {
if !response.status().is_success() {
let status = response.status().to_string();
tracing::error!("[AR] • Failed to download authlib: HTTP {}", status);
tracing::error!("[AR] • Failed to download file: HTTP {}", status);
return Err(crate::ErrorKind::NetworkErrorOccurred {
error: format!("Failed to download authlib: HTTP {status}"),
error: format!("Failed to download file: HTTP {status}"),
}
.as_error());
}
@@ -265,9 +453,11 @@ async fn fetch_bytes_from_url(url: &str) -> Result<bytes::Bytes> {
})
}
/// ### AR • AuthLib (Ely By)
/// ### AR • AuthLib (Ely.by)
/// Gets the Minecraft library metadata from the local libraries directory.
async fn get_minecraft_library_metadata(minecraft_version: &str) -> Result<Library> {
async fn get_minecraft_library_metadata(
minecraft_version: &str,
) -> Result<Library> {
let state = State::get().await?;
let path = state
@@ -319,48 +509,3 @@ async fn get_minecraft_library_metadata(minecraft_version: &str) -> Result<Libra
}
.as_error())
}
/// ### AR • Migration
/// Applying migration fix for SQLite database.
pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
tracing::info!("[AR] • Attempting to apply migration fix");
let patched = db::apply_migration_fix(eol).await?;
if patched {
tracing::info!("[AR] • Successfully applied migration fix");
} else {
tracing::error!("[AR] • Failed to apply migration fix");
}
Ok(patched)
}
/// ### AR • Updater
/// Initialize the update launcher.
pub async fn init_update_launcher(
download_url: &str,
local_filename: &str,
os_type: &str,
auto_update_supported: bool,
) -> Result<()> {
tracing::info!("[AR] • Initialize downloading from • {:?}", download_url);
tracing::info!("[AR] • Save local file name • {:?}", local_filename);
tracing::info!("[AR] • OS type • {}", os_type);
tracing::info!("[AR] • Auto update supported • {}", auto_update_supported);
if let Err(e) = update::get_resource(
download_url,
local_filename,
os_type,
auto_update_supported,
)
.await
{
eprintln!(
"[AR] • An error occurred! Failed to download the file: {}",
e
);
} else {
println!("[AR] • Code finishes without errors.");
process::exit(0)
}
Ok(())
}

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
sodipodi:docname="elyby-icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
inkscape:zoom="20.48"
inkscape:cx="13.891602"
inkscape:cy="18.09082"
inkscape:window-width="1776"
inkscape:window-height="1236"
inkscape:window-x="244"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#00c600;stroke-width:0.264999;paint-order:markers fill stroke;fill-opacity:1"
id="rect1"
width="10"
height="10"
x="0"
y="0"
rx="1"
ry="1" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:9.61119px;font-family:Arial;-inkscape-font-specification:'Arial, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke-width:0.80219;paint-order:markers fill stroke"
x="1.0449646"
y="9.1731787"
id="text1"
inkscape:label="text1"
transform="scale(1.1466469,0.87210806)"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:9.61119px;font-family:Arial;-inkscape-font-specification:'Arial, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.802193"
x="1.0449646"
y="9.1731787">E</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -89,6 +89,7 @@ import _PirateIcon from './icons/pirate.svg?component'
import _MicrosoftIcon from './icons/microsoft.svg?component'
import _PirateShipIcon from './icons/pirate-ship.svg?component'
import _AstralRinthLogo from './icons/astralrinth-logo.svg?component'
import _ElyByIcon from './icons/elyby-icon.svg?component'
// [AR] Feature. Exports
@@ -96,6 +97,7 @@ export const PirateIcon = _PirateIcon
export const MicrosoftIcon = _MicrosoftIcon
export const PirateShipIcon = _PirateShipIcon
export const AstralRinthLogo = _AstralRinthLogo
export const ElyByIcon = _ElyByIcon
// Skin Models
export { default as ClassicPlayerModel } from './models/classic-player.gltf?url'

View File

@@ -1,16 +1,16 @@
import { promises as fs } from 'fs'
import * as path from 'path'
import fastGlob from 'fast-glob'
import { repoPath, toVarName } from './utils'
import { glob } from 'glob'
import { PUBLIC_SRC, PUBLIC_LOCATIONS, ARTICLES_GLOB, COMPILED_DIR } from './blog.config'
async function checkPublicAssets() {
const srcFiles = await fastGlob(['**/*'], { cwd: PUBLIC_SRC, dot: true })
const srcFiles = await glob('**/*', { cwd: PUBLIC_SRC, dot: true })
let allOk = true
for (const target of PUBLIC_LOCATIONS) {
for (const relativeFile of srcFiles) {
const shouldExist = path.join(target, relativeFile)
const shouldExist = path.posix.join(target, relativeFile)
try {
await fs.access(shouldExist)
} catch {
@@ -26,15 +26,15 @@ async function checkPublicAssets() {
}
async function checkCompiledArticles() {
const mdFiles = await fastGlob([ARTICLES_GLOB])
const compiledFiles = await fastGlob([`${COMPILED_DIR}/*.ts`])
const mdFiles = await glob(ARTICLES_GLOB)
const compiledFiles = await glob(`${COMPILED_DIR}/*.ts`)
const compiledVarNames = compiledFiles.map((f) => path.basename(f, '.ts'))
// Check all .md have compiled .ts and .content.ts and the proper public thumbnail
for (const file of mdFiles) {
const varName = toVarName(path.basename(file, '.md'))
const compiledPath = path.join(COMPILED_DIR, varName + '.ts')
const contentPath = path.join(COMPILED_DIR, varName + '.content.ts')
const compiledPath = path.posix.join(COMPILED_DIR, varName + '.ts')
const contentPath = path.posix.join(COMPILED_DIR, varName + '.content.ts')
if (!compiledVarNames.includes(varName)) {
console.error(`⚠️ Missing compiled article for: ${file} (should be: ${compiledPath})`)
process.exit(1)
@@ -59,7 +59,7 @@ async function checkCompiledArticles() {
if (varName === 'index' || varName.endsWith('.content')) continue
const mdPathGlob = repoPath(`packages/blog/articles/**/${varName.replace(/_/g, '*')}.md`)
const found = await fastGlob([mdPathGlob])
const found = await glob(mdPathGlob)
if (!found.length) {
console.error(`❌ Compiled article ${compiled} has no matching markdown source!`)
process.exit(1)

View File

@@ -1,12 +1,12 @@
import { promises as fs } from 'fs'
import * as path from 'path'
import fg from 'fast-glob'
import matter from 'gray-matter'
import { md } from '@modrinth/utils'
import { minify } from 'html-minifier-terser'
import { copyDir, toVarName } from './utils'
import RSS from 'rss'
import { parseStringPromise } from 'xml2js'
import { glob } from 'glob'
import {
ARTICLES_GLOB,
@@ -24,7 +24,7 @@ async function ensureCompiledDir() {
}
async function hasThumbnail(slug: string): Promise<boolean> {
const thumbnailPath = path.join(PUBLIC_SRC, slug, 'thumbnail.webp')
const thumbnailPath = path.posix.join(PUBLIC_SRC, slug, 'thumbnail.webp')
try {
await fs.access(thumbnailPath)
return true
@@ -48,7 +48,7 @@ function getThumbnailUrl(slug: string, hasThumb: boolean): string {
async function compileArticles() {
await ensureCompiledDir()
const files = await fg([ARTICLES_GLOB])
const files = await glob(ARTICLES_GLOB)
console.log(`🔎 Found ${files.length} markdown articles!`)
const articleExports: string[] = []
const articlesArray: string[] = []
@@ -75,8 +75,8 @@ async function compileArticles() {
const slug = frontSlug || path.basename(file, '.md')
const varName = toVarName(slug)
const exportFile = path.join(COMPILED_DIR, `${varName}.ts`)
const contentFile = path.join(COMPILED_DIR, `${varName}.content.ts`)
const exportFile = path.posix.join(COMPILED_DIR, `${varName}.ts`)
const contentFile = path.posix.join(COMPILED_DIR, `${varName}.content.ts`)
const thumbnailPresent = await hasThumbnail(slug)
const contentTs = `
@@ -221,7 +221,7 @@ async function deleteDirContents(dir: string) {
const entries = await fs.readdir(dir, { withFileTypes: true })
await Promise.all(
entries.map(async (entry) => {
const fullPath = path.join(dir, entry.name)
const fullPath = path.posix.join(dir, entry.name)
if (entry.isDirectory()) {
await fs.rm(fullPath, { recursive: true, force: true })
} else {

View File

@@ -1,56 +1,56 @@
// AUTO-GENERATED FILE - DO NOT EDIT
import { article as a_new_chapter_for_modrinth_servers } from './a_new_chapter_for_modrinth_servers'
import { article as accelerating_development } from './accelerating_development'
import { article as becoming_sustainable } from './becoming_sustainable'
import { article as capital_return } from './capital_return'
import { article as carbon_ads } from './carbon_ads'
import { article as creator_monetization } from './creator_monetization'
import { article as creator_update } from './creator_update'
import { article as creator_updates_july_2025 } from './creator_updates_july_2025'
import { article as design_refresh } from './design_refresh'
import { article as download_adjustment } from './download_adjustment'
import { article as knossos_v2_1_0 } from './knossos_v2_1_0'
import { article as licensing_guide } from './licensing_guide'
import { article as modpack_changes } from './modpack_changes'
import { article as modpacks_alpha } from './modpacks_alpha'
import { article as modrinth_app_beta } from './modrinth_app_beta'
import { article as modrinth_beta } from './modrinth_beta'
import { article as modrinth_servers_beta } from './modrinth_servers_beta'
import { article as new_site_beta } from './new_site_beta'
import { article as plugins_resource_packs } from './plugins_resource_packs'
import { article as pride_campaign_2025 } from './pride_campaign_2025'
import { article as redesign } from './redesign'
import { article as skins_now_in_modrinth_app } from './skins_now_in_modrinth_app'
import { article as two_years_of_modrinth_history } from './two_years_of_modrinth_history'
import { article as two_years_of_modrinth } from './two_years_of_modrinth'
import { article as whats_modrinth } from './whats_modrinth'
import { article as windows_borderless_malware_disclosure } from './windows_borderless_malware_disclosure'
import { article as whats_modrinth } from './whats_modrinth'
import { article as two_years_of_modrinth } from './two_years_of_modrinth'
import { article as two_years_of_modrinth_history } from './two_years_of_modrinth_history'
import { article as skins_now_in_modrinth_app } from './skins_now_in_modrinth_app'
import { article as redesign } from './redesign'
import { article as pride_campaign_2025 } from './pride_campaign_2025'
import { article as plugins_resource_packs } from './plugins_resource_packs'
import { article as new_site_beta } from './new_site_beta'
import { article as modrinth_servers_beta } from './modrinth_servers_beta'
import { article as modrinth_beta } from './modrinth_beta'
import { article as modrinth_app_beta } from './modrinth_app_beta'
import { article as modpacks_alpha } from './modpacks_alpha'
import { article as modpack_changes } from './modpack_changes'
import { article as licensing_guide } from './licensing_guide'
import { article as knossos_v2_1_0 } from './knossos_v2_1_0'
import { article as download_adjustment } from './download_adjustment'
import { article as design_refresh } from './design_refresh'
import { article as creator_updates_july_2025 } from './creator_updates_july_2025'
import { article as creator_update } from './creator_update'
import { article as creator_monetization } from './creator_monetization'
import { article as carbon_ads } from './carbon_ads'
import { article as capital_return } from './capital_return'
import { article as becoming_sustainable } from './becoming_sustainable'
import { article as accelerating_development } from './accelerating_development'
import { article as a_new_chapter_for_modrinth_servers } from './a_new_chapter_for_modrinth_servers'
export const articles = [
a_new_chapter_for_modrinth_servers,
accelerating_development,
becoming_sustainable,
capital_return,
carbon_ads,
creator_monetization,
creator_update,
creator_updates_july_2025,
design_refresh,
download_adjustment,
knossos_v2_1_0,
licensing_guide,
modpack_changes,
modpacks_alpha,
modrinth_app_beta,
modrinth_beta,
modrinth_servers_beta,
new_site_beta,
plugins_resource_packs,
pride_campaign_2025,
redesign,
skins_now_in_modrinth_app,
two_years_of_modrinth_history,
two_years_of_modrinth,
whats_modrinth,
windows_borderless_malware_disclosure,
whats_modrinth,
two_years_of_modrinth,
two_years_of_modrinth_history,
skins_now_in_modrinth_app,
redesign,
pride_campaign_2025,
plugins_resource_packs,
new_site_beta,
modrinth_servers_beta,
modrinth_beta,
modrinth_app_beta,
modpacks_alpha,
modpack_changes,
licensing_guide,
knossos_v2_1_0,
download_adjustment,
design_refresh,
creator_updates_july_2025,
creator_update,
creator_monetization,
carbon_ads,
capital_return,
becoming_sustainable,
accelerating_development,
a_new_chapter_for_modrinth_servers,
]

View File

@@ -9,6 +9,7 @@
"fix": "jiti ./compile.ts && eslint . --fix && prettier --write ."
},
"devDependencies": {
"@types/glob": "^9.0.0",
"@types/html-minifier-terser": "^7.0.2",
"@types/rss": "^0.0.32",
"@types/xml2js": "^0.4.14",
@@ -19,7 +20,7 @@
},
"dependencies": {
"@modrinth/utils": "workspace:*",
"fast-glob": "^3.3.3",
"glob": "^10.2.7",
"gray-matter": "^4.0.3",
"html-minifier-terser": "^7.2.0",
"rss": "^1.2.2",

View File

@@ -8,7 +8,7 @@ export function getRepoRoot(): string {
}
export function repoPath(...segments: string[]): string {
return path.join(getRepoRoot(), ...segments)
return path.posix.join(getRepoRoot(), ...segments)
}
export async function copyDir(
@@ -20,8 +20,8 @@ export async function copyDir(
await fs.mkdir(dest, { recursive: true })
const entries = await fs.readdir(src, { withFileTypes: true })
for (const entry of entries) {
const srcPath = path.join(src, entry.name)
const destPath = path.join(dest, entry.name)
const srcPath = path.posix.join(src, entry.name)
const destPath = path.posix.join(dest, entry.name)
if (entry.isDirectory()) {
await copyDir(srcPath, destPath, logFn)
} else if (entry.isFile()) {

View File

@@ -1,28 +1,32 @@
import type { Stage } from '../types/stage'
import modpackPermissionsStage from './modpack-permissions-stage'
import categories from './stages/categories'
import copyright from './stages/copyright'
import reupload from './stages/reupload'
import description from './stages/description'
import gallery from './stages/gallery'
import links from './stages/links'
import ruleFollowing from './stages/rule-following'
import sideTypes from './stages/side-types'
import slug from './stages/slug'
import summary from './stages/summary'
import title from './stages/title'
import titleSlug from './stages/title-slug'
import versions from './stages/versions'
import license from './stages/license'
import undefinedProject from './stages/undefined-project'
import statusAlerts from './stages/status-alerts'
export default [
title,
slug,
titleSlug,
summary,
description,
links,
license,
categories,
sideTypes,
gallery,
versions,
copyright,
reupload,
ruleFollowing,
modpackPermissionsStage,
statusAlerts,
undefinedProject,
] as ReadonlyArray<Stage>

View File

@@ -1,3 +1,3 @@
## Misuse of Tags
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate. Including that selected tags honestly represent your project.
Per section 5.1 of %RULES%, it is important that the metadata of your projects is accurate. Including that selected tags honestly represent your project.

View File

@@ -0,0 +1 @@
It looks like the Optimization tag is not accurate for this project.

View File

@@ -0,0 +1,5 @@
Currently, your pack has multiple Resolutions selected, in most cases, this will misrepresent your project.
For a brief rundown of how this works:
Vanilla Minecraft textures like blocks and items have a width and height of 16 pixels.
This means that packs with textures the same size as the default are considered 16x.
Some packs make the resolution of textures smaller than the default, such as 8x, and some bigger, such as 32x.

View File

@@ -0,0 +1,2 @@
**Featured Tags:** %PROJECT_CATEGORIES% \
**Additional Tags:** %PROJECT_ADDITIONAL_CATEGORIES%

View File

@@ -0,0 +1,2 @@
**License id:** %PROJECT_LICENSE_ID% \
**License Link:** %PROJECT_LICENSE_URL%

View File

@@ -0,0 +1,4 @@
**Discord:** %PROJECT_DISCORD_URL% \
**Issues:** %PROJECT_ISSUES_URL% \
**Source:** %PROJECT_SOURCE_URL% \
**Wiki:** %PROJECT_WIKI_URL%

View File

@@ -0,0 +1 @@
> **{PLATFORM}:** {URL}<br />

View File

@@ -0,0 +1,2 @@
<br />
<u>**Donation Links:**</u><br />

View File

@@ -0,0 +1 @@
**Applying for:** `%PROJECT_REQUESTED_STATUS%`

View File

@@ -0,0 +1,2 @@
**Summary:**
`%PROJECT_SUMMARY%`

View File

@@ -0,0 +1,4 @@
**Title:** %PROJECT_TITLE% \
**Slug:** `%PROJECT_SLUG%`
**Title issues?**

View File

@@ -1,7 +1,6 @@
## Insufficient Description
Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
Per section 2.1 of %RULES% your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
Currently, it looks like there are some missing details.
> %EXPLAINER%
%EXPLAINER%

View File

@@ -1,5 +1,5 @@
## No English Description
Per section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#accessibility) a project's Summary and Description must be in English, unless meant exclusively for non-English use, such as translations.
Per section 2.2 of %RULES% a project's [Summary](%PROJECT_SETTINGS_LINK%) and %PROJECT_DESCRIPTION_FLINK% must be in English, unless meant exclusively for non-English use, such as translations.
You may include your non-English Description if you would like but we ask that you also add an English translation of the Description to your Description page, if you would like to use an online translator to do this, we recommend [DeepL](https://www.deepl.com/translator).
You may include your non-English Description if you would like but we ask that you also add an English translation of the Description to your project page, if you would like to use an online translator to do this, we recommend [DeepL](https://www.deepl.com/translator).

View File

@@ -1,6 +1,6 @@
## Description Accessibility
Per section 2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#clear-and-honest-function) your description must be plainly readable and accessible.
Per section 2 of %RULES% your description must be plainly readable and accessible.
Using non-standard text characters like Zalgo or "fancy text" in place of text anywhere in your project, including the Description, Summary, or Title can make your project pages inaccessible.

View File

@@ -1,7 +1,4 @@
## Unfinished Description
It looks like your project Description is still a WIP (Work In Progress).
> %REASON%
Please remember to submit only when ready, as it is important your project meets the requirements of Section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations), if you have any questions on this feel free to reach out!
It looks like your project Description is still a Work In Progress.
Please remember to submit only when ready, as it is important your project meets the requirements of Section 2.1 of %RULES%.

View File

@@ -1,6 +1,6 @@
## Insufficient Gallery Images
We ask that projects like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of its content per section 2.1 of [Modrinth's content rules](https://modrinth.com/legal/rules#general-expectations).
We ask that projects like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of its content per section 2.1 of %RULES%.
Keep in mind that you should:
- Set a featured image that best represents your project.

View File

@@ -1,3 +1,3 @@
## Unrelated Gallery Images
Per section 5.5 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) any images in your project's Gallery must be relevant to the project and also include a Title.
Per section 5.5 of %RULES% any images in your project's Gallery must be relevant to the project and also include a Title.

View File

@@ -0,0 +1 @@
If this is a project-specific License you've written yourself, you must host the full License in a way that makes it publicly available, for instance, in a public source repository on a platform like GitHub.

View File

@@ -0,0 +1,4 @@
## Invalid License Link
It's important that your project's License link is accurate and leads directly to a valid license for this content.
Your current link: `%PROJECT_LICENSE_URL%` does not appear to lead to a valid license for this project, or it is not publicly accessable.

View File

@@ -0,0 +1,5 @@
## No Source Code Provided
Your project's license of `%PROJECT_LICENSE_NAME%`, requires source disclosure.
Consider adding a Source link to your project's repository, or including a Sources file for each version as an Additional File.
Keep in mind this may be a requirement of the source work's licensing, which must be abided per section 4 of %RULES%.

View File

@@ -0,0 +1,4 @@
## No Source Code Provided
Your project's license of `%PROJECT_LICENSE_NAME%`, requires source disclosure.
Consider adding a Source link to your project's repository, or including a Sources file for each version as an Additional File. You may also want to refer to %LICENSING_GUIDE% if you wish to select a different License, remember to make sure your selected License is consistent with the license in your project's files as well.

View File

@@ -1,3 +1,4 @@
## Misuse of External Resources
## Misuse of Links
Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.
Per section 5.4 of %RULES% all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project.
Currently it looks like your %MISUSED_LINKS% link(s) are misused or incorrectly labeled.

View File

@@ -1,5 +0,0 @@
## Unreachable Links
Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.
Currently, your %LINK% link is inaccessible!

View File

@@ -1,5 +0,0 @@
## Unreachable Links
Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.
Currently, your Source link directs to a Page Not Found error, likely because your repository is private, make sure to make your repository public before resubmitting your project!

View File

@@ -0,0 +1 @@
Currently, your Discord link directs to an invalid invite, likely because your invite has expired. Make sure to set your invite link to permanent with unlimited uses before resubmitting your project.

View File

@@ -0,0 +1 @@
Currently, your Source link directs to a Page Not Found error, likely because your repository is private. Make sure to set your repository to public before resubmitting your project.

View File

@@ -0,0 +1,3 @@
## Unreachable Links
Per section 5.4 of %RULES% all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project.

View File

@@ -0,0 +1,4 @@
## Forks and Reuploads
Per section 4 of %RULES%, please provide proof that this project is both license-abiding and significantly divergent from the source work.
Alternatively, please provide proof of your explicit permission from the author of the source work to distribute this content on Modrinth.

View File

@@ -0,0 +1,6 @@
## Identity Verification
**Welcome to Modrinth!** We're happy to see you here, we just want to make sure you're you.
Since this project already exists on the internet we ask that you provide evidence you are the rightful owner of this content.
For instance, by submitting a screenshot accessing the settings of the project's existing pages such as %PLATFORM%.

View File

@@ -0,0 +1,4 @@
## Insufficient Fork
This project does not appear to significantly diverge from the source work, or does not abide by the license of the source work as required by section 4 of %RULES%.
Please provide proof of your explicit permission to distribute this project from the creator(s) of the source work.

View File

@@ -0,0 +1,5 @@
## Proof of permissions
This project appears to contain content from other creators.
Per section 4 of %RULES%, we ask that you provide proof of your permission to distribute this content, or derivatives of this content in your project on Modrinth.
Either implicit permission abiding by the terms of the content's license(s) or explicit permission from the original creator of the content.

View File

@@ -1,7 +1,5 @@
## Reuploads are forbidden
This project appears to contain content from %ORIGINAL_PROJECT% by %ORIGINAL_AUTHOR%.
Per section 4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) this is strictly forbidden.
If you believe this is an error, or you can verify you are the creator and rightful owner of this content please let us know. Otherwise, we ask that you **do not resubmit this project**.`,
This project appears to contain content from %ORIGINAL_PROJECT% by %ORIGINAL_AUTHOR%.
Per section 4 of %RULES% this is strictly forbidden.
If you believe this is an error, or you can verify you are the creator and rightful owner of this content please let us know. Otherwise, we ask that you **do not resubmit this project**.

View File

@@ -1,5 +1 @@
# Does not follow content rules
Our moderators have determined that your project does not follow Modrinth's Content Rules, and has been rejected.
%MESSAGE%

View File

@@ -1,6 +1,6 @@
## Environment Information
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
Per section 5.1 of %RULES%, it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
For a brief rundown of how this works:

View File

@@ -1,6 +1,6 @@
## Incorrect Environment Information
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
Per section 5.1 of %RULES%, it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
For a brief rundown of how this works:

View File

@@ -1,3 +1,3 @@
## Misuse of Slug
Per section 5.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your project slug (URL) must accurately represent your project.
Per section 5.2 of %RULES% must accurately represent your project.

View File

@@ -0,0 +1,6 @@
---
## Account Issues Indicated
We're sorry to hear you're having trouble accessing your accounts, unfortunately, our moderation team is unable to assist with account-related issues.
Before resubmitting your project, %SUPPORT%.

View File

@@ -0,0 +1,5 @@
---
Unfortunately, our AutoMod cannot read your project's Description or your messages to moderation.
AutoMod will warn both you and our Moderation Staff about potential issues, but if you've already followed the necessary steps these warnings can safely be ignored.
Note that if your project is being rejected by AutoMod this means your project has content that can not be included in your modpack and must be removed before resubmission, including deleting versions of your modpack that include the content.

View File

@@ -0,0 +1,5 @@
---
## Corrections Applied
I've gone ahead and corrected the issues listed above so your project can be Approved.

View File

@@ -0,0 +1,8 @@
---
## Private Use
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.
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.

View File

@@ -0,0 +1,5 @@
## Source Code Requested
To ensure the safety of all Modrinth users, we ask that you provide the source code for this project before resubmission so that it can be reviewed by our Moderation Team.
We also ask that you provide the source for any included binary files, as well as detailed build instructions allowing us to verify that the compiled code you are distributing matches the provided source.
We understand that you may not want to publish the source code for this project, so you are welcome to share it privately to the [Modrinth Content Moderation Team](https://github.com/ModrinthModeration) on GitHub.

View File

@@ -0,0 +1,4 @@
## Source Code Requested
To ensure the safety of all Modrinth users, we ask that you provide the source code for this project and the process you used to obfuscate it before resubmission so that it can be reviewed by our Moderation Team.
We understand that you may not want to publish the source code for this project, so you are welcome to share it privately to the [Modrinth Content Moderation Team](https://github.com/ModrinthModeration) on GitHub.

View File

@@ -1,6 +1,6 @@
## Insufficient Summary
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not include any extra formatting such as lists, or links.
Per section 5.3 of %RULES% your Summary can not include any extra formatting such as lists, or links.
Your project summary should provide a brief overview of your project that informs and entices users.

View File

@@ -1,5 +1,5 @@
## Insufficient Summary
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your project summary should provide a brief overview of your project that informs and entices users.
Per section 5.3 of %RULES% your project summary should provide a brief overview of your project that informs and entices users.
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.

View File

@@ -1,5 +1,5 @@
## No English Summary
Per section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#accessibility) a project's Summary and Description must be in English, unless meant exclusively for non-English use, such as translations.
Per section 2.2 of %RULES% a project's Summary and Description must be in English, unless meant exclusively for non-English use, such as translations.
You may include your non-English Summary but we ask that you also add an English translation.

View File

@@ -1,6 +1,6 @@
## Insufficient Summary
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not be the same as your project's Title.
Per section 5.3 of %RULES% your Summary can not be the same as your project's Title.
Your project summary should provide a brief overview of your project that informs and entices users.

View File

@@ -1,7 +1,6 @@
## Project Title
## Minecraft Project Names
Projects must not use Minecraft's branding or include "Minecraft" as a significant part of the title.
The title of your project may be confusingly similar to the game, and we encourage you to change your title to avoid a potential violation of Minecraft's Usage Guidelines.
Abbreviations like "MC" or elaborate titles that do not make the name Minecraft a significant portion of the name are okay.
Projects must not use Minecraft's branding or include "Minecraft" as a significant part of the title.
Your project's current Name of `%PROJECT_TITLE%` may be confusingly similar to, or imply association with, the game Minecraft. We encourage you to change your project's [Name](%PROJECT_SETTINGS_LINK%) to avoid a potential violation of Minecraft's Usage Guidelines.
Abbreviations like "MC" or elaborate titles that do not make the name Minecraft a significant portion of the name are okay.
When editing your project's Name, remember to update its [URL](%PROJECT_SETTINGS_LINK%) to match.

View File

@@ -0,0 +1,2 @@
You may reference the source work in the Description of your fork, however, you must do so in a way that is unlikely to cause confusion or imply association with or endorsement from the author of the source work.
Additionally, per section 4 of %RULES%, we ask that you ensure your project's Name and Branding abide by the license of the source work.

View File

@@ -0,0 +1 @@
You may reference the projects that make up the focus point of your modpack, however, you must do so in a way that is unlikely to cause confusion or imply association with or endorsement from the author of that content.

View File

@@ -1,3 +1,5 @@
## Project Branding
Per section 1.8 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) we ask that you change your project title and other relevant branding to avoid causing confusion or implying association with existing projects.
Per section 1.8 of %RULES%, your project or its branding must not imply association or be easily confused with any other person or organization.
We ask that you change your project's [Name](%PROJECT_SETTINGS_LINK%) and other relevant branding to avoid causing confusion or implying association with existing projects or individuals.
When editing your project's Name, remember to update its [URL](%PROJECT_SETTINGS_LINK%) to match.

View File

@@ -1,7 +1,6 @@
## Misuse of Title
## Misuse of Project Name
Per section 5.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) we ask that you limit the title to just the name of your project.
Additional information, such as themes, tags, supported versions or loaders, etc. should be saved for the Summary or Description.
When changing your project title, remember to also ensure that your project slug (URL) matches and accurately represents your project.
Per section 5.2 of %RULES%, your project's [Name](%PROJECT_SETTINGS_LINK%) should not include unnecessary information such as loaders, themes, tags, or versions.
Your project's current Name of `%PROJECT_TITLE%` appears to contain extra information.
We ask that you remove all additional information from the [Name](%PROJECT_SETTINGS_LINK%). Instead, consider including this in your project's Summary or Description, or as a part of its relevant metadata.
When editing your project's Name, remember to update its [URL](%PROJECT_SETTINGS_LINK%) to match.

View File

@@ -0,0 +1,4 @@
## No Versions
It looks like all versions of your project have been deleted, meaning that our Moderation Team cannot finish reviewing your project.
Please resubmit your project once you've uploaded a new version.

View File

@@ -0,0 +1,5 @@
## Unsupported Project
Per section 5.7 of [Modrinth's Content Rules](https://modrinth.com/legal/rules), Modrinth does not support uploading multiple variations of your project as Additional files.
Having alternate versions of your content on the same project will hurt the functionality of the Modrinth App and other supported launchers as it would prevent users from updating your content, and may make it harder for your users to find the content they want.
We ask that you upload each alternate version of your project as a new project, ensuring that all users will be able to access and easily find your content.

View File

@@ -0,0 +1,5 @@
## Unsupported Project
Modrinth does not support uploading projects that have unnecessary or extraneous installation steps.
Having alternate versions of your content on the same project will hurt the functionality of the Modrinth App and other supported launchers as it would prevent users from updating your content, and may make it harder for your users to find the content they want.
We ask that you upload each alternate version of your project as a new project, ensuring that all users will be able to access and easily find your content.

View File

@@ -0,0 +1,6 @@
## Unsupported Project
Modrinth does not support uploading multiple variations of your project as separate versions.
New Versions of your project should strictly be a linear upgrade of the same content.
Having alternate versions of your content on the same project will hurt the functionality of the Modrinth App and other supported launchers as it would prevent users from updating your content, and may make it harder for your users to find the content they want.
We ask that you upload each alternate version of your project as a new project, ensuring that all users will be able to access and easily find your content.

View File

@@ -0,0 +1,4 @@
## Incorrect Additional Files
Per section 5.7 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) the additional files section should only be used for specific designated purposes such as a `Sources.jar`.
To ensure a smooth experience for you and your users, please upload each alternate version of your modpack as its own Modpack project, thank you.

View File

@@ -0,0 +1,6 @@
## Unsupported Project
Modrinth does not support uploading multiple variations of your project as separate versions.
New Versions of your project should strictly be a linear upgrade of the same content.
Having alternate versions of your content on the same project will hurt the functionality of the Modrinth App and other supported launchers as it would prevent users from updating your content, and may make it harder for your users to find the content they want.
To ensure a smooth experience for you and your users, please upload each alternate version of your modpack as its own Modpack project, thank you.

View File

@@ -0,0 +1,5 @@
## Incorrect Additional Files
Per section 5.7 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) the additional files section should only be used for specific designated purposes such as a `Sources.jar`.
Modrinth does not support the upload of modpacks in the `.zip` format, as this may cause issues for Modrinth users or distribute copyrighted content without the proper permissions.
If you would like to upload a server-specific version of your modpack, consider creating a separate Modpack project.

View File

@@ -0,0 +1,5 @@
## Broken Version
It looks like you've uploaded multiple files with the same name to the same version. This triggers a bug that produces duplicate primary file entries.
This may cause issues for users and creators as it would prevent Modpacks that include this content from functioning in the Modrinth App and other launchers.
We ask that you delete and reupload all applicable versions. Be sure to only upload one file to each version before resubmission.

Some files were not shown because too many files have changed in this diff Show More