├── apps ├── .gitkeep ├── glowsquid-frontend │ ├── src │ │ ├── lib │ │ │ ├── index.ts │ │ │ ├── state.svelte.ts │ │ │ ├── index.test.ts │ │ │ └── bindings.ts │ │ ├── routes │ │ │ ├── +layout.ts │ │ │ ├── +layout.svelte │ │ │ ├── +page.svelte │ │ │ ├── header.svelte │ │ │ ├── app.css │ │ │ └── accountDropdown.svelte │ │ ├── app.d.ts │ │ └── app.html │ ├── setup-vitest.js │ ├── static │ │ ├── favicon.png │ │ ├── vite.svg │ │ ├── svelte.svg │ │ └── tauri.svg │ ├── .gitignore │ ├── eslint.config.js │ ├── README.md │ ├── tsconfig.json │ ├── svelte.config.js │ ├── moon.yml │ ├── package.json │ └── vite.config.js └── glowsquid │ ├── build.rs │ ├── db │ └── migrations │ │ ├── 20240623153115_init_auth.down.sql │ │ └── 20240623153115_init_auth.up.sql │ ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 32x32.png │ ├── icon.icns │ ├── StoreLogo.png │ ├── 128x128@2x.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ └── Square89x89Logo.png │ ├── .gitignore │ ├── capabilities │ └── migrated.json │ ├── src │ ├── database.rs │ ├── error.rs │ ├── main.rs │ └── auth │ │ ├── server.rs │ │ ├── mod.rs │ │ └── state.rs │ ├── setup-db.js │ ├── tauri.conf.json │ ├── Cargo.toml │ └── moon.yml ├── libs ├── .gitkeep ├── ui │ ├── src │ │ ├── theme │ │ │ ├── breakpoints.css │ │ │ ├── colors │ │ │ │ ├── light.css │ │ │ │ └── dark.css │ │ │ ├── sizing │ │ │ │ ├── compact.css │ │ │ │ └── default.css │ │ │ └── fonts │ │ │ │ ├── recursive │ │ │ │ ├── Recursive_VF_1.085--subset_range_latin_1.woff2 │ │ │ │ ├── Recursive_VF_1.085--subset_range_latin_ext.woff2 │ │ │ │ ├── Recursive_VF_1.085--subset_range_remaining.woff2 │ │ │ │ ├── Recursive_VF_1.085--subset_range_vietnamese.woff2 │ │ │ │ ├── Recursive_VF_1.085--subset_range_english_basic.woff2 │ │ │ │ └── Recursive_VF_1.085--subset_range_latin_1_punc.woff2 │ │ │ │ └── recursive.css │ │ ├── dropdown │ │ │ └── Dropdown.svelte │ │ ├── app.d.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── message │ │ │ ├── Message.svelte │ │ │ └── Message.test.ts │ │ ├── theme.ts │ │ ├── input │ │ │ └── Input.svelte │ │ └── button │ │ │ └── Button.svelte │ ├── setup-vitest.js │ ├── .gitignore │ ├── svelte.config.js │ ├── eslint.config.js │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── moon.yml │ ├── package.json │ └── README.md ├── copper │ ├── .gitignore │ ├── src │ │ ├── api │ │ │ ├── mod.rs │ │ │ ├── auth.rs │ │ │ ├── asset_index.rs │ │ │ └── version.rs │ │ ├── client │ │ │ ├── mod.rs │ │ │ ├── manifest.rs │ │ │ ├── assets.rs │ │ │ └── library.rs │ │ ├── lib.rs │ │ ├── downloader.rs │ │ ├── merger.rs │ │ └── parser.rs │ ├── tsconfig.json │ ├── moon.yml │ ├── README.md │ ├── Cargo.toml │ └── examples │ │ ├── asset_downloader.rs │ │ ├── auth.rs │ │ └── full_launch.rs ├── eslint-config │ ├── eslint.config.js │ ├── moon.yml │ ├── tsconfig.json │ ├── package.json │ └── index.js └── tauri-plugin-state │ ├── build.rs │ ├── eslint.config.js │ ├── .gitignore │ ├── permissions │ ├── autogenerated │ │ ├── reference.md │ │ └── commands │ │ │ ├── ping.toml │ │ │ └── execute.toml │ └── schemas │ │ └── schema.json │ ├── tsconfig.json │ ├── Cargo.toml │ ├── rollup.config.js │ ├── package.json │ ├── moon.yml │ ├── guest-js │ └── index.ts │ ├── README.md │ └── src │ ├── lib.rs │ └── state.rs ├── .cargo └── config.toml ├── .npmrc ├── pnpm-workspace.yaml ├── commitlint.config.js ├── rust-toolchain.toml ├── vitest.workspace.ts ├── .prototools ├── .moon ├── hooks │ ├── commit-msg.sh │ ├── commit-msg.ps1 │ ├── pre-commit.sh │ └── pre-commit.ps1 ├── tasks │ ├── typescript.yml │ ├── rust-library.yml │ ├── rust-application.yml │ ├── rust.yml │ └── tag-sveltekit.yml ├── workspace.yml └── toolchain.yml ├── .envrc ├── .editorconfig ├── .vscode ├── extensions.json └── settings.json ├── .sqlx ├── query-8378a16075954401c905ceff5427beddffb84b41637d0165382f0a85ce729879.json ├── query-a16ae5458e6d5845e84f448441ba181a7f6f656f89231c905054878b42645be0.json └── query-994db1bd44f973d8255728d25ad7ff34de85684bc4d6b56a4a8b23fe53e6d583.json ├── tsconfig.options.json ├── README.md ├── package.json ├── .gitignore ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ ├── ci.yml │ └── codecov.yml ├── flake.nix └── flake.lock /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/ui/src/theme/breakpoints.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/ui/src/dropdown/Dropdown.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/ui/src/theme/colors/light.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/ui/src/theme/sizing/compact.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target-dir = 'target' 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /libs/ui/src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "libs/*" 4 | -------------------------------------------------------------------------------- /libs/ui/setup-vitest.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest' 2 | -------------------------------------------------------------------------------- /apps/glowsquid/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/setup-vitest.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest' 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /libs/copper/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.direnv 3 | /.minecraft 4 | neovide_backtrace.log 5 | codecov.json 6 | -------------------------------------------------------------------------------- /libs/copper/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod asset_index; 2 | pub mod auth; 3 | pub mod client; 4 | pub mod version; 5 | -------------------------------------------------------------------------------- /libs/copper/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod assets; 2 | pub mod auth; 3 | pub mod library; 4 | pub mod manifest; 5 | -------------------------------------------------------------------------------- /libs/eslint-config/eslint.config.js: -------------------------------------------------------------------------------- 1 | import configEslint from './index.js' 2 | export default configEslint 3 | -------------------------------------------------------------------------------- /apps/glowsquid/db/migrations/20240623153115_init_auth.down.sql: -------------------------------------------------------------------------------- 1 | -- Add down migration script here 2 | DROP TABLE auth; -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rustfmt", "rust-src", "rust-analyzer"] 4 | -------------------------------------------------------------------------------- /apps/glowsquid/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/icon.ico -------------------------------------------------------------------------------- /apps/glowsquid/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/icon.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/128x128.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/32x32.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/icon.icns -------------------------------------------------------------------------------- /apps/glowsquid/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/StoreLogo.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/128x128@2x.png -------------------------------------------------------------------------------- /apps/glowsquid-frontend/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid-frontend/static/favicon.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /apps/glowsquid/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/apps/glowsquid/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /apps/glowsquid/.gitignore: -------------------------------------------------------------------------------- 1 | codecov.json 2 | db/glowsquid.db3 3 | 4 | # Generated by Tauri 5 | # will have schema files for capabilities auto-completion 6 | /gen/schemas 7 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'libs/*/vitest.config.ts', 3 | 'libs/*/vite.config.ts', 4 | 'apps/*/vite.config.ts', 5 | 'apps/*/vitest.config.ts', 6 | ] 7 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | // tauri config: disable preendering and ssr since we don't need either 2 | export const prerender = false 3 | export const ssr = false 4 | -------------------------------------------------------------------------------- /.prototools: -------------------------------------------------------------------------------- 1 | moon = "1.25.6" 2 | node = "~20" 3 | pnpm = "~9" 4 | rust = "stable" 5 | 6 | [plugins] 7 | moon = "https://raw.githubusercontent.com/moonrepo/moon/master/proto-plugin.toml" 8 | -------------------------------------------------------------------------------- /libs/ui/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /.moon/hooks/commit-msg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # Automatically generated by moon. DO NOT MODIFY! 5 | # https://moonrepo.dev/docs/guides/vcs-hooks 6 | 7 | pnpm commitlint --edit $1 8 | 9 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /libs/copper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.options.json", 3 | "include": [ 4 | "**/*" 5 | ], 6 | "references": [], 7 | "compilerOptions": { 8 | "paths": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/eslint-config/moon.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: Eslint Config 3 | description: Standard eslint config for the repo. Doesn't include ignore files 4 | 5 | type: configuration 6 | language: typescript 7 | platform: node 8 | -------------------------------------------------------------------------------- /libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_latin_1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_latin_1.woff2 -------------------------------------------------------------------------------- /libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_latin_ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_latin_ext.woff2 -------------------------------------------------------------------------------- /libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_remaining.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_remaining.woff2 -------------------------------------------------------------------------------- /libs/tauri-plugin-state/build.rs: -------------------------------------------------------------------------------- 1 | const COMMANDS: &[&str] = &["ping", "execute"]; 2 | 3 | fn main() { 4 | tauri_plugin::Builder::new(COMMANDS) 5 | .android_path("android") 6 | .ios_path("ios") 7 | .build(); 8 | } 9 | -------------------------------------------------------------------------------- /libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_vietnamese.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_vietnamese.woff2 -------------------------------------------------------------------------------- /libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_english_basic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_english_basic.woff2 -------------------------------------------------------------------------------- /libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_latin_1_punc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glowsquid-launcher/glowsquid/HEAD/libs/ui/src/theme/fonts/recursive/Recursive_VF_1.085--subset_range_latin_1_punc.woff2 -------------------------------------------------------------------------------- /.moon/hooks/commit-msg.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env powershell 2 | $ErrorActionPreference = 'Stop' 3 | 4 | # Automatically generated by moon. DO NOT MODIFY! 5 | # https://moonrepo.dev/docs/guides/vcs-hooks 6 | 7 | pnpm commitlint --edit $1 8 | 9 | -------------------------------------------------------------------------------- /.moon/hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # Automatically generated by moon. DO NOT MODIFY! 5 | # https://moonrepo.dev/docs/guides/vcs-hooks 6 | 7 | pnpm moon run :lint :format --affected --status=staged 8 | 9 | -------------------------------------------------------------------------------- /libs/eslint-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.options.json", 3 | "include": [ 4 | "**/*" 5 | ], 6 | "references": [], 7 | "compilerOptions": { 8 | "esModuleInterop": true, 9 | "paths": {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.moon/hooks/pre-commit.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env powershell 2 | $ErrorActionPreference = 'Stop' 3 | 4 | # Automatically generated by moon. DO NOT MODIFY! 5 | # https://moonrepo.dev/docs/guides/vcs-hooks 6 | 7 | pnpm moon run :lint :format --affected --status=staged 8 | 9 | -------------------------------------------------------------------------------- /.moon/tasks/typescript.yml: -------------------------------------------------------------------------------- 1 | $schema: "https://moonrepo.dev/schemas/tasks.json" 2 | 3 | fileGroups: 4 | sources: 5 | - "src/**/*" 6 | 7 | tasks: 8 | lint: 9 | command: "eslint ." 10 | format: 11 | command: "eslint --fix ." 12 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | if ! has nix_direnv_version || ! nix_direnv_version 2.1.1; then 2 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.1.1/direnvrc" "sha256-b6qJ4r34rbE23yWjMqbmu3ia2z4b2wIlZUksBke/ol0=" 3 | fi 4 | 5 | use flake 6 | layout node 7 | dotenv 8 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/lib/state.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { AuthState } from './bindings' 2 | import { tauriState } from 'tauri-plugin-state' 3 | 4 | export const authState = tauriState('auth', { 5 | profiles: [], 6 | currentProfileIndex: null, 7 | }) 8 | -------------------------------------------------------------------------------- /libs/copper/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] 2 | #![allow(clippy::multiple_crate_versions)] 3 | 4 | pub mod api; 5 | pub mod client; 6 | pub mod downloader; 7 | pub mod launcher; 8 | pub mod merger; 9 | pub mod parser; 10 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from '@repo/eslint-config' 2 | 3 | /** @type {import('eslint').Linter.FlatConfig[]} */ 4 | export default [ 5 | { 6 | ignores: ['node_modules', 'dist-js'], 7 | }, 8 | ...baseConfig, 9 | ] 10 | -------------------------------------------------------------------------------- /libs/ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /coverage 7 | 8 | # OS 9 | .DS_Store 10 | Thumbs.db 11 | 12 | # Env 13 | .env 14 | .env.* 15 | !.env.example 16 | !.env.test 17 | 18 | # Vitest 19 | vitest.config.js.timestamp-* 20 | vitest.config.ts.timestamp-* 21 | -------------------------------------------------------------------------------- /libs/ui/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | import { preprocessMeltUI } from '@melt-ui/pp' 3 | 4 | export default { 5 | preprocess: [vitePreprocess(), preprocessMeltUI()], 6 | compilerOptions: { 7 | runes: true, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/app.d.ts: -------------------------------------------------------------------------------- 1 | import 'unplugin-icons/types/svelte' 2 | 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {} 13 | -------------------------------------------------------------------------------- /libs/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | // Reexport your entry components here 2 | export { default as Message } from './message/Message.svelte' 3 | export { default as Dropdown } from './dropdown/Dropdown.svelte' 4 | export { default as Input } from './input/Input.svelte' 5 | export { default as Button } from './button/Button.svelte' 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vivaxy.vscode-conventional-commits", 4 | "rust-lang.rust-analyzer", 5 | "tauri-apps.tauri-vscode", 6 | "svelte.svelte-vscode", 7 | "tamasfe.even-better-toml", 8 | "moonrepo.moon-console", 9 | "vitest.explorer" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | .DS_Store 3 | .Thumbs.db 4 | *.sublime* 5 | .idea/ 6 | debug.log 7 | package-lock.json 8 | .vscode/settings.json 9 | yarn.lock 10 | 11 | /.tauri 12 | /target 13 | Cargo.lock 14 | node_modules/ 15 | /.rollup.cache 16 | /tsconfig.tsbuildinfo 17 | 18 | dist-js 19 | dist 20 | -------------------------------------------------------------------------------- /libs/ui/src/message/Message.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |

This app is sponsored by {sponsor}!

12 | -------------------------------------------------------------------------------- /libs/ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig, { createSvelteConfig } from '@repo/eslint-config' 2 | import svelteConfig from './svelte.config.js' 3 | 4 | /** @type {import('eslint').Linter.FlatConfig[]} */ 5 | export default [ 6 | { 7 | ignores: ['node_modules', 'coverage'], 8 | }, 9 | ...baseConfig, 10 | ...createSvelteConfig(svelteConfig), 11 | ] 12 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig, { createSvelteConfig } from '@repo/eslint-config' 2 | import svelteConfig from './svelte.config.js' 3 | 4 | /** @type {import('eslint').Linter.FlatConfig[]} */ 5 | export default [ 6 | { 7 | ignores: ['node_modules', 'dist', '.svelte-kit', 'src/lib/bindings.ts'], 8 | }, 9 | ...baseConfig, 10 | ...createSvelteConfig(svelteConfig), 11 | ] 12 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/permissions/autogenerated/reference.md: -------------------------------------------------------------------------------- 1 | | Permission | Description | 2 | |------|-----| 3 | |`allow-execute`|Enables the execute command without any pre-configured scope.| 4 | |`deny-execute`|Denies the execute command without any pre-configured scope.| 5 | |`allow-ping`|Enables the ping command without any pre-configured scope.| 6 | |`deny-ping`|Denies the ping command without any pre-configured scope.| 7 | -------------------------------------------------------------------------------- /.sqlx/query-8378a16075954401c905ceff5427beddffb84b41637d0165382f0a85ce729879.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "\n DELETE FROM auth\n WHERE id = ?\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 1 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "8378a16075954401c905ceff5427beddffb84b41637d0165382f0a85ce729879" 12 | } 13 | -------------------------------------------------------------------------------- /apps/glowsquid/capabilities/migrated.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "migrated", 3 | "description": "permissions that were migrated from v1", 4 | "local": true, 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "path:default", 10 | "event:default", 11 | "window:default", 12 | "app:default", 13 | "resources:default", 14 | "menu:default", 15 | "tray:default" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /libs/copper/moon.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: Copper 3 | description: Minecraft launcher library for rust 4 | 5 | type: library 6 | language: rust 7 | platform: rust 8 | 9 | tasks: 10 | example/auth: 11 | command: cargo run --example auth 12 | example/assets: 13 | command: cargo run --example asset_downloader 14 | example/launch: 15 | command: cargo run --example full_launch 16 | 17 | env: 18 | CARGO_TERM_COLOR: "always" 19 | -------------------------------------------------------------------------------- /libs/copper/README.md: -------------------------------------------------------------------------------- 1 | # copper 2 | 3 | ## A rust minecraft launcher 4 | 5 | Built with reqwest and serde. 6 | 7 | ## Features 8 | 9 | - [x] can actually download minecraft 10 | - [x] (secure!) authentication builtin via oauth2 11 | - [x] quickplay support 12 | 13 | **MICROSOFT ONLY** because Mojang auth has been removed. 14 | 15 | Currently, this is being used as the launcher backend for the Glowsquid launcher. 16 | 17 | ## TODO 18 | 19 | - Full tauri bindings 20 | -------------------------------------------------------------------------------- /libs/ui/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | import { svelteTesting } from '@testing-library/svelte/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [svelte(), svelteTesting()], 7 | test: { 8 | coverage: { 9 | reporter: ['json', 'lcov'], 10 | }, 11 | environment: 'jsdom', 12 | setupFiles: ['./setup-vitest.js'], 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/permissions/autogenerated/commands/ping.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-ping" 7 | description = "Enables the ping command without any pre-configured scope." 8 | commands.allow = ["ping"] 9 | 10 | [[permission]] 11 | identifier = "deny-ping" 12 | description = "Denies the ping command without any pre-configured scope." 13 | commands.deny = ["ping"] 14 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + SvelteKit + Typescript App 8 | %sveltekit.head% 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /.moon/tasks/rust-library.yml: -------------------------------------------------------------------------------- 1 | $schema: "https://moonrepo.dev/schemas/tasks.json" 2 | 3 | tasks: 4 | test: 5 | command: "cargo nextest run" 6 | inputs: 7 | - "@globs(sources)" 8 | - "@globs(tests)" 9 | coverage: 10 | command: "cargo llvm-cov nextest --all-features --codecov --output-path codecov.json" 11 | inputs: 12 | - "@globs(sources)" 13 | - "@globs(tests)" 14 | outputs: 15 | - "codecov.json" 16 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/permissions/autogenerated/commands/execute.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-execute" 7 | description = "Enables the execute command without any pre-configured scope." 8 | commands.allow = ["execute"] 9 | 10 | [[permission]] 11 | identifier = "deny-execute" 12 | description = "Denies the execute command without any pre-configured scope." 13 | commands.deny = ["execute"] 14 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/README.md: -------------------------------------------------------------------------------- 1 | # Tauri + SvelteKit + TypeScript 2 | 3 | This template should help get you started developing with Tauri, SvelteKit and TypeScript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer). 8 | -------------------------------------------------------------------------------- /tsconfig.options.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "noFallthroughCasesInSwitch": true, 5 | "noImplicitOverride": true, 6 | "noImplicitReturns": true, 7 | "noPropertyAccessFromIndexSignature": true, 8 | "noUncheckedIndexedAccess": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "skipLibCheck": true, 12 | "module": "Preserve", 13 | "moduleResolution": "Bundler" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-a16ae5458e6d5845e84f448441ba181a7f6f656f89231c905054878b42645be0.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "\n INSERT INTO auth (username, id, expires_at, access_token, ms_refresh_token, ms_access_token, encrypted_token)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 7 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "a16ae5458e6d5845e84f448441ba181a7f6f656f89231c905054878b42645be0" 12 | } 13 | -------------------------------------------------------------------------------- /libs/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "resolveJsonModule": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "module": "Preserve", 11 | "composite": true, 12 | "moduleResolution": "Bundler" 13 | }, 14 | "references": [ 15 | { 16 | "path": "../eslint-config" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /apps/glowsquid/db/migrations/20240623153115_init_auth.up.sql: -------------------------------------------------------------------------------- 1 | -- Add up migration script here 2 | CREATE TABLE auth ( 3 | id TEXT PRIMARY KEY NOT NULL, 4 | -- minecraft username 5 | username VARCHAR(16) UNIQUE NOT NULL, 6 | -- determines if all access tokens are encrypted and require a password to decrypt 7 | encrypted_token BOOLEAN NOT NULL, 8 | access_token TEXT NOT NULL, 9 | ms_access_token TEXT NOT NULL, 10 | ms_refresh_token TEXT NOT NULL, 11 | -- ISO 8601 date format 12 | expires_at TEXT NOT NULL 13 | ) -------------------------------------------------------------------------------- /libs/tauri-plugin-state/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noImplicitAny": true, 10 | "composite": true 11 | }, 12 | "include": [ 13 | "guest-js/*.ts" 14 | ], 15 | "exclude": [ 16 | "dist-js", 17 | "node_modules" 18 | ], 19 | "references": [ 20 | { 21 | "path": "../eslint-config" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /apps/glowsquid-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "moduleResolution": "bundler" 12 | }, 13 | "references": [ 14 | { 15 | "path": "../../libs/tauri-plugin-state" 16 | }, 17 | { 18 | "path": "../../libs/ui" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /libs/ui/src/message/Message.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { describe, it, expect } from 'vitest' 4 | import { render, screen } from '@testing-library/svelte/svelte5' 5 | import Message from './Message.svelte' 6 | 7 | describe('message component', () => { 8 | it('can load the component', () => { 9 | render(Message, { sponsor: 'absolutely no one' }) 10 | 11 | const greeting = screen.getByText(/This app/u) 12 | 13 | expect(greeting).toBeInTheDocument() 14 | expect(greeting.innerHTML).toBe('This app is sponsored by absolutely no one!') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /libs/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "main": "./index.js", 6 | "devDependencies": { 7 | "@eslint/js": "^9.5.0", 8 | "@types/eslint": "^8.56.7", 9 | "@types/eslint__js": "^8.42.3", 10 | "@stylistic/eslint-plugin": "^2.2.2", 11 | "typescript-eslint": "8.0.0-alpha.30", 12 | "@typescript-eslint/utils": "8.0.0-alpha.30", 13 | "eslint-plugin-svelte": "2.40.0", 14 | "svelte": "^4.2.18" 15 | }, 16 | "overrides": { 17 | "@typescript-eslint/utils": "$@typescript-eslint/utils" 18 | } 19 | } -------------------------------------------------------------------------------- /.moon/workspace.yml: -------------------------------------------------------------------------------- 1 | $schema: https://moonrepo.dev/schemas/workspace.json 2 | projects: 3 | - apps/* 4 | - libs/* 5 | 6 | vcs: 7 | manager: "git" 8 | defaultBranch: "dev" 9 | syncHooks: true 10 | hooks: 11 | pre-commit: 12 | - "pnpm moon run :lint :format --affected --status=staged" 13 | commit-msg: 14 | - "pnpm commitlint --edit $1" 15 | 16 | runner: 17 | archivableTargets: 18 | - ":check" 19 | - ":lint" 20 | - ":test" 21 | - ":coverage" 22 | - ":lint-clippy" 23 | - ":lint-format" 24 | - ":test-unit" 25 | - ":test-intergration" 26 | -------------------------------------------------------------------------------- /libs/ui/moon.yml: -------------------------------------------------------------------------------- 1 | $schema: "https://moonrepo.dev/schemas/project.json" 2 | 3 | project: 4 | name: UI 5 | description: Common UI components and utilities 6 | 7 | type: library 8 | stack: frontend 9 | language: typescript 10 | platform: node 11 | 12 | tasks: 13 | test: 14 | command: pnpm vitest run 15 | inputs: 16 | - src/**/*.ts 17 | - vitest.config.ts 18 | - package.json 19 | coverage: 20 | command: pnpm vitest run --coverage 21 | inputs: 22 | - src/**/*.ts 23 | - vitest.config.ts 24 | - package.json 25 | outputs: 26 | - coverage/ 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glowsquid Launcher 2 | 3 | ![Lines of code](https://img.shields.io/tokei/lines/github/glowsquid-launcher/glowsquid?style=for-the-badge) 4 | ![Discord](https://img.shields.io/discord/1050624267592663050?style=for-the-badge) 5 | 6 | ## Developing 7 | 8 | Once you've installed dependencies with `npm install` and made sure tauri is setup, start a development server + tauri: 9 | 10 | ```bash 11 | npm run tauri dev 12 | ``` 13 | 14 | ## Building 15 | 16 | To create a production version of your app: 17 | 18 | ```bash 19 | npm run tauri build 20 | ``` 21 | 22 | You can preview the production build with by using the binary in `src-tauri/target/release/glowsquid` 23 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 |
20 | 21 |
22 | {@render children()} 23 |
24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@glowsquid/source", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "moon run :build", 7 | "test": "moon run :test", 8 | "lint": "moon run :lint", 9 | "updater": "pnpm update -i -r --latest", 10 | "format": "moon run :format" 11 | }, 12 | "private": true, 13 | "type": "module", 14 | "devDependencies": { 15 | "@commitlint/cli": "^19.3.0", 16 | "@commitlint/config-conventional": "^19.2.2", 17 | "@monodon/rust": "^1.4.0", 18 | "@moonrepo/cli": "^1.25.6", 19 | "vitest": "^1.6.0" 20 | }, 21 | "engines": { 22 | "node": "~20" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/glowsquid/src/database.rs: -------------------------------------------------------------------------------- 1 | use sqlx::{migrate, migrate::MigrateError, SqlitePool}; 2 | 3 | #[derive(Clone)] 4 | pub struct DbState { 5 | pool: SqlitePool, 6 | } 7 | impl DbState { 8 | /// Initializes and runs migrations on the database 9 | /// 10 | /// TODO: error handling and propagation 11 | /// TODO: logging 12 | pub async fn new(pool: SqlitePool) -> Result { 13 | migrate!("db/migrations").run(&pool).await?; 14 | 15 | Ok(Self { pool }) 16 | } 17 | 18 | /// Get a connection to the database 19 | pub fn connection(&self) -> SqlitePool { 20 | // connection is a cheap clone as it's just a Sender internally 21 | self.pool.clone() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/glowsquid/setup-db.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env npx zx 2 | 3 | // create db/glowsquid.db3 files in root and apps/glowsquid if not exists 4 | 5 | cd('../..'); 6 | if (!fs.existsSync('db/glowsquid.db3')) { 7 | console.log('Creating db/glowsquid.db3'); 8 | await fs.outputFile('db/glowsquid.db3', ''); 9 | await $("cargo sqlx migrate run -D sqlite://db/glowsquid.db3 --source apps/glowsquid/db/migrations") 10 | } 11 | 12 | if (!fs.existsSync('apps/glowsquid/db/glowsquid.db3')) { 13 | console.log('Creating apps/glowsquid/db/glowsquid.db3'); 14 | await fs.outputFile('apps/glowsquid/db/glowsquid.db3', ''); 15 | await $("cargo sqlx migrate run -D sqlite://apps/glowsquid/db/glowsquid.db3 --source apps/glowsquid/db/migrations") 16 | } 17 | -------------------------------------------------------------------------------- /libs/ui/src/theme/sizing/default.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --size-thin: 0.1rem; 3 | --size-sm: 0.5rem; 4 | --size-md: 1rem; 5 | --size-lg: 1.5rem; 6 | --size-xl: 2rem; 7 | 8 | --rounded-sm: 0.25rem; 9 | --rounded-md: 0.5rem; 10 | --rounded-lg: 1rem; 11 | 12 | --font-size-sm: clamp(0.83rem, 0.21vi + 0.78rem, 0.95rem); 13 | --font-size-base: clamp(1rem, 0.34vi + 0.91rem, 1.19rem); 14 | --font-size-md: clamp(1.2rem, 0.52vi + 1.07rem, 1.48rem); 15 | --font-size-lg: clamp(1.44rem, 0.76vi + 1.25rem, 1.86rem); 16 | --font-size-xl: clamp(1.73rem, 1.08vi + 1.46rem, 2.32rem); 17 | --font-size-xxl: clamp(2.07rem, 1.5vi + 1.7rem, 2.9rem); 18 | --font-size-xxxl: clamp(2.49rem, 2.06vi + 1.97rem, 3.62rem); 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | /.direnv 37 | /target 38 | /.minecraft 39 | /logs 40 | /db 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | 46 | # moon 47 | .moon/cache 48 | .moon/docker 49 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/svelte.config.js: -------------------------------------------------------------------------------- 1 | // Tauri doesn't have a Node.js server to do proper SSR 2 | // so we will use adapter-static to prerender the app (SSG) 3 | // See: https://beta.tauri.app/start/frontend/sveltekit/ for more info 4 | import adapter from '@sveltejs/adapter-static' 5 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 6 | import { preprocessMeltUI } from '@melt-ui/pp' 7 | 8 | /** @type {import('@sveltejs/kit').Config} */ 9 | const config = { 10 | preprocess: [vitePreprocess(), preprocessMeltUI()], 11 | compilerOptions: { 12 | runes: true, 13 | }, 14 | kit: { 15 | adapter: adapter({ 16 | pages: 'dist', 17 | assets: 'dist', 18 | fallback: 'index.html', 19 | }), 20 | }, 21 | } 22 | 23 | export default config 24 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/moon.yml: -------------------------------------------------------------------------------- 1 | $schema: "https://moonrepo.dev/schemas/project.json" 2 | 3 | project: 4 | name: Glowsquid Frontend 5 | description: Frontend code for Glowsquid 6 | 7 | type: library 8 | stack: frontend 9 | language: typescript 10 | platform: node 11 | 12 | tasks: 13 | test: 14 | command: noop 15 | deps: 16 | # - test-intergration 17 | - test-unit 18 | # TODO: Add test-intergration through webdriver 19 | # test-intergration: 20 | # command: 'pnpm playwright test' 21 | # inputs: 22 | # - '@globs(sources)' 23 | # - '@globs(tests)' 24 | test-unit: 25 | command: "pnpm vitest run" 26 | inputs: 27 | - "@globs(sources)" 28 | - "@globs(tests)" 29 | 30 | tags: 31 | - sveltekit 32 | -------------------------------------------------------------------------------- /apps/glowsquid/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "frontendDist": "../glowsquid-frontend/dist", 4 | "devUrl": "http://localhost:1420" 5 | }, 6 | "bundle": { 7 | "active": true, 8 | "targets": "all", 9 | "icon": [ 10 | "icons/32x32.png", 11 | "icons/128x128.png", 12 | "icons/128x128@2x.png", 13 | "icons/icon.icns", 14 | "icons/icon.ico" 15 | ] 16 | }, 17 | "productName": "Glowsquid", 18 | "version": "0.0.0", 19 | "identifier": "in.wobbl.glowsquid", 20 | "plugins": {}, 21 | "app": { 22 | "security": { 23 | "csp": null 24 | }, 25 | "windows": [ 26 | { 27 | "title": "Glowsquid" 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.moon/tasks/rust-application.yml: -------------------------------------------------------------------------------- 1 | $schema: "https://moonrepo.dev/schemas/tasks.json" 2 | 3 | tasks: 4 | build: 5 | command: "cargo build --release" 6 | inputs: 7 | - "@globs(sources)" 8 | deps: 9 | - "^:build" 10 | preview: 11 | command: "cargo run --release" 12 | deps: 13 | - build 14 | - "^:build" 15 | - "^:preview" 16 | local: true 17 | test: 18 | command: "cargo nextest run" 19 | inputs: 20 | - "@globs(sources)" 21 | - "@globs(tests)" 22 | coverage: 23 | command: "cargo llvm-cov nextest --all-features --codecov --output-path codecov.json" 24 | inputs: 25 | - "@globs(sources)" 26 | - "@globs(tests)" 27 | outputs: 28 | - "codecov.json" 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [workspace] 3 | members = ['libs/copper', 'apps/glowsquid', 'libs/tauri-plugin-state'] 4 | resolver = "2" 5 | 6 | [workspace.dependencies] 7 | serde_json = "1.0.114" 8 | serde = { version = "1.0.197", features = ["derive"] } 9 | oauth2 = "4.4.2" 10 | 11 | specta = { version = "=2.0.0-rc.12", features = [ 12 | "serde", 13 | "serde_json", 14 | "typescript", 15 | ] } 16 | tauri-specta = { version = "=2.0.0-rc.11", features = [ 17 | "javascript", 18 | "typescript", 19 | ] } 20 | 21 | reqwest = { version = "0.12.5", default-features = false, features = [ 22 | "rustls-tls", 23 | "json", 24 | "stream", 25 | ] } 26 | 27 | chrono = { version = "0.4.38", default-features = false, features = [ 28 | "std", 29 | "serde", 30 | ] } 31 | 32 | error-stack = { version = "0.4.1", features = ["spantrace", "serde"] } 33 | 34 | [profile.release] 35 | lto = true 36 | -------------------------------------------------------------------------------- /libs/ui/src/theme.ts: -------------------------------------------------------------------------------- 1 | export const sizings = [ 2 | 'sm', 3 | 'md', 4 | 'lg', 5 | 'xl', 6 | ] as const 7 | 8 | export type Sizing = typeof sizings[number] 9 | 10 | export const baseColors = [ 11 | 'base', 12 | 'surface', 13 | 'overlay', 14 | 'primary', 15 | ] as const 16 | 17 | export type BaseColor = typeof baseColors[number] 18 | 19 | export const colors = [ 20 | 'base', 21 | 'surface', 22 | 'overlay', 23 | 'primary', 24 | 'secondary', 25 | 'green', 26 | 'red', 27 | 'blue', 28 | 'orange', 29 | 'teal', 30 | ] 31 | 32 | export type Color = typeof colors[number] 33 | 34 | export const meaningfulColors = [ 35 | 'primary', 36 | 'secondary', 37 | 'success', 38 | 'warning', 39 | 'danger', 40 | 'link', 41 | 'muted', 42 | ] as const 43 | 44 | export type MeaningfulColor = typeof meaningfulColors[number] 45 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-plugin-state" 3 | version = "0.1.0" 4 | authors = ["Suyashtnt "] 5 | description = "Dead simple state management for tauri" 6 | license = "MIT" 7 | homepage = "https://github.com/glowsquid-launcher/glowsquid/tree/dev/libs/tauri-plugin-state" 8 | repository = "https://github.com/glowsquid-launcher/glowsquid" 9 | documentation = "https://docs.rs/tauri-plugin-state" 10 | edition = "2021" 11 | rust-version = "1.70" 12 | exclude = ["/webview-dist", "/webview-src", "/node_modules"] 13 | links = "tauri-plugin-state" 14 | 15 | [dependencies] 16 | tauri = { version = "=2.0.0-beta.22", features = [] } 17 | specta.workspace = true 18 | tauri-specta.workspace = true 19 | serde.workspace = true 20 | thiserror = "1.0" 21 | tokio = "1.38.0" 22 | 23 | [build-dependencies] 24 | tauri-plugin = { version = "2.0.0-beta.17", features = ["build"] } 25 | -------------------------------------------------------------------------------- /libs/ui/src/theme/colors/dark.css: -------------------------------------------------------------------------------- 1 | /* based of kleur for now */ 2 | :root { 3 | /** base colors */ 4 | --bg-base: oklch(13% 0.03 284); 5 | --fg-base: oklch(88% 0.03 284); 6 | 7 | --bg-surface: oklch(15% 0.05 284); 8 | --fg-surface: oklch(90% 0.05 284); 9 | 10 | --bg-overlay: oklch(20% 0.05 284); 11 | --fg-overlay: oklch(94% 0.07 284); 12 | 13 | --bg-primary: oklch(35% 0.2 284); 14 | --fg-primary: oklch(77% 0.2 284); 15 | 16 | 17 | /** foreground colors */ 18 | 19 | --red: oklch(62% 0.2 27); 20 | --orange: oklch(77% 0.2 83) 21 | --secondary: oklch(77% 0.2 240); 22 | --blue: oklch(77% 0.2 240); 23 | --teal: oklch(77% 0.2 190); 24 | --green: oklch(77% 0.2 152); 25 | 26 | /** meaningful colors */ 27 | --success: var(--green); 28 | --warning: var(--orange); 29 | --danger: var(--red); 30 | --muted: oklch(30% 0.04 284); 31 | } 32 | -------------------------------------------------------------------------------- /.moon/tasks/rust.yml: -------------------------------------------------------------------------------- 1 | $schema: "https://moonrepo.dev/schemas/tasks.json" 2 | 3 | fileGroups: 4 | sources: 5 | - "src/**/*" 6 | - "Cargo.toml" 7 | tests: 8 | - "benches/**/*" 9 | - "tests/**/*" 10 | 11 | tasks: 12 | check: 13 | command: "cargo check" 14 | inputs: 15 | - "@globs(sources)" 16 | format: 17 | command: "cargo fmt" 18 | inputs: 19 | - "@globs(sources)" 20 | - "@globs(tests)" 21 | lint: 22 | command: noop 23 | deps: 24 | - lint-clippy 25 | - lint-format 26 | lint-clippy: 27 | command: "cargo clippy" 28 | inputs: 29 | - "@globs(sources)" 30 | - "@globs(tests)" 31 | lint-format: 32 | command: "cargo fmt --check" 33 | inputs: 34 | - "@globs(sources)" 35 | - "@globs(tests)" 36 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { join } from 'path' 3 | import { cwd } from 'process' 4 | import typescript from '@rollup/plugin-typescript' 5 | 6 | const pkg = JSON.parse(readFileSync(join(cwd(), 'package.json'), 'utf8')) 7 | 8 | export default { 9 | input: 'guest-js/index.ts', 10 | output: [ 11 | { 12 | file: pkg.exports.import, 13 | format: 'esm', 14 | }, 15 | { 16 | file: pkg.exports.require, 17 | format: 'cjs', 18 | }, 19 | ], 20 | plugins: [ 21 | typescript({ 22 | declaration: true, 23 | declarationDir: `./${pkg.exports.import.split('/')[0]}`, 24 | }), 25 | ], 26 | external: [ 27 | /^@tauri-apps\/api/, 28 | ...Object.keys(pkg.dependencies || {}), 29 | ...Object.keys(pkg.peerDependencies || {}), 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /.moon/tasks/tag-sveltekit.yml: -------------------------------------------------------------------------------- 1 | $schema: "https://moonrepo.dev/schemas/tasks.json" 2 | 3 | fileGroups: 4 | sources: 5 | - "src/**/*" 6 | - "static/**/*" 7 | tests: 8 | - "tests/**/*" 9 | 10 | tasks: 11 | dev: 12 | command: "vite dev" 13 | deps: 14 | - "^:dev" 15 | local: true 16 | preview: 17 | command: "vite preview" 18 | deps: 19 | - build 20 | - "^:build" 21 | - "^:preview" 22 | local: true 23 | build: 24 | command: "vite build" 25 | deps: 26 | - ^:build 27 | inputs: 28 | - "@globs(sources)" 29 | outputs: 30 | - "dist" 31 | check: 32 | command: "svelte-check --tsconfig ./tsconfig.json" 33 | inputs: 34 | - "@globs(sources)" 35 | deps: 36 | - sync 37 | sync: 38 | command: "svelte-kit sync" 39 | inputs: 40 | - "@globs(sources)" 41 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tauri-plugin-state", 3 | "version": "0.1.0", 4 | "author": "Suyashtnt", 5 | "description": "", 6 | "type": "module", 7 | "types": "./dist-js/index.d.ts", 8 | "main": "./dist-js/index.cjs", 9 | "module": "./dist-js/index.js", 10 | "exports": { 11 | "types": "./dist-js/index.d.ts", 12 | "import": "./dist-js/index.js", 13 | "require": "./dist-js/index.cjs" 14 | }, 15 | "files": [ 16 | "dist-js", 17 | "README.md" 18 | ], 19 | "scripts": { 20 | "build": "rollup -c", 21 | "prepublishOnly": "pnpm build", 22 | "pretest": "pnpm build" 23 | }, 24 | "dependencies": { 25 | "@tauri-apps/api": ">=2.0.0-beta.6" 26 | }, 27 | "devDependencies": { 28 | "@repo/eslint-config": "workspace:*", 29 | "@rollup/plugin-typescript": "^11.1.6", 30 | "rollup": "^4.9.6", 31 | "tslib": "^2.6.2", 32 | "typescript": "^5.3.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glowsquid", 3 | "version": "0.0.0", 4 | "description": "", 5 | "type": "module", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@tauri-apps/api": "2.0.0-beta.13", 9 | "@melt-ui/svelte": "^0.81.0", 10 | "@repo/ui": "workspace:*", 11 | "tauri-plugin-state": "workspace:*" 12 | }, 13 | "devDependencies": { 14 | "@iconify-json/material-symbols": "^1.1.82", 15 | "@melt-ui/pp": "^0.3.2", 16 | "@sveltejs/adapter-static": "^3.0.1", 17 | "@sveltejs/kit": "^2.5.17", 18 | "@sveltejs/vite-plugin-svelte": "^3.1.1", 19 | "@tauri-apps/cli": "2.0.0-beta.20", 20 | "@testing-library/jest-dom": "^6.4.6", 21 | "@testing-library/svelte": "^5.1.0", 22 | "jsdom": "^24.1.0", 23 | "svelte": "5.0.0-next.162", 24 | "svelte-check": "^3.6.0", 25 | "tslib": "^2.6.3", 26 | "typescript": "^5.5.2", 27 | "unplugin-icons": "^0.19.0", 28 | "vite": "^5.0.3", 29 | "vitest": "^1.6.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/ui", 3 | "version": "0.0.1", 4 | "exports": { 5 | ".": { 6 | "types": "./src/index.ts", 7 | "svelte": "./src/index.ts" 8 | }, 9 | "./theme/": "./src/theme/" 10 | }, 11 | "files": [ 12 | "dist", 13 | "!dist/**/*.test.*", 14 | "!dist/**/*.spec.*" 15 | ], 16 | "peerDependencies": { 17 | "svelte": "^4.0.0" 18 | }, 19 | "devDependencies": { 20 | "@melt-ui/pp": "^0.3.2", 21 | "@melt-ui/svelte": "^0.81.0", 22 | "@repo/eslint-config": "workspace:*", 23 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 24 | "@testing-library/jest-dom": "^6.4.6", 25 | "@testing-library/svelte": "^5.1.0", 26 | "@types/eslint": "^8.56.7", 27 | "@vitest/coverage-v8": "^1.6.0", 28 | "jsdom": "^24.1.0", 29 | "svelte": "5.0.0-next.162", 30 | "typescript": "^5.5.2", 31 | "vitest": "^1.6.0" 32 | }, 33 | "svelte": "./src/index.ts", 34 | "types": "./src/index.ts", 35 | "type": "module" 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Glowsquid Launcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/copper/src/api/auth.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use veil::Redact; 3 | 4 | pub struct XboxLiveResponse; 5 | pub struct XstsResponse; 6 | 7 | #[derive(Redact, Deserialize)] 8 | pub(crate) struct MinecraftResponse { 9 | #[redact] 10 | pub access_token: String, 11 | pub expires_in: i64, 12 | } 13 | 14 | #[derive(Redact, Deserialize)] 15 | #[serde(rename_all = "PascalCase")] 16 | pub(crate) struct XboxResponse { 17 | #[serde(skip)] 18 | marker: std::marker::PhantomData, 19 | #[redact] 20 | token: String, 21 | display_claims: DisplayClaims, 22 | } 23 | 24 | impl XboxResponse { 25 | #[must_use] 26 | pub(crate) fn uhs(&self) -> Option<&str> { 27 | self.display_claims.xui.first().map(|xui| xui.uhs.as_str()) 28 | } 29 | 30 | pub(crate) fn token(&self) -> &str { 31 | &self.token 32 | } 33 | } 34 | 35 | #[derive(Debug, Deserialize)] 36 | struct DisplayClaims { 37 | xui: Vec, 38 | } 39 | 40 | #[derive(Debug, Deserialize)] 41 | struct Xui { 42 | uhs: String, 43 | } 44 | 45 | #[derive(Debug, Deserialize, Clone)] 46 | pub(crate) struct MinecraftProfile { 47 | pub id: String, 48 | pub name: String, 49 | } 50 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { sveltekit } from '@sveltejs/kit/vite' 3 | import { svelteTesting } from '@testing-library/svelte/vite' 4 | import Icons from 'unplugin-icons/vite' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig(async () => ({ 8 | plugins: [ 9 | sveltekit(), 10 | svelteTesting(), 11 | Icons({ 12 | compiler: 'svelte', 13 | autoInstall: true, 14 | }), 15 | ], 16 | 17 | optimizeDeps: { 18 | exclude: ['tauri-plugin-state'], 19 | }, 20 | 21 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 22 | // 23 | // 1. prevent vite from obscuring rust errors 24 | clearScreen: false, 25 | // 2. tauri expects a fixed port, fail if that port is not available 26 | server: { 27 | port: 1420, 28 | strictPort: true, 29 | fs: { 30 | allow: ['../../'], 31 | }, 32 | }, 33 | test: { 34 | coverage: { 35 | reporter: ['json'], 36 | }, 37 | environment: 'jsdom', 38 | setupFiles: ['./setup-vitest.js'], 39 | }, 40 | })) 41 | -------------------------------------------------------------------------------- /apps/glowsquid/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "glowsquid" 3 | version = "0.0.0" 4 | description = "Next-gen hyperspeed Minecraft launcher" 5 | authors = ["Suyashtnt "] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [build-dependencies] 11 | tauri-build = { version = "2.0.0-beta", features = [] } 12 | 13 | [dependencies] 14 | tauri = { version = "=2.0.0-beta.22", features = [] } 15 | copper = { path = "../../libs/copper" } 16 | tauri-plugin-state = { path = "../../libs/tauri-plugin-state" } 17 | 18 | serde.workspace = true 19 | serde_json.workspace = true 20 | 21 | oauth2.workspace = true 22 | error-stack.workspace = true 23 | reqwest.workspace = true 24 | 25 | once_cell = "1.19.0" 26 | axum = "0.7.5" 27 | tokio = "1.38.0" 28 | open = "5.1.4" 29 | 30 | specta.workspace = true 31 | tauri-specta.workspace = true 32 | sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"] } 33 | prqlx = "0.2.1" 34 | chrono.workspace = true 35 | futures = "0.3.30" 36 | 37 | [features] 38 | # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! 39 | custom-protocol = ["tauri/custom-protocol"] 40 | -------------------------------------------------------------------------------- /apps/glowsquid/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error, 3 | fmt::{self, Debug}, 4 | }; 5 | 6 | use serde::Serialize; 7 | use specta::{Any, Type}; 8 | 9 | /// A custom error type that wraps around a error-stack error, converting it from Report to Error 10 | /// 11 | /// Also known as the most unholy error type known to crabkind 12 | #[derive(Debug, Serialize, Type)] 13 | pub struct Error { 14 | error: T, 15 | #[specta(type = Any)] 16 | report: error_stack::Report, 17 | } 18 | 19 | impl error::Error for Error {} 20 | 21 | impl fmt::Display for Error { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | std::fmt::Display::fmt(&self.report, f) 24 | } 25 | } 26 | 27 | // copy instead of clone to enforce cheapness 28 | impl From> 29 | for Error 30 | { 31 | fn from(report: error_stack::Report) -> Self { 32 | let error = *report.current_context(); 33 | Self { error, report } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |

Welcome to Glowsquid!

18 | 19 |
20 | 21 | 22 |
23 | 24 |

{greetMsg}

25 | 26 | 27 |
28 | 29 | 50 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/lib/index.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test, vi } from 'vitest' 2 | import { randomFillSync } from 'crypto' 3 | 4 | import { mockIPC } from '@tauri-apps/api/mocks' 5 | import { invoke } from '@tauri-apps/api/core' 6 | 7 | // jsdom doesn't come with a WebCrypto implementation 8 | beforeAll(() => { 9 | Object.defineProperty(window, 'crypto', { 10 | value: { 11 | // @ts-expect-error buffer is fine 12 | getRandomValues: (buffer) => { 13 | return randomFillSync(buffer) 14 | }, 15 | }, 16 | }) 17 | }) 18 | 19 | test('IPC mocks work', async () => { 20 | // @ts-expect-error we know it's a number output 21 | mockIPC(async (cmd, payload) => { 22 | switch (cmd) { 23 | case 'add': 24 | // @ts-expect-error we know it's a number 25 | return (payload?.a as number) + (payload?.b as number) 26 | default: 27 | break 28 | } 29 | }) 30 | 31 | // we can use the spying tools provided by vitest to track the mocked function 32 | // @ts-expect-error tauri internals does indeed exist due to mockIPC 33 | const spy = vi.spyOn(window.__TAURI_INTERNALS__, 'invoke') 34 | 35 | expect(invoke('add', { a: 12, b: 15 })).resolves.toBe(27) 36 | expect(spy).toHaveBeenCalled() 37 | }) 38 | -------------------------------------------------------------------------------- /apps/glowsquid/moon.yml: -------------------------------------------------------------------------------- 1 | $schema: "https://moonrepo.dev/schemas/project.json" 2 | 3 | fileGroups: 4 | sources: 5 | - "src/**/*" 6 | - "icons/**/*" 7 | - "tauri.conf.json" 8 | - "Cargo.toml" 9 | 10 | project: 11 | name: Glowsquid 12 | description: Tauri code and actual app for Glowsquid 13 | 14 | type: application 15 | stack: frontend 16 | language: rust 17 | platform: rust 18 | 19 | tasks: 20 | dev: 21 | command: noop 22 | deps: 23 | - prepare 24 | - dev-tauri 25 | - ^:dev 26 | local: true 27 | dev-tauri: 28 | command: "cargo tauri dev" 29 | local: true 30 | build: 31 | command: "tauri build" 32 | deps: 33 | - prepare 34 | - ^:build 35 | outputs: 36 | - "/target/release/glowsquid*" 37 | - "!/target/release/glowsquid.d" 38 | - "!/target/release/glowsquid.pdb" 39 | - "/target/release/bundle/**/*" 40 | options: 41 | mergeArgs: "replace" 42 | coverage: 43 | deps: 44 | # tauri proc macro moment 45 | - glowsquid-frontend:build 46 | prepare: 47 | command: "npx zx setup-db.js" 48 | options: 49 | cache: false 50 | 51 | tags: 52 | - tauri 53 | 54 | dependsOn: 55 | - glowsquid-frontend 56 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/moon.yml: -------------------------------------------------------------------------------- 1 | $schema: "https://moonrepo.dev/schemas/project.json" 2 | 3 | project: 4 | name: Tauri Plugin State 5 | description: Simple yet effective state management for Tauri apps 6 | 7 | type: library 8 | stack: frontend 9 | language: typescript 10 | platform: node 11 | 12 | tasks: 13 | build: 14 | command: "pnpm rollup -c" 15 | inputs: 16 | - rollup.config.js 17 | - guest-js 18 | dev: 19 | command: "pnpm rollup -c -w" 20 | local: true 21 | test: 22 | command: noop 23 | deps: 24 | - test-doctest 25 | - test-nextest 26 | - test-js 27 | test-js: 28 | command: noop 29 | inputs: 30 | - rollup.config.js 31 | - guest-js 32 | deps: 33 | - build 34 | test-doctest: 35 | command: "cargo test --doc" 36 | inputs: 37 | - src 38 | - Cargo.toml 39 | test-nextest: 40 | command: "cargo nextest run" 41 | inputs: 42 | - src 43 | - Cargo.toml 44 | release: 45 | command: noop 46 | deps: 47 | - publish-npm 48 | - publish-crate 49 | publish-npm: 50 | command: "pnpm publish" 51 | deps: 52 | - build 53 | publish-crate: 54 | command: "cargo publish" 55 | -------------------------------------------------------------------------------- /.sqlx/query-994db1bd44f973d8255728d25ad7ff34de85684bc4d6b56a4a8b23fe53e6d583.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT username, id, expires_at, access_token, ms_refresh_token, ms_access_token, encrypted_token FROM auth", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "username", 8 | "ordinal": 0, 9 | "type_info": "Text" 10 | }, 11 | { 12 | "name": "id", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | }, 16 | { 17 | "name": "expires_at", 18 | "ordinal": 2, 19 | "type_info": "Text" 20 | }, 21 | { 22 | "name": "access_token", 23 | "ordinal": 3, 24 | "type_info": "Text" 25 | }, 26 | { 27 | "name": "ms_refresh_token", 28 | "ordinal": 4, 29 | "type_info": "Text" 30 | }, 31 | { 32 | "name": "ms_access_token", 33 | "ordinal": 5, 34 | "type_info": "Text" 35 | }, 36 | { 37 | "name": "encrypted_token", 38 | "ordinal": 6, 39 | "type_info": "Bool" 40 | } 41 | ], 42 | "parameters": { 43 | "Right": 0 44 | }, 45 | "nullable": [ 46 | false, 47 | false, 48 | false, 49 | false, 50 | false, 51 | false, 52 | false 53 | ] 54 | }, 55 | "hash": "994db1bd44f973d8255728d25ad7ff34de85684bc4d6b56a4a8b23fe53e6d583" 56 | } 57 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/routes/header.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 22 |
23 | 24 | 57 | -------------------------------------------------------------------------------- /.moon/toolchain.yml: -------------------------------------------------------------------------------- 1 | # https://moonrepo.dev/docs/config/toolchain 2 | $schema: "https://moonrepo.dev/schemas/toolchain.json" 3 | 4 | node: 5 | packageManager: "pnpm" 6 | 7 | # Add `node.version` as a constraint in the root `package.json` `engines`. 8 | addEnginesConstraint: true 9 | 10 | # Currently buggy and dedupes even with up to date lockfiles 11 | dedupeOnLockfileChange: false 12 | 13 | dependencyVersionFormat: "workspace" 14 | 15 | inferTasksFromScripts: false 16 | 17 | # Support the "one version policy" by only declaring dependencies in the root `package.json`. 18 | # rootPackageOnly: true 19 | 20 | # Sync a project's relationships as `dependencies` within the project's `package.json`. 21 | syncProjectWorkspaceDependencies: true 22 | 23 | # Configures Rust within the toolchain. 24 | rust: 25 | version: "stable" 26 | bins: 27 | [ 28 | "cargo-watch", 29 | "tauri-cli@^2.0.0-beta", 30 | "cargo-nextest", 31 | "cargo-llvm-cov", 32 | "sqlx-cli", 33 | ] 34 | components: ["rustfmt", "rust-src", "rust-analyzer", "llvm-tools-preview"] 35 | targets: [] 36 | 37 | syncToolchainConfig: true 38 | 39 | typescript: 40 | createMissingConfig: true 41 | includeProjectReferenceSources: false 42 | includeSharedTypes: true 43 | routeOutDirToCache: false 44 | syncProjectReferences: true 45 | syncProjectReferencesToPaths: false 46 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/static/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/routes/app.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | background-color: var(--bg-base); 4 | color: var(--fg-base); 5 | font-size: var(--font-size-base); 6 | } 7 | 8 | body { 9 | min-height: 100vh; 10 | } 11 | 12 | button 13 | input, 14 | button, 15 | textarea, 16 | select { 17 | font: inherit; 18 | font-size: var(--font-size-base); 19 | } 20 | 21 | /* 22 | Josh's Custom CSS Reset 23 | https://www.joshwcomeau.com/css/custom-css-reset/ 24 | */ 25 | 26 | *, 27 | *::before, 28 | *::after { 29 | box-sizing: border-box; 30 | } 31 | 32 | * { 33 | margin: 0; 34 | } 35 | 36 | body { 37 | /* A nicer line height default */ 38 | line-height: calc(1em + 0.5rem); 39 | 40 | font-synthesis: none; 41 | text-rendering: optimizeLegibility; 42 | -moz-osx-font-smoothing: grayscale; 43 | -webkit-text-size-adjust: 100%; 44 | -webkit-font-smoothing: antialiased; 45 | } 46 | 47 | img, 48 | picture, 49 | video, 50 | canvas, 51 | svg { 52 | display: block; 53 | max-width: 100%; 54 | } 55 | 56 | p, 57 | h1, 58 | h2, 59 | h3, 60 | h4, 61 | h5, 62 | h6 { 63 | overflow-wrap: break-word; 64 | } 65 | 66 | p { 67 | font-size: var(--font-size-base) 68 | } 69 | 70 | small { 71 | font-size: var(--font-size-sm); 72 | } 73 | 74 | h1 { 75 | font-size: var(--font-size-xxxl); 76 | } 77 | 78 | h2 { 79 | font-size: var(--font-size-xxl); 80 | } 81 | 82 | h3 { 83 | font-size: var(--font-size-xl); 84 | } 85 | 86 | h4 { 87 | font-size: var(--font-size-lg); 88 | } 89 | -------------------------------------------------------------------------------- /libs/copper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "copper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["suyashtnt "] 6 | description = "Minecraft launcher: rust edition" 7 | documentation = "https://docs.rs/copper" 8 | readme = "README.md" 9 | license = "MIT" 10 | homepage = "https://github.com/glowsquid-launcher/glowsquid#readme" 11 | repository = "https://github.com/glowsquid-launcher/glowsquid" 12 | keywords = ["minecraft", "launcher", "game"] 13 | categories = ["api-bindings"] 14 | 15 | [dependencies] 16 | serde.workspace = true 17 | serde_json.workspace = true 18 | 19 | error-stack.workspace = true 20 | oauth2.workspace = true 21 | reqwest.workspace = true 22 | specta.workspace = true 23 | chrono.workspace = true 24 | 25 | derive_builder = { version = "0.20.0", features = ["clippy"] } 26 | dunce = "1.0.4" 27 | futures = "0.3.30" 28 | hex = "0.4.3" 29 | itertools = "0.13.0" 30 | sha1 = "0.10.6" 31 | test-case = "3.3.1" 32 | tokio = { version = "1.37.0", features = ["fs", "process", "macros"] } 33 | tracing = "0.1.40" 34 | tracing-error = "0.2.0" 35 | veil = "0.1.7" 36 | zip = "2.1.3" 37 | 38 | [dev-dependencies] 39 | axum = { version = "0.7.5", features = ["tracing"] } 40 | tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] } 41 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 42 | indicatif = "0.17.8" 43 | java-locator = "0.1.5" 44 | 45 | # why do you have to do this mojang 46 | [target.'cfg(target_os = "windows")'.dependencies] 47 | winsafe = { version = "0.0.20", features = ["kernel"] } 48 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/guest-js/index.ts: -------------------------------------------------------------------------------- 1 | import { emit, listen } from '@tauri-apps/api/event' 2 | 3 | let internalStateStore: Map = new Map() 4 | 5 | /** 6 | * Set's up the js side for state management. This should be called once on page start/reload. 7 | * 8 | * @param mapImpl The map implementation to use for the state store. Defaults to a new Map instance. If you're using Svelte 5, you can use the map from `svelte/reactivity` to make the state reactive. 9 | */ 10 | export const setupState = (mapImpl: Map = new Map()) => { 11 | internalStateStore.forEach((value, key) => { 12 | mapImpl.set(key, value) 13 | }) 14 | 15 | internalStateStore = mapImpl 16 | 17 | listen<{ key: string, value: unknown }>('plugin:state-change', (event) => { 18 | const { key, value } = event.payload 19 | internalStateStore?.set(key, value) 20 | }) 21 | 22 | emit('plugin:state-ready') 23 | } 24 | 25 | /** 26 | * Create a state getter object that can be used to get the current value of the state. 27 | * @param key The key to use for the state. 28 | * @param defaultValue The default value to use for the state when it's not set. 29 | * @returns A getter object with a `value` property that returns the current value of the state. 30 | */ 31 | export const tauriState = (key: string, defaultValue: T) => { 32 | if (!internalStateStore.has(key)) { 33 | internalStateStore.set(key, defaultValue) 34 | } 35 | 36 | return { 37 | get value() { 38 | return internalStateStore.get(key) as T 39 | }, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /libs/copper/src/api/asset_index.rs: -------------------------------------------------------------------------------- 1 | use error_stack::{report, ResultExt}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::to_string; 4 | use std::{collections::HashMap, path::Path}; 5 | use tokio::fs; 6 | use tracing::debug; 7 | 8 | use super::client::SaveError; 9 | 10 | #[derive(Debug, Serialize, Deserialize, Clone)] 11 | pub struct Assets { 12 | pub(crate) objects: HashMap, 13 | } 14 | 15 | impl Assets { 16 | /// Saves the manifest to disk. 17 | /// 18 | /// # Errors 19 | /// Returns a [`SaveError`] if the manifest could not be serialized or if an IO error occurred. 20 | #[tracing::instrument] 21 | pub async fn save_to_disk(&self, path: &Path) -> error_stack::Result<(), SaveError> { 22 | debug!("Saving Asset index to disk"); 23 | debug!("Serializing index to JSON"); 24 | let value = to_string(self).change_context(SaveError::SerializeError)?; 25 | 26 | let directory = path.parent().ok_or_else(|| report!(SaveError::IOError))?; 27 | 28 | if !directory.exists() { 29 | debug!("Creating directory {}", directory.display()); 30 | fs::create_dir_all(directory) 31 | .await 32 | .change_context(SaveError::IOError)?; 33 | } 34 | 35 | debug!("Writing asset index to {}", path.display()); 36 | fs::write(path, value) 37 | .await 38 | .change_context(SaveError::IOError) 39 | } 40 | } 41 | 42 | #[derive(Debug, Serialize, Deserialize, Clone)] 43 | pub struct Object { 44 | pub(crate) hash: String, 45 | pub(crate) size: u64, 46 | } 47 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/README.md: -------------------------------------------------------------------------------- 1 | # Tauri Plugin state 2 | 3 | ## A Stupid simple state management plugin for Tauri 4 | 5 | Let's say you want to manage state between your frontend and backend, but you want to keep it in sync and _completely_ managed from rust. 6 | 7 | This plugin is for you. 8 | 9 | ## Frontend Framework guides 10 | 11 | By default, we use a simple `Map` object to store state on the frontend. 12 | If you need reactivity, frameworks have their own reactive Map implementations that you can use. 13 | 14 | - Svelte: `import { Map } from 'svelte/reactivity';` 15 | - Vue: You can wrap a map in a `reactive` object. `setupState(reactive(new Map()));` 16 | - SolidJS: You can use to create a reactive map. `import { ReactiveMap } from "@solid-primitives/map";` 17 | - React: N/A. No idea how to do this in React. Make a PR if you know how. 18 | 19 | ## Manually watching for state changes 20 | 21 | All state changes are broadcasted to the frontend via Tauri's native event API. You can listen for these changes by listening to the `plugin:state-change` event. 22 | 23 | ```js 24 | import { listen } from '@tauri-apps/api/event' 25 | 26 | listen<{ key: string, value: unknown }>('plugin:state-change', (event) => { 27 | const { key, value } = event.payload 28 | console.log(`State change for key ${key} with value ${value}`) 29 | }) 30 | ``` 31 | 32 | How you type `value` is up to you. We recommend using a discriminated union to type the value. 33 | 34 | ## Panic guide 35 | 36 | This plugin considers not being able to sync state as an invariant violation and will panic if it can't sync state. We always want to be in sync to make sure nothing goes wrong. 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main", "dev"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | env: 10 | MOONBASE_SECRET_KEY: ${{ secrets.MOONBASE_SECRET_KEY }} 11 | DO_NOT_TRACK: 1 12 | 13 | jobs: 14 | build: 15 | name: Build and Lint 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | index: [0, 1, 2] 20 | steps: 21 | - name: Check out code 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Install binstall 27 | uses: cargo-bins/cargo-binstall@main 28 | 29 | - uses: "moonrepo/setup-toolchain@v0" 30 | with: 31 | auto-install: true 32 | 33 | - name: Get pnpm store directory 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 37 | 38 | - uses: actions/cache@v4 39 | name: Cache Pnpm 40 | with: 41 | path: ${{ env.STORE_PATH }} 42 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 43 | restore-keys: | 44 | ${{ runner.os }}-pnpm-store- 45 | 46 | - name: install tauri dependencies 47 | run: | 48 | sudo apt-get update 49 | sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 50 | 51 | - name: Lint and Build 52 | run: "pnpm moon ci --job ${{ matrix.index }} --jobTotal 3 :lint :build" 53 | 54 | - name: report to moon app 55 | uses: "moonrepo/run-report-action@v1" 56 | if: success() || failure() 57 | with: 58 | access-token: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/src/lib.rs: -------------------------------------------------------------------------------- 1 | use tauri::{ 2 | plugin::{Builder, TauriPlugin}, 3 | Manager, Runtime, 4 | }; 5 | 6 | mod state; 7 | 8 | pub use state::{State, StateGuard}; 9 | 10 | struct PluginState 11 | where 12 | R: Runtime, 13 | { 14 | /// Builds or returns existing state. 15 | build_fn: fn(&tauri::AppHandle) -> (), 16 | } 17 | 18 | /// Initializes the state plugin. 19 | /// 20 | /// # Arguments 21 | /// - `build_fn` - A function that builds or returns existing state. 22 | /// 23 | /// ```no_run 24 | /// use tauri::{async_runtime::block_on, Manager}; 25 | /// use tauri_plugin_state::State; 26 | /// 27 | /// // Since this is designed to be used with specta for typesafety, we need to derive specta::Type 28 | /// // Even though it's not strictly necessary for this whole thing to work 29 | /// #[derive(Clone, serde::Serialize, specta::Type)] 30 | /// struct AuthState { 31 | /// profiles: Vec, 32 | /// } 33 | /// 34 | /// tauri_plugin_state::init(|app| { 35 | /// let handle = app.app_handle().clone(); 36 | /// if !handle.manage(State::new("auth", AuthState { profiles: vec![] }, handle.clone())) { 37 | /// block_on(handle.state::>().sync()); 38 | /// } 39 | /// }); 40 | /// ``` 41 | pub fn init(build_fn: fn(&tauri::AppHandle) -> ()) -> TauriPlugin { 42 | Builder::new("state") 43 | .setup(move |app, _| { 44 | app.manage(PluginState { build_fn }); 45 | 46 | let app = app.clone(); 47 | let app_handle = app.clone(); 48 | app.listen("plugin:state-ready", move |_| { 49 | let state: tauri::State> = app_handle.state(); 50 | (state.build_fn)(&app_handle) 51 | }); 52 | 53 | Ok(()) 54 | }) 55 | .build() 56 | } 57 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ], 7 | "editor.formatOnSave": true, 8 | "svelte.enable-ts-plugin": true, 9 | "eslint.useFlatConfig": true, 10 | "eslint.format.enable": true, 11 | "eslint.enable": true, 12 | "rust-analyzer.testExplorer": true, 13 | "rust-analyzer.interpret.tests": true, 14 | "rust-analyzer.check.command": "clippy", 15 | "rust-analyzer.server.extraEnv": { 16 | "DATABASE_URL": "sqlite://db/glowsquid.db3" 17 | }, 18 | "rust-analyzer.check.extraEnv": { 19 | "DATABASE_URL": "sqlite://db/glowsquid.db3" 20 | }, 21 | "rust-analyzer.cargo.extraEnv": { 22 | "DATABASE_URL": "sqlite://db/glowsquid.db3" 23 | }, 24 | "eslint.validate": [ 25 | "svelte", 26 | "typescript", 27 | "javascript", 28 | "json", 29 | "yaml" 30 | ], 31 | "eslint.probe": [ 32 | "svelte", 33 | "typescript", 34 | "javascript", 35 | "json", 36 | "yaml" 37 | ], 38 | "conventionalCommits.scopes": [ 39 | "copper", 40 | "glowsquid", 41 | "tauri-plugin-state", 42 | "tauri-plugin-state" 43 | ], 44 | "yaml.schemas": { 45 | "https://moonrepo.dev/schemas/workspace.json": ".moon/workspace.yml", 46 | "https://moonrepo.dev/schemas/project.json": "**/moon.yml", 47 | "https://moonrepo.dev/schemas/tasks.json": ".moon/tasks/*.yml" 48 | }, 49 | "rust-analyzer.showUnlinkedFileNotification": false, 50 | "cSpell.words": [ 51 | "glowsquid", 52 | "Mojang", 53 | "Pkce", 54 | "quickplay", 55 | "reqwest", 56 | "rusqlite", 57 | "Singleplayer", 58 | "specta", 59 | "Tauri", 60 | "typesafety" 61 | ] 62 | } -------------------------------------------------------------------------------- /libs/ui/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). 4 | 5 | Read more about creating a library [in the docs](https://kit.svelte.dev/docs/packaging). 6 | 7 | ## Creating a project 8 | 9 | If you're seeing this, you've probably already done this step. Congrats! 10 | 11 | ```bash 12 | # create a new project in the current directory 13 | npm create svelte@latest 14 | 15 | # create a new project in my-app 16 | npm create svelte@latest my-app 17 | ``` 18 | 19 | ## Developing 20 | 21 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 22 | 23 | ```bash 24 | npm run dev 25 | 26 | # or start the server and open the app in a new browser tab 27 | npm run dev -- --open 28 | ``` 29 | 30 | Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app. 31 | 32 | ## Building 33 | 34 | To build your library: 35 | 36 | ```bash 37 | npm run package 38 | ``` 39 | 40 | To create a production version of your showcase app: 41 | 42 | ```bash 43 | npm run build 44 | ``` 45 | 46 | You can preview the production build with `npm run preview`. 47 | 48 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 49 | 50 | ## Publishing 51 | 52 | Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)). 53 | 54 | To publish your library to [npm](https://www.npmjs.com): 55 | 56 | ```bash 57 | npm publish 58 | ``` 59 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | push: 5 | branches: ['main', 'dev'] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | env: 10 | MOONBASE_SECRET_KEY: ${{ secrets.MOONBASE_SECRET_KEY }} 11 | DO_NOT_TRACK: 1 12 | 13 | jobs: 14 | codecov: 15 | name: Test and Codecov 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Check out code 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Install binstall 24 | uses: cargo-bins/cargo-binstall@main 25 | 26 | - uses: 'moonrepo/setup-toolchain@v0' 27 | with: 28 | auto-install: true 29 | 30 | - name: Get pnpm store directory 31 | shell: bash 32 | run: | 33 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 34 | 35 | - uses: actions/cache@v4 36 | name: Cache Pnpm 37 | with: 38 | path: ${{ env.STORE_PATH }} 39 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 40 | restore-keys: | 41 | ${{ runner.os }}-pnpm-store- 42 | 43 | - name: Install playwright 44 | run: pnpm dlx playwright install --with-deps 45 | 46 | - name: install tauri dependencies 47 | run: | 48 | sudo apt-get update 49 | sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 50 | 51 | - name: Test and Codecov 52 | run: 'pnpm moon run :coverage' 53 | 54 | - name: report to moon app 55 | uses: 'moonrepo/run-report-action@v1' 56 | if: success() || failure() 57 | with: 58 | access-token: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | - name: Upload coverage reports to Codecov 61 | uses: codecov/codecov-action@v4.0.1 62 | with: 63 | token: ${{ secrets.CODECOV_TOKEN }} 64 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/static/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/ui/src/input/Input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {#if startIcon} 16 | 17 | {@render startIcon()} 18 | 19 | {/if} 20 | 21 | 29 |
30 | 31 | 32 | 80 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | 6 | rust-overlay = { 7 | url = "github:oxalica/rust-overlay"; 8 | inputs = { 9 | nixpkgs.follows = "nixpkgs"; 10 | }; 11 | }; 12 | }; 13 | 14 | outputs = { 15 | self, 16 | nixpkgs, 17 | flake-utils, 18 | rust-overlay, 19 | }: 20 | flake-utils.lib.eachDefaultSystem (system: let 21 | pkgs = import nixpkgs { 22 | inherit system; 23 | overlays = [(import rust-overlay)]; 24 | }; 25 | 26 | rustTC = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 27 | 28 | libraries = with pkgs; [ 29 | openssl_3 30 | pkg-config 31 | libGL 32 | mesa_drivers 33 | glfw-wayland 34 | libglvnd 35 | webkitgtk_4_1 36 | gtk3 37 | cairo 38 | gdk-pixbuf 39 | glib 40 | dbus 41 | librsvg 42 | libxkbcommon 43 | ]; 44 | 45 | packages = with pkgs; [ 46 | # binaries 47 | nodejs 48 | corepack 49 | rustTC 50 | cargo-watch 51 | cargo-binstall # just in case 52 | playwright-driver.browsers 53 | temurin-jre-bin-21 54 | 55 | # lsps 56 | nil 57 | nodePackages.svelte-language-server 58 | nodePackages.typescript-language-server 59 | marksman 60 | alejandra 61 | ]; 62 | in { 63 | devShell = pkgs.mkShell { 64 | buildInputs = packages; 65 | nativeBuildInputs = libraries; 66 | 67 | PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers; 68 | PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = true; 69 | LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath libraries; 70 | PKG_CONFIG_PATH = pkgs.lib.makeLibraryPath libraries; 71 | RUST_SRC_PATH = "${rustTC}/lib/rustlib/src/rust/library/"; 72 | }; 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /libs/ui/src/button/Button.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 | 79 | -------------------------------------------------------------------------------- /apps/glowsquid/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | mod auth; 5 | pub mod database; 6 | pub mod error; 7 | 8 | use auth::*; 9 | use specta::TypeCollection; 10 | use sqlx::SqlitePool; 11 | use tauri::{async_runtime::block_on, Manager}; 12 | 13 | #[tauri::command] 14 | #[specta::specta] 15 | fn greet(name: &str) -> String { 16 | format!("Hello, {}! You've been greeted from Rust!", name) 17 | } 18 | 19 | fn main() { 20 | let invoke_handler = { 21 | let builder = tauri_specta::ts::builder() 22 | .types(TypeCollection::default().register::()) 23 | .commands(tauri_specta::collect_commands![ 24 | greet, 25 | add_account, 26 | remove_account, 27 | switch_to_account 28 | ]); 29 | 30 | #[cfg(debug_assertions)] // <- Only export on non-release builds 31 | let builder = builder.path("../glowsquid-frontend/src/lib/bindings.ts"); 32 | 33 | builder.build().unwrap() 34 | }; 35 | 36 | tauri::Builder::default() 37 | .setup(|app| { 38 | let data_path = app.path().app_data_dir()?; 39 | let db_path = data_path.join("glowsquid.db3"); 40 | 41 | if !db_path.exists() { 42 | std::fs::create_dir_all(&data_path)?; 43 | std::fs::File::create(&db_path)?; 44 | } 45 | 46 | let db = block_on(async move { SqlitePool::connect(db_path.to_str().unwrap()).await })?; 47 | 48 | let db_state = block_on(database::DbState::new(db))?; 49 | 50 | app.manage(db_state); 51 | 52 | Ok(()) 53 | }) 54 | .plugin(tauri_plugin_state::init(|app| { 55 | let handle = app.app_handle().clone(); 56 | if !handle.manage(block_on(auth::state::State::new(&handle))) { 57 | block_on(handle.state::().sync()); 58 | } 59 | })) 60 | .invoke_handler(invoke_handler) 61 | .run(tauri::generate_context!()) 62 | .expect("error while running tauri application"); 63 | } 64 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1719506693, 24 | "narHash": "sha256-C8e9S7RzshSdHB7L+v9I51af1gDM5unhJ2xO1ywxNH8=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "b2852eb9365c6de48ffb0dc2c9562591f652242a", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1719627476, 52 | "narHash": "sha256-LBfULF+2sCaWmkjmj1LkkGrAS/E9ZdXU1A5wWKjt9p0=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "5be53be9e5c766fc72fc5d65ba8a566cc0c3217f", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /libs/eslint-config/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js' 4 | import svelte from 'eslint-plugin-svelte' 5 | import typescript from 'typescript-eslint' 6 | import stylistic from '@stylistic/eslint-plugin' 7 | import svelteParser from 'svelte-eslint-parser' 8 | 9 | /** @type {import("eslint").Linter.FlatConfig[]} */ 10 | // @ts-expect-error this is a valid eslint config 11 | export default [ 12 | eslint.configs.recommended, 13 | ...typescript.configs.recommended, 14 | stylistic.configs.customize({ 15 | flat: true, 16 | indent: 4, 17 | }), 18 | ({ 19 | files: ['**/*.ts'], 20 | languageOptions: { 21 | parser: typescript.parser, 22 | parserOptions: { 23 | ecmaVersion: 'latest', 24 | extraFileExtensions: ['.svelte'], 25 | sourceType: 'module', 26 | }, 27 | }, 28 | rules: { 29 | 'no-undef': 'off', 30 | }, 31 | }), 32 | ] 33 | 34 | /** 35 | * Creates a svelte eslint config 36 | * 37 | * @param {import('svelte-eslint-parser').SvelteConfig} svelteConfig 38 | * @returns {import("eslint").Linter.FlatConfig[]} 39 | * */ 40 | export const createSvelteConfig = svelteConfig => ([ 41 | ...svelte.configs['flat/recommended'].map(({ rules, ...rest }) => ({ 42 | rules: { 43 | ...rules, 44 | }, 45 | ...rest, 46 | })), 47 | { 48 | files: ['*.svelte', '**/*.svelte'], 49 | languageOptions: { 50 | parser: svelteParser, 51 | parserOptions: { 52 | parser: typescript.parser, 53 | svelteConfig, 54 | svelteFeatures: { 55 | runes: true, 56 | experimentalGenerics: true, 57 | }, 58 | }, 59 | }, 60 | rules: { 61 | 'no-undef': 'off', 62 | }, 63 | processor: svelte.processors.svelte, 64 | }, 65 | { 66 | files: ['**/*.svelte.ts', '*.svelte.ts'], 67 | languageOptions: { 68 | parser: svelteParser, 69 | parserOptions: { 70 | parser: typescript.parser, 71 | svelteConfig, 72 | svelteFeatures: { 73 | runes: true, 74 | experimentalGenerics: true, 75 | }, 76 | }, 77 | }, 78 | }, 79 | ]) 80 | -------------------------------------------------------------------------------- /libs/copper/src/downloader.rs: -------------------------------------------------------------------------------- 1 | use error_stack::Result; 2 | use std::{ 3 | error::Error, 4 | fmt::{self, Display, Formatter}, 5 | future::Future, 6 | sync::Arc, 7 | }; 8 | 9 | use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; 10 | 11 | #[derive(Debug)] 12 | /// An error that can occur when downloading a file 13 | pub enum DownloadError { 14 | /// An error occured when requesting/downloading the file 15 | ReqwestError, 16 | /// An I/O error occured when writing or reading the file 17 | IoError, 18 | /// An error occured when joining the download task 19 | JoinError, 20 | /// An error occured during channel communication (such as sending progress updates) 21 | ChannelError, 22 | /// An error occured when extracting the downloaded file (if applicable) 23 | ExtractionError, 24 | } 25 | 26 | impl Display for DownloadError { 27 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 28 | f.write_str(match self { 29 | Self::ReqwestError => "Reqwest Error", 30 | Self::IoError => "I/O Error", 31 | Self::JoinError => "Join Error", 32 | Self::ChannelError => "Channel Error", 33 | Self::ExtractionError => "Extraction Error", 34 | }) 35 | } 36 | } 37 | 38 | impl Error for DownloadError {} 39 | 40 | #[derive(Debug)] 41 | pub enum DownloadMessage { 42 | /// A file was successfully downloaded 43 | Downloaded(T), 44 | /// When you recieve this event, you can be sure that all file have been downloaded. 45 | /// This is only sent once. You must close the channel after this, else your program will hang 46 | DownloadedAll, 47 | /// (T, downloaded bytes) 48 | DownloadProgress(T, u64), 49 | } 50 | 51 | pub type DownloadChannelSender = UnboundedSender>; 52 | pub type DownloadChannelReceiver = UnboundedReceiver>; 53 | 54 | pub trait Downloader { 55 | type DownloadItem; 56 | 57 | /// Creates a channel for downloading 58 | fn create_channel(&mut self) -> DownloadChannelReceiver; 59 | 60 | /// Downloads all files for this specific downloader. Requires an Arc for multithreading 61 | /// purposes 62 | /// 63 | /// See the downloaders documentation for more information. Usually appends the path to the 64 | /// main directory. 65 | fn download_all(self: Arc) -> impl Future> + Send; 66 | } 67 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/static/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /apps/glowsquid/src/auth/server.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt, sync::Arc}; 2 | 3 | use axum::{extract, response::IntoResponse}; 4 | use copper::client::auth::{MicrosoftAuthenticator, MinecraftToken, OauthCode}; 5 | use oauth2::{CsrfToken, PkceCodeVerifier}; 6 | use serde::Serialize; 7 | use specta::Type; 8 | use tauri::async_runtime; 9 | 10 | use error_stack::ResultExt; 11 | 12 | #[derive(Debug, Clone, Copy, Type, Serialize)] 13 | pub enum AuthError { 14 | MsToken, 15 | MinecraftToken, 16 | MinecraftProfile, 17 | } 18 | 19 | impl fmt::Display for AuthError { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | match self { 22 | AuthError::MsToken => f.write_str("Failed to get Microsoft token"), 23 | AuthError::MinecraftToken => f.write_str("Failed to get Minecraft token"), 24 | AuthError::MinecraftProfile => f.write_str("Failed to get Minecraft profile"), 25 | } 26 | } 27 | } 28 | 29 | impl Error for AuthError {} 30 | 31 | #[derive(Clone)] 32 | pub(super) struct AuthServerState { 33 | pub oauth: MicrosoftAuthenticator, 34 | pub csrf_token: CsrfToken, 35 | pub pkce_verifier: Arc, 36 | pub send_token: async_runtime::Sender>, 37 | } 38 | 39 | pub(super) async fn get_code( 40 | extract::Query(code): extract::Query, 41 | extract::State(state): extract::State, 42 | ) -> Result<&'static str, impl IntoResponse> { 43 | let ms_token = match state 44 | .oauth 45 | .get_ms_access_token(code, state.csrf_token, &state.pkce_verifier) 46 | .await 47 | .change_context(AuthError::MsToken) 48 | { 49 | Ok(token) => token, 50 | Err(e) => { 51 | state.send_token.send(Err(e)).await.unwrap(); 52 | return Err(( 53 | axum::http::StatusCode::INTERNAL_SERVER_ERROR, 54 | "Failed to get Microsoft token! Check glowsquid for extra information and to report this issue.", 55 | )); 56 | } 57 | }; 58 | 59 | let token = match state 60 | .oauth 61 | .get_minecraft_token(ms_token.clone()) 62 | .await 63 | .change_context(AuthError::MinecraftToken) 64 | { 65 | Ok(token) => token, 66 | Err(e) => { 67 | state.send_token.send(Err(e)).await.unwrap(); 68 | return Err(( 69 | axum::http::StatusCode::INTERNAL_SERVER_ERROR, 70 | "Failed to get Minecraft token! Check glowsquid for extra information and to report this issue.", 71 | )); 72 | } 73 | }; 74 | 75 | state.send_token.send(Ok(token)).await.unwrap(); 76 | 77 | Ok("Authenticated! You can now close this window and go back to Glowsquid.") 78 | } 79 | -------------------------------------------------------------------------------- /apps/glowsquid/src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod server; 2 | pub mod state; 3 | 4 | use std::{future::IntoFuture, sync::Arc}; 5 | 6 | use axum::{routing, Router}; 7 | use tauri::async_runtime; 8 | use tokio::net::TcpListener; 9 | 10 | use error_stack::ResultExt; 11 | 12 | use crate::error::Error; 13 | 14 | #[tauri::command] 15 | #[specta::specta] 16 | /// Switch to a different account 17 | /// 18 | /// # Returns 19 | /// returns Ok(()) if the account was switched successfully, otherwise returns Err(()) 20 | pub async fn switch_to_account( 21 | state: tauri::State<'_, state::State>, 22 | account_index: u8, 23 | ) -> Result<(), ()> { 24 | if state.switch_to(account_index).await { 25 | Ok(()) 26 | } else { 27 | Err(()) 28 | } 29 | } 30 | 31 | #[tauri::command] 32 | #[specta::specta] 33 | /// Removes an account 34 | /// 35 | /// # Returns 36 | /// returns Ok(()) if the account was removed successfully, otherwise returns Err(()) 37 | pub async fn remove_account( 38 | state: tauri::State<'_, state::State>, 39 | account_index: u8, 40 | ) -> Result<(), ()> { 41 | state.remove_account(account_index).await; 42 | 43 | Ok(()) 44 | } 45 | 46 | #[tauri::command] 47 | #[specta::specta] 48 | /// Add a new account 49 | /// 50 | /// # Returns 51 | /// returns the index of the account that was added 52 | pub async fn add_account( 53 | state: tauri::State<'_, state::State>, 54 | ) -> Result> { 55 | // setup the server 56 | let state::State { oauth, port, .. } = state.inner(); 57 | 58 | // TODO: tell frontend that the user needs to go to the redirect uri and complete the auth process 59 | // for now, we'll just auto open it 60 | 61 | let (send_token, mut recv_token) = async_runtime::channel(1); 62 | let authentication_info = oauth.create_auth_info(); 63 | 64 | let router = Router::new() 65 | .route("/code", routing::get(server::get_code)) 66 | .with_state(server::AuthServerState { 67 | oauth: oauth.clone(), 68 | csrf_token: authentication_info.csrf_token, 69 | pkce_verifier: Arc::new(authentication_info.pkce_verifier), 70 | send_token, 71 | }); 72 | 73 | let listener = TcpListener::bind(("127.0.0.1", *port)).await.unwrap(); 74 | let server_task = 75 | async_runtime::spawn(axum::serve(listener, router.into_make_service()).into_future()); 76 | 77 | open::that(authentication_info.url.as_str()).expect("Failed to open browser"); 78 | 79 | let token = recv_token.recv().await.unwrap()?; 80 | 81 | // shutdown the server 82 | server_task.abort(); 83 | 84 | let profile = oauth 85 | .get_minecraft_profile(&token) 86 | .await 87 | .change_context(server::AuthError::MinecraftProfile) 88 | .map_err(Into::>::into)?; 89 | 90 | let idx = state.add_account(token, profile).await; 91 | 92 | Ok(idx) 93 | } 94 | -------------------------------------------------------------------------------- /libs/copper/src/merger.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::api::client::{Arguments, AssetIndex, Downloads, JavaVersion, Library, Logging, Type}; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 6 | #[serde(rename_all = "camelCase", default)] 7 | pub struct MergableManifest { 8 | asset_index: Option, 9 | assets: Option, 10 | downloads: Option, 11 | id: Option, 12 | /// Default to be empty 13 | libraries: Vec, 14 | main_class: Option, 15 | /// Used before 1.13. Already optional. 16 | minecraft_arguments: Option, 17 | /// Already optional. When merging, this is preferred over `minecraft_arguments` 18 | arguments: Option, 19 | minimum_launcher_version: Option, 20 | /// Already optional 21 | java_version: Option, 22 | release_time: Option, 23 | time: Option, 24 | #[serde(rename = "type")] 25 | manifest_type: Option, 26 | /// Already optional 27 | compliance_level: Option, 28 | /// Already optional 29 | logging: Option, 30 | /// Already optional. 31 | inherits_from: Option, 32 | } 33 | 34 | impl MergableManifest { 35 | pub fn merge_with(&mut self, other: Self) { 36 | // arguments (vector merging) 37 | if let Some(arguments) = other.arguments { 38 | let mut current_arguments = self.arguments.take().unwrap_or_default(); 39 | 40 | current_arguments.jvm.extend(arguments.jvm); 41 | current_arguments.game.extend(arguments.game); 42 | 43 | self.arguments = Some(current_arguments); 44 | self.minecraft_arguments = None; 45 | } 46 | 47 | // inheriting (overriding reverse) 48 | self.inherits_from = other.inherits_from.or_else(|| self.inherits_from.take()); 49 | 50 | // asset index (overriding) 51 | self.asset_index = self.asset_index.take().or(other.asset_index); 52 | 53 | // compliance (overriding) 54 | self.compliance_level = self.compliance_level.take().or(other.compliance_level); 55 | 56 | // download (overriding) 57 | self.downloads = self.downloads.take().or(other.downloads); 58 | 59 | // id (overriding) 60 | self.id = self.id.take().or(other.id); 61 | 62 | // java version (overriding) 63 | self.java_version = self.java_version.take().or(other.java_version); 64 | 65 | // library (combining) 66 | self.libraries.extend(other.libraries); 67 | 68 | // main class (overriding) 69 | self.main_class = self.main_class.take().or(other.main_class); 70 | 71 | // minimum launcher version (overriding) 72 | self.minimum_launcher_version = self 73 | .minimum_launcher_version 74 | .take() 75 | .or(other.minimum_launcher_version); 76 | 77 | // release time (overriding) 78 | self.release_time = self.release_time.take().or(other.release_time); 79 | 80 | // time (overriding) 81 | self.time = self.time.take().or(other.time); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /libs/copper/examples/asset_downloader.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use copper::{ 4 | api::version, 5 | client::{self, assets}, 6 | downloader::{DownloadMessage, Downloader}, 7 | }; 8 | use error_stack::Report; 9 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 10 | use tracing::info; 11 | use tracing_subscriber::{fmt::format::PrettyFields, prelude::*}; 12 | 13 | extern crate copper; 14 | extern crate error_stack; 15 | extern crate indicatif; 16 | extern crate tokio; 17 | extern crate tracing; 18 | extern crate tracing_subscriber; 19 | 20 | #[tokio::main] 21 | async fn main() { 22 | // some setup for logging 23 | Report::set_color_mode(error_stack::fmt::ColorMode::Color); 24 | 25 | let error_handler = tracing_error::ErrorLayer::new(PrettyFields::new()); 26 | 27 | tracing_subscriber::fmt() 28 | .pretty() 29 | .with_env_filter( 30 | tracing_subscriber::EnvFilter::builder() 31 | .with_default_directive(tracing::Level::INFO.into()) 32 | .from_env_lossy(), 33 | ) 34 | .finish() 35 | .with(error_handler) 36 | .init(); 37 | 38 | info!("Downloading minecraft manifest"); 39 | let manifest = version::Manifest::get().await.unwrap(); 40 | 41 | info!("Downloaded minecraft manifest"); 42 | 43 | let latest = manifest.latest_release(); 44 | info!("Downloading manifest for latest release: {}", latest.id()); 45 | 46 | let manifest: client::manifest::Manifest = latest.download().await.unwrap().try_into().unwrap(); 47 | info!("Downloaded manifest for latest release: {}", latest.id()); 48 | info!("Downloading asset index"); 49 | 50 | let cwd = std::env::current_dir().unwrap(); 51 | let assets = cwd.join(".minecraft").join("assets"); 52 | info!("Assets directory: {}", assets.display()); 53 | 54 | let index = manifest.asset_index().download().await.unwrap(); 55 | 56 | let mut download_index = 57 | assets::Downloader::new(index.into(), assets, reqwest::Client::new(), 16); 58 | let mut reciever = download_index.create_channel(); 59 | 60 | info!("Downloaded asset index"); 61 | info!("Downloading assets"); 62 | 63 | let downloader = Arc::new(download_index); 64 | 65 | let mut bars = HashMap::new(); 66 | let m = MultiProgress::new(); 67 | let sty = ProgressStyle::with_template( 68 | "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})" 69 | ) 70 | .unwrap() 71 | .progress_chars("##-"); 72 | 73 | let task_downloader = downloader.clone(); 74 | let task = tokio::task::spawn(async move { 75 | task_downloader.download_all().await.unwrap(); 76 | }); 77 | 78 | info!("Watching for messages"); 79 | while let Some(dl) = reciever.recv().await { 80 | match dl { 81 | DownloadMessage::Downloaded(object) => { 82 | let hash = object.hash().to_owned(); 83 | let Some((_, bar)): Option<(String, ProgressBar)> = bars.remove(&hash) else { 84 | continue; 85 | }; 86 | 87 | bar.finish(); 88 | m.remove(&bar); 89 | } 90 | DownloadMessage::DownloadedAll => { 91 | m.println("Downloaded all objects. Joining tasks").unwrap(); 92 | // we're done, make sure to break 93 | break; 94 | } 95 | DownloadMessage::DownloadProgress(object, how_much) => { 96 | let hash = object.hash().to_owned(); 97 | let size = object.size(); 98 | let bar = bars.entry(hash.clone()).or_insert_with(|| { 99 | let bar = m.add(indicatif::ProgressBar::new(size)); 100 | bar.set_style(sty.clone()); 101 | bar.set_message(hash.clone()); 102 | 103 | (hash.clone(), bar) 104 | }); 105 | 106 | bar.1.set_position(how_much); 107 | } 108 | } 109 | } 110 | 111 | task.await.unwrap(); 112 | 113 | info!("Done"); 114 | } 115 | -------------------------------------------------------------------------------- /libs/ui/src/theme/fonts/recursive.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'RecVar', sans-serif; 3 | } 4 | 5 | /* The bare minimum English subset, plus copyright & arrows (← ↑ → ↓) & quotes (“ ” ‘ ’) & bullet (•) */ 6 | @font-face { 7 | font-family: 'RecVar'; 8 | font-style: oblique 0deg 15deg; 9 | font-weight: 300 1000; 10 | font-display: swap; 11 | src: url('./recursive/Recursive_VF_1.085--subset_range_english_basic.woff2') format('woff2'); 12 | unicode-range: U+0020-007F,U+00A9,U+2190-2193,U+2018,U+2019,U+201C,U+201D,U+2022; 13 | } 14 | 15 | /* unicode latin-1 letters, basic european diacritics */ 16 | @font-face { 17 | font-family: 'RecVar'; 18 | font-style: oblique 0deg 15deg; 19 | font-weight: 300 1000; 20 | font-display: swap; 21 | src: url('./recursive/Recursive_VF_1.085--subset_range_latin_1.woff2') format('woff2'); 22 | unicode-range: U+00C0-00FF; 23 | } 24 | 25 | /* unicode latin-1, punc/symbols & arrows (↔ ↕ ↖ ↗ ↘ ↙) */ 26 | @font-face { 27 | font-family: 'RecVar'; 28 | font-style: oblique 0deg 15deg; 29 | font-weight: 300 1000; 30 | font-display: swap; 31 | src: url('./recursive/Recursive_VF_1.085--subset_range_latin_1_punc.woff2') format('woff2'); 32 | unicode-range: U+00A0-00A8,U+00AA-00BF,U+2194-2199; 33 | } 34 | 35 | /* unicode latin A extended */ 36 | @font-face { 37 | font-family: 'RecVar'; 38 | font-style: oblique 0deg 15deg; 39 | font-weight: 300 1000; 40 | font-display: swap; 41 | src: url('./recursive/Recursive_VF_1.085--subset_range_latin_ext.woff2') format('woff2'); 42 | unicode-range: U+0100-017F; 43 | } 44 | 45 | /* unicodes for vietnamese */ 46 | @font-face { 47 | font-family: 'RecVar'; 48 | font-style: oblique 0deg 15deg; 49 | font-weight: 300 1000; 50 | font-display: swap; 51 | src: url('./recursive/Recursive_VF_1.085--subset_range_vietnamese.woff2') format('woff2'); 52 | unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB; 53 | } 54 | 55 | /* remaining Unicodes */ 56 | @font-face { 57 | font-family: 'RecVar'; 58 | font-style: oblique 0deg 15deg; 59 | font-weight: 300 1000; 60 | font-display: swap; 61 | src: url('./recursive/Recursive_VF_1.085--subset_range_remaining.woff2') format('woff2'); 62 | unicode-range: U+2007,U+2008,U+2009,U+200A,U+200B,U+D,U+2010,U+2012,U+2013,U+2014,U+2015,U+201A,U+201E,U+2020,U+2021,U+2026,U+2030,U+2032,U+2033,U+2039,U+203A,U+203E,U+2044,U+2052,U+2070,U+2074,U+2075,U+2076,U+2077,U+2078,U+2079,U+207B,U+2080,U+2081,U+2082,U+2083,U+2084,U+2085,U+2086,U+2087,U+2088,U+2089,U+20A1,U+20A6,U+20A8,U+20A9,U+20AA,U+20AC,U+20AD,U+20B1,U+20B2,U+20B4,U+20B5,U+20B8,U+20B9,U+20BA,U+20BC,U+20BD,U+20BF,U+F8FF,U+2113,U+2116,U+2122,U+2126,U+212E,U+E132,U+E133,U+2153,U+2154,U+215B,U+215C,U+215D,U+215E,U+18F,U+192,U+19D,U+1C4,U+1C5,U+1C6,U+1C7,U+1C8,U+1C9,U+1CA,U+1CB,U+1CC,U+1E6,U+1E7,U+1EA,U+1EB,U+1F1,U+1F2,U+1F3,U+1FA,U+1FB,U+1FC,U+1FD,U+1FE,U+1FF,U+200,U+201,U+202,U+203,U+204,U+205,U+206,U+207,U+208,U+209,U+20A,U+20B,U+20C,U+20D,U+20E,U+20F,U+210,U+211,U+212,U+213,U+214,U+215,U+216,U+217,U+218,U+219,U+21A,U+21B,U+2215,U+2219,U+221E,U+221A,U+22A,U+22B,U+22C,U+22D,U+222B,U+230,U+231,U+232,U+233,U+2236,U+237,U+2248,U+259,U+2260,U+2261,U+2264,U+2265,U+272,U+2B9,U+2BA,U+2BB,U+2BC,U+2BE,U+2BF,U+2C6,U+2C7,U+2C8,U+2C9,U+2CA,U+2CB,U+2D8,U+2D9,U+2DA,U+2DB,U+2DC,U+2DD,U+300,U+301,U+FB02,U+FB03,U+302,U+303,U+304,U+FB01,U+306,U+307,U+308,U+309,U+30A,U+30B,U+30C,U+30F,U+311,U+312,U+315,U+31B,U+2202,U+323,U+324,U+325,U+326,U+327,U+328,U+329,U+2205,U+32E,U+2206,U+331,U+335,U+220F,U+2211,U+2212,U+391,U+392,U+393,U+394,U+398,U+39B,U+39C,U+39D,U+3A0,U+3A6,U+3B1,U+3B2,U+3B3,U+3B4,U+3B8,U+3BB,U+3BC,U+3BD,U+3C0,U+3C6,U+25A0,U+25A1,U+25B2,U+25B3,U+25B6,U+25B7,U+25BC,U+25BD,U+25C0,U+25C1,U+25C6,U+25C7,U+25CA,U+1E08,U+1E09,U+1E0C,U+1E0D,U+1E0E,U+1E0F,U+2610,U+2611,U+1E14,U+1E15,U+1E16,U+1E17,U+1E1C,U+1E1D,U+1E20,U+1E21,U+1E24,U+1E25,U+1E2A,U+1E2B,U+1E2E,U+1E2F,U+1E36,U+1E37,U+1E3A,U+1E3B,U+E3F,U+1E42,U+1E43,U+1E44,U+1E45,U+1E46,U+1E47,U+1E48,U+1E49,U+1E4C,U+1E4D,U+1E4E,U+1E4F,U+1E50,U+1E51,U+1E52,U+1E53,U+1E5A,U+1E5B,U+1E5E,U+1E5F,U+1E60,U+2661,U+1E61,U+1E62,U+1E63,U+1E64,U+1E65,U+1E66,U+1E67,U+1E68,U+1E69,U+2665,U+1E6C,U+1E6D,U+1E6E,U+1E6F,U+1E78,U+1E79,U+1E7A,U+1E7B,U+1E80,U+1E81,U+1E82,U+1E83,U+1E84,U+1E85,U+1E8E,U+1E8F,U+1E92,U+1E93,U+1E97,U+1E9E,U+2713,U+27E8,U+27E9; 63 | } 64 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{Deref, DerefMut}, 3 | sync::Arc, 4 | thread::panicking, 5 | }; 6 | 7 | use serde::Serialize; 8 | use specta::Type; 9 | use tauri::{async_runtime::Mutex, AppHandle, Manager}; 10 | use tokio::sync::MutexGuard; 11 | 12 | const STATE_CHANGE_EVENT: &str = "plugin:state-change"; 13 | 14 | /// A tauri <-> frontend state bridge 15 | /// 16 | /// This is to only readable by the frontend, and is only writable by tauri 17 | /// See the `StateGuard` for a way to write to the state from tauri. 18 | #[derive(Debug, Clone)] 19 | pub struct State { 20 | key: String, 21 | inner: Arc>, 22 | tauri_handle: AppHandle, 23 | } 24 | 25 | impl State { 26 | /// Creates a new state object and syncs the initial value to the frontend 27 | /// 28 | /// # Arguments 29 | /// - `key` - The key to use for the state 30 | /// - `value` - The initial value of the state 31 | /// - `handle` - The tauri handle to use for sending updates to the frontend 32 | /// 33 | /// See usage from [`crate::init`] 34 | pub fn new(key: &str, value: T, handle: AppHandle) -> Self { 35 | if let Err(e) = handle.emit( 36 | STATE_CHANGE_EVENT, 37 | StateUpdate { 38 | key: key.to_owned(), 39 | value: value.clone(), 40 | }, 41 | ) { 42 | panic!("Failed to send state update to frontend: {}", e); 43 | } 44 | 45 | Self { 46 | key: key.to_string(), 47 | inner: Arc::new(Mutex::new(value)), 48 | tauri_handle: handle, 49 | } 50 | } 51 | 52 | /// Force syncs the current state to the frontend 53 | /// 54 | /// This is useful if you absolutely want to make sure the frontend has the latest state 55 | /// 56 | /// # Locking 57 | /// This will lock the state, so make sure to not call this while holding a lock on the state 58 | pub async fn sync(&self) { 59 | if let Err(e) = self.tauri_handle.emit( 60 | STATE_CHANGE_EVENT, 61 | StateUpdate { 62 | key: self.key.clone(), 63 | value: self.inner.lock().await.clone(), 64 | }, 65 | ) { 66 | if panicking() { 67 | eprintln!("Failed to send state update to frontend: {}", e); 68 | } else { 69 | panic!("Failed to send state update to frontend: {}", e); 70 | } 71 | } 72 | } 73 | 74 | /// Gets a lock on the state 75 | /// 76 | /// This returns a [`StateGuard`] that will unlock the state when dropped and will send any changes to the frontend 77 | /// 78 | /// # Locking 79 | /// This, as the name implies, locks the state, so make sure to not call this while holding a lock on the state 80 | pub async fn lock(&self) -> StateGuard { 81 | StateGuard { 82 | inner: self.inner.lock().await, 83 | key: self.key.clone(), 84 | app_handle: self.tauri_handle.clone(), 85 | } 86 | } 87 | } 88 | 89 | /// A guard for a state object 90 | /// 91 | /// This will send any changes to the frontend when dropped 92 | /// 93 | /// See [`State::lock`] for more information 94 | pub struct StateGuard<'a, T: Serialize + Type + Clone> { 95 | inner: MutexGuard<'a, T>, 96 | key: String, 97 | app_handle: AppHandle, 98 | } 99 | 100 | #[derive(Serialize, Clone, Type)] 101 | struct StateUpdate { 102 | key: String, 103 | value: T, 104 | } 105 | 106 | impl Drop for StateGuard<'_, T> { 107 | fn drop(&mut self) { 108 | if let Err(e) = self.app_handle.emit( 109 | STATE_CHANGE_EVENT, 110 | StateUpdate { 111 | key: self.key.clone(), 112 | value: self.inner.clone(), 113 | }, 114 | ) { 115 | if panicking() { 116 | eprintln!("Failed to send state update to frontend: {}", e); 117 | } else { 118 | panic!("Failed to send state update to frontend: {}", e); 119 | } 120 | } 121 | } 122 | } 123 | 124 | impl<'a, T: Serialize + Type + Clone> Deref for StateGuard<'a, T> { 125 | type Target = T; 126 | 127 | fn deref(&self) -> &Self::Target { 128 | &self.inner 129 | } 130 | } 131 | 132 | impl<'a, T: Serialize + Type + Clone> DerefMut for StateGuard<'a, T> { 133 | fn deref_mut(&mut self) -> &mut Self::Target { 134 | &mut self.inner 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/routes/accountDropdown.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 | {#snippet account(account: string)} 53 | {account}'s avatar 59 | 60 |

{account}

61 | {/snippet} 62 | 63 | 76 | 77 | {#if $open} 78 |
    82 | {#each profiles as profile, idx} 83 |
  • 84 | 87 |
  • 88 | {/each} 89 | 90 |
    91 | 92 |
  • 93 | 98 |
  • 99 | 100 |
  • 101 | 106 |
  • 107 |
108 | {/if} 109 | 110 | 170 | -------------------------------------------------------------------------------- /apps/glowsquid-frontend/src/lib/bindings.ts: -------------------------------------------------------------------------------- 1 | 2 | // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. 3 | 4 | /** user-defined commands **/ 5 | 6 | export const commands = { 7 | async greet(name: string) : Promise { 8 | return await TAURI_INVOKE("greet", { name }); 9 | }, 10 | /** 11 | * Add a new account 12 | * 13 | * # Returns 14 | * returns the index of the account that was added 15 | */ 16 | async addAccount() : Promise>> { 17 | try { 18 | return { status: "ok", data: await TAURI_INVOKE("add_account") }; 19 | } catch (e) { 20 | if(e instanceof Error) throw e; 21 | else return { status: "error", error: e as any }; 22 | } 23 | }, 24 | /** 25 | * Removes an account 26 | * 27 | * # Returns 28 | * returns Ok(()) if the account was removed successfully, otherwise returns Err(()) 29 | */ 30 | async removeAccount(accountIndex: number) : Promise> { 31 | try { 32 | return { status: "ok", data: await TAURI_INVOKE("remove_account", { accountIndex }) }; 33 | } catch (e) { 34 | if(e instanceof Error) throw e; 35 | else return { status: "error", error: e as any }; 36 | } 37 | }, 38 | /** 39 | * Switch to a different account 40 | * 41 | * # Returns 42 | * returns Ok(()) if the account was switched successfully, otherwise returns Err(()) 43 | */ 44 | async switchToAccount(accountIndex: number) : Promise> { 45 | try { 46 | return { status: "ok", data: await TAURI_INVOKE("switch_to_account", { accountIndex }) }; 47 | } catch (e) { 48 | if(e instanceof Error) throw e; 49 | else return { status: "error", error: e as any }; 50 | } 51 | } 52 | } 53 | 54 | /** user-defined events **/ 55 | 56 | 57 | 58 | /** user-defined statics **/ 59 | 60 | 61 | 62 | /** user-defined types **/ 63 | 64 | export type AuthError = "MsToken" | "MinecraftToken" | "MinecraftProfile" 65 | /** 66 | * State of currently authenticated profiles 67 | */ 68 | export type AuthState = { 69 | /** 70 | * All the profiles that the user has added 71 | */ 72 | profiles: MinecraftProfile[]; 73 | /** 74 | * All currently encrypted profiles usernames 75 | */ 76 | encryptedProfiles: string[]; 77 | /** 78 | * Index of the currently selected profile 79 | */ 80 | currentProfileIndex: number | null } 81 | export type Cape = { id: string; state: UsageState; url: string; alias: string } 82 | /** 83 | * A custom error type that wraps around a error-stack error, converting it from Report to Error 84 | * 85 | * Also known as the most unholy error type known to crabkind 86 | */ 87 | export type Error = { error: T; report: any } 88 | export type MinecraftProfile = { id: string; name: string; skins: Skin[]; capes: Cape[] } 89 | export type Skin = { id: string; state: UsageState; url: string; variant: SkinVariant } 90 | export type SkinVariant = "CLASSIC" | "SLIM" 91 | export type UsageState = "ACTIVE" | "INACTIVE" 92 | 93 | /** tauri-specta globals **/ 94 | 95 | import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core"; 96 | import * as TAURI_API_EVENT from "@tauri-apps/api/event"; 97 | import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; 98 | 99 | type __EventObj__ = { 100 | listen: ( 101 | cb: TAURI_API_EVENT.EventCallback 102 | ) => ReturnType>; 103 | once: ( 104 | cb: TAURI_API_EVENT.EventCallback 105 | ) => ReturnType>; 106 | emit: T extends null 107 | ? (payload?: T) => ReturnType 108 | : (payload: T) => ReturnType; 109 | }; 110 | 111 | export type Result = 112 | | { status: "ok"; data: T } 113 | | { status: "error"; error: E }; 114 | 115 | function __makeEvents__>( 116 | mappings: Record 117 | ) { 118 | return new Proxy( 119 | {} as unknown as { 120 | [K in keyof T]: __EventObj__ & { 121 | (handle: __WebviewWindow__): __EventObj__; 122 | }; 123 | }, 124 | { 125 | get: (_, event) => { 126 | const name = mappings[event as keyof T]; 127 | 128 | return new Proxy((() => {}) as any, { 129 | apply: (_, __, [window]: [__WebviewWindow__]) => ({ 130 | listen: (arg: any) => window.listen(name, arg), 131 | once: (arg: any) => window.once(name, arg), 132 | emit: (arg: any) => window.emit(name, arg), 133 | }), 134 | get: (_, command: keyof __EventObj__) => { 135 | switch (command) { 136 | case "listen": 137 | return (arg: any) => TAURI_API_EVENT.listen(name, arg); 138 | case "once": 139 | return (arg: any) => TAURI_API_EVENT.once(name, arg); 140 | case "emit": 141 | return (arg: any) => TAURI_API_EVENT.emit(name, arg); 142 | } 143 | }, 144 | }); 145 | }, 146 | } 147 | ); 148 | } 149 | 150 | -------------------------------------------------------------------------------- /libs/copper/src/client/manifest.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use error_stack::{report, Report, ResultExt}; 4 | 5 | use crate::api::{ 6 | self, 7 | client::{ 8 | Arguments, AssetIndex, Downloads, GameArgument, JavaVersion, JvmArgument, Logging, Type, 9 | }, 10 | }; 11 | 12 | use super::library::Library; 13 | 14 | /// A launcher manifest with a standardized set of options and API 15 | #[derive(Debug, Clone)] 16 | pub struct Manifest { 17 | /// The asset index to download 18 | asset_index: AssetIndex, 19 | /// The name of the asset index. Useful for saving the asset index to a file 20 | assets: String, 21 | /// The versions of the game that are available to download 22 | downloads: Downloads, 23 | /// The id of the version 24 | id: String, 25 | /// The libraries that are required to run the game 26 | libraries: Vec, 27 | /// The main class to run 28 | /// 29 | /// This is the actual class name. 30 | /// Some were obfuscated by Mojang which requires special handling during creating the [`LauncherManifest`] 31 | main_class: String, 32 | /// The arguments to launch the game with 33 | arguments: Arguments, 34 | /// The recommended (and minimum) java version required to run the game 35 | java_version: JavaVersion, 36 | /// The type of version 37 | manifest_type: Type, 38 | /// The logging information for the game 39 | logging: Logging, 40 | } 41 | 42 | impl Manifest { 43 | #[must_use] 44 | pub const fn logging(&self) -> &Logging { 45 | &self.logging 46 | } 47 | 48 | #[must_use] 49 | pub const fn manifest_type(&self) -> &Type { 50 | &self.manifest_type 51 | } 52 | 53 | #[must_use] 54 | pub const fn java_version(&self) -> &JavaVersion { 55 | &self.java_version 56 | } 57 | 58 | #[must_use] 59 | pub const fn arguments(&self) -> &Arguments { 60 | &self.arguments 61 | } 62 | 63 | #[must_use] 64 | pub fn libraries(&self) -> &[Library] { 65 | &self.libraries 66 | } 67 | 68 | #[must_use] 69 | pub fn main_class(&self) -> &str { 70 | &self.main_class 71 | } 72 | 73 | #[must_use] 74 | pub const fn downloads(&self) -> &Downloads { 75 | &self.downloads 76 | } 77 | 78 | pub fn add_jvm_argument(&mut self, arg: JvmArgument) { 79 | self.arguments.jvm.push(arg); 80 | } 81 | 82 | #[must_use] 83 | pub const fn asset_index(&self) -> &AssetIndex { 84 | &self.asset_index 85 | } 86 | 87 | #[must_use] 88 | pub fn assets(&self) -> &str { 89 | &self.assets 90 | } 91 | 92 | #[must_use] 93 | pub fn id(&self) -> &str { 94 | &self.id 95 | } 96 | } 97 | 98 | impl TryFrom for Manifest { 99 | type Error = Report; 100 | 101 | #[allow(clippy::manual_let_else)] 102 | fn try_from(value: api::client::Manifest) -> Result { 103 | let java_version = value.java_version.unwrap_or(JavaVersion { 104 | component: "java-runtime-delta".to_string(), 105 | major_version: 8, 106 | }); 107 | 108 | let libraries = value 109 | .libraries 110 | .into_iter() 111 | .map(|library| { 112 | Library::try_from(library).change_context(ConversionError::LibraryConversionError) 113 | }) 114 | .collect::, Report>>()?; 115 | 116 | // bug: this cannot be written as let ... else as we're returning something from both branches 117 | let arguments = match value.arguments { 118 | Some(args) => args, 119 | None => Arguments { 120 | game: value 121 | .minecraft_arguments 122 | .ok_or(report!(ConversionError::MissingArguments))? 123 | .split_whitespace() 124 | .map(ToString::to_string) 125 | .map(GameArgument::String) 126 | .collect(), 127 | jvm: todo!( 128 | "Figure out best default jvm arguments for the game for pre 1.13 versions" 129 | ), 130 | }, 131 | }; 132 | 133 | Ok(Self { 134 | asset_index: value.asset_index, 135 | assets: value.assets, 136 | downloads: value.downloads, 137 | id: value.id, 138 | libraries, 139 | main_class: value.main_class, 140 | arguments, 141 | java_version, 142 | manifest_type: value.manifest_type, 143 | logging: value 144 | .logging 145 | .unwrap_or_else(|| todo!("Create default logging")), 146 | }) 147 | } 148 | } 149 | 150 | #[derive(Debug)] 151 | pub enum ConversionError { 152 | LibraryConversionError, 153 | MissingArguments, 154 | } 155 | 156 | impl fmt::Display for ConversionError { 157 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 158 | match self { 159 | Self::LibraryConversionError => f.write_str("Failed to convert a library to LauncherLibrary"), 160 | Self::MissingArguments => f.write_str("Missing arguments field in the manifest. There should either be the modern 1.13 arguments struct or the legacy <1.13 minecraft arguments"), 161 | } 162 | } 163 | } 164 | 165 | impl Error for ConversionError {} 166 | -------------------------------------------------------------------------------- /libs/copper/src/api/version.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::{ 3 | error::Error, 4 | fmt::{Display, Formatter}, 5 | }; 6 | 7 | use error_stack::ResultExt; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use super::client; 11 | 12 | const VERSION_MANIFEST_URL: &str = 13 | "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; 14 | 15 | #[derive(Serialize, Deserialize, Debug)] 16 | pub struct Manifest { 17 | latest: Latest, 18 | versions: Vec, 19 | } 20 | 21 | impl Manifest { 22 | /// Fetches the version manifest from Mojang's servers. 23 | /// 24 | /// # Errors 25 | /// Errors if the request fails or if the response is not a valid [`VersionManifest`]. 26 | pub async fn get() -> Result { 27 | reqwest::get(VERSION_MANIFEST_URL).await?.json().await 28 | } 29 | 30 | /// Returns the latest release version. 31 | /// 32 | /// # Panics 33 | /// Panics if the latest snapshot version is not in the manifest. This should never happen. 34 | #[must_use] 35 | pub fn latest_release(&self) -> &Version { 36 | self.versions 37 | .iter() 38 | .find(|v| v.id == self.latest.release) 39 | .expect("Latest version to be in manifest") 40 | } 41 | 42 | /// Returns the latest snapshot version. 43 | /// 44 | /// Note that this may be the same as the latest release version. 45 | /// 46 | /// # Panics 47 | /// Panics if the latest snapshot version is not in the manifest. This should never happen. 48 | #[must_use] 49 | pub fn latest_snapshot(&self) -> &Version { 50 | self.versions 51 | .iter() 52 | .find(|v| v.id == self.latest.snapshot) 53 | .expect("Latest version to be in manifest") 54 | } 55 | } 56 | 57 | #[derive(Serialize, Deserialize, Debug)] 58 | pub struct Latest { 59 | release: String, 60 | snapshot: String, 61 | } 62 | 63 | #[derive(Serialize, Deserialize, Debug)] 64 | #[serde(rename_all = "camelCase")] 65 | pub struct Version { 66 | id: String, 67 | #[serde(rename = "type")] 68 | version_type: Type, 69 | url: String, 70 | time: String, 71 | release_time: String, 72 | sha1: String, 73 | compliance_level: i64, 74 | } 75 | 76 | #[derive(Debug)] 77 | pub enum GetError { 78 | Request, 79 | CannotParse, 80 | } 81 | 82 | impl Display for GetError { 83 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 84 | match self { 85 | Self::Request => write!(f, "Could not get version manifest"), 86 | Self::CannotParse => write!( 87 | f, 88 | "Could not parse version manifest. Please report this as a bug." 89 | ), 90 | } 91 | } 92 | } 93 | 94 | impl Error for GetError {} 95 | 96 | impl Version { 97 | /// Tries to download and parse the version manifest. 98 | /// 99 | /// # Errors 100 | /// Errors if the request fails or if the response is not a valid [`client::Manifest`]. 101 | pub async fn download(&self) -> error_stack::Result { 102 | reqwest::get(&self.url) 103 | .await 104 | .change_context(GetError::Request)? 105 | .json::() 106 | .await 107 | .change_context(GetError::CannotParse) 108 | } 109 | } 110 | 111 | impl Version { 112 | #[must_use] 113 | pub fn id(&self) -> &str { 114 | &self.id 115 | } 116 | } 117 | 118 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] 119 | #[serde(rename_all = "snake_case")] 120 | pub enum Type { 121 | #[serde(rename = "old_alpha")] 122 | OldAlpha, 123 | #[serde(rename = "old_beta")] 124 | OldBeta, 125 | Release, 126 | Snapshot, 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use super::*; 132 | use test_case::test_case; 133 | 134 | #[tokio::test] 135 | async fn test_getting_latest_release() { 136 | let manifest = Manifest::get().await.unwrap(); 137 | 138 | assert_eq!(manifest.latest_release().version_type, Type::Release); 139 | // latest snapshot may be the latest release 140 | assert!( 141 | manifest.latest_snapshot().version_type == Type::Snapshot 142 | || manifest.latest_snapshot().version_type == Type::Release 143 | ); 144 | } 145 | 146 | #[tokio::test] 147 | async fn parse_latest() { 148 | let manifest = Manifest::get().await.unwrap(); 149 | 150 | manifest.latest_release().download().await.unwrap(); 151 | } 152 | 153 | #[test_case("13w38a"; "Version 1")] 154 | #[test_case("13w39a"; "Version 2")] 155 | #[test_case("19w35a"; "Version 3")] 156 | #[test_case("20w20a"; "Version 4")] 157 | #[test_case("20w21a"; "Version 5")] 158 | #[test_case("20w45a"; "Version 6")] 159 | // some standards 160 | #[test_case("1.20"; "1.20")] 161 | #[test_case("1.18"; "1.18")] 162 | #[test_case("1.13"; "1.13")] 163 | #[test_case("1.12"; "1.12")] 164 | #[test_case("1.15"; "1.15")] 165 | #[test_case("1.7.10"; "1.7.10")] 166 | #[test_case("1.8.9"; "1.8.9")] 167 | #[tokio::test] 168 | async fn parse(id: &str) { 169 | let manifest = Manifest::get().await.unwrap(); 170 | 171 | manifest 172 | .versions 173 | .iter() 174 | .find(|v| v.id == id) 175 | .unwrap() 176 | .download() 177 | .await 178 | .unwrap(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /libs/copper/examples/auth.rs: -------------------------------------------------------------------------------- 1 | use std::{future::IntoFuture, net::SocketAddr, sync::OnceLock}; 2 | 3 | use axum::{ 4 | extract::{Query, State}, 5 | routing::get, 6 | Router, 7 | }; 8 | use copper::client::auth::{AuthenticationInfo, MicrosoftAuthenticator, MinecraftToken, OauthCode}; 9 | use error_stack::Report; 10 | use oauth2::{CsrfToken, PkceCodeVerifier}; 11 | use tokio::{net::TcpListener, task}; 12 | use tracing::info; 13 | use tracing_subscriber::{fmt::format::PrettyFields, prelude::*}; 14 | 15 | extern crate axum; 16 | extern crate copper; 17 | extern crate error_stack; 18 | extern crate tokio; 19 | extern crate tracing; 20 | extern crate tracing_subscriber; 21 | 22 | // Glowsquids details. If you are going to make your own app, please do not use these. 23 | // these are just for ease of use. 24 | const CLIENT_ID: &str = "2aa32806-92e3-4242-babc-392ac0f0fd30"; 25 | const CLIENT_SECRET: &str = "nky8Q~8lORwTC0OjdxVsgZSs0hCdTcEdec3hNbaP"; 26 | 27 | static CSRF_TOKEN: OnceLock = OnceLock::new(); 28 | static PKCE_VERIFIER: OnceLock = OnceLock::new(); 29 | 30 | #[derive(Clone)] 31 | struct AppState { 32 | oauth: MicrosoftAuthenticator, 33 | } 34 | 35 | #[tokio::main] 36 | async fn main() { 37 | // some setup for logging 38 | Report::set_color_mode(error_stack::fmt::ColorMode::Color); 39 | 40 | let error_handler = tracing_error::ErrorLayer::new(PrettyFields::new()); 41 | 42 | tracing_subscriber::fmt() 43 | .pretty() 44 | .with_env_filter( 45 | tracing_subscriber::EnvFilter::builder() 46 | .with_default_directive(tracing::Level::INFO.into()) 47 | .from_env_lossy(), 48 | ) 49 | .finish() 50 | .with(error_handler) 51 | .init(); 52 | 53 | info!("Initializing oauth2 client..."); 54 | 55 | // now lets start! 56 | // we need a redirect uri so that the oauth server can redirect the user back to our server 57 | let redirect_uri = "http://localhost:3000/code".to_string(); 58 | 59 | // This creates the actual authenticator. 60 | // This is the main struct that will be used to authenticate with Microsoft and Mojang APIs 61 | let oauth = MicrosoftAuthenticator::new( 62 | reqwest::Client::new(), 63 | redirect_uri, 64 | CLIENT_ID.to_string(), 65 | CLIENT_SECRET.to_string(), 66 | ) 67 | .expect("To be able to create client"); 68 | 69 | let authentication_info = oauth.create_auth_info(); 70 | 71 | info!("Initializing server..."); 72 | 73 | // some server setup. You will probably need to set it up using your preferred framework of 74 | // choice 75 | let router = Router::new() 76 | .route("/code", get(get_code)) 77 | .with_state(AppState { 78 | oauth: oauth.clone(), 79 | }); 80 | 81 | let socket_addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 82 | let listener = TcpListener::bind(&socket_addr).await.unwrap(); 83 | let server = task::spawn(axum::serve(listener, router.into_make_service()).into_future()); 84 | 85 | info!("Initializing initialized on port 3000!..."); 86 | 87 | // get the auth url and csrf token 88 | // the csrf token is to prevent Cross Site Request Forgery 89 | // read more about it at https://en.wikipedia.org/wiki/Cross-site_request_forgery 90 | // the tl;dr is that it is a security measure to prevent malicious websites from impersonating 91 | // us 92 | let AuthenticationInfo { 93 | url, 94 | csrf_token, 95 | pkce_verifier, 96 | } = authentication_info; 97 | 98 | // now that we have the verifier and token, we can set it so it can be accessed by the server 99 | // this is not the best way to do it, but it is the easiest 100 | PKCE_VERIFIER 101 | .set(pkce_verifier) 102 | .expect("To be able to set pkce verifier"); 103 | 104 | CSRF_TOKEN 105 | .set(csrf_token) 106 | .expect("To be able to set csrf token"); 107 | 108 | info!("Please go to this url to authenticate: {}", url); 109 | 110 | server.await.unwrap().unwrap(); 111 | } 112 | 113 | async fn get_code( 114 | Query(code): Query, 115 | State(state): State, 116 | ) -> axum::Json { 117 | info!("Received code: {:?}. Authenticating...", code); 118 | 119 | // Retrieve the csrf token and pkce verifier 120 | let csrf = CSRF_TOKEN 121 | .get() 122 | .expect("To be able to get CSRF token") 123 | .clone(); 124 | 125 | let pkce = PKCE_VERIFIER 126 | .get() 127 | .expect("To be able to get pkce verifier"); 128 | 129 | // get the microsoft access token 130 | // this is the token that will be used to get the minecraft token and refresh it if needed 131 | // 132 | // the csrf token is used to prevent Cross Site Request Forgery 133 | // and the pkce verifier is used to prevent man in the middle attacks by making sure there is a consistent auth code throughout the process 134 | let ms_token = state 135 | .oauth 136 | .get_ms_access_token(code, csrf, pkce) 137 | .await 138 | .expect("To be able to get token"); 139 | 140 | let mut token = state 141 | .oauth 142 | .get_minecraft_token(ms_token.clone()) 143 | .await 144 | .expect("To be able to get token"); 145 | 146 | info!("Authenticated! Token: {:?}", token); 147 | info!("testing refresh"); 148 | 149 | // This is how you'd refresh the ms code and token if you need to 150 | token.refresh(&state.oauth).await.unwrap(); 151 | info!("Sucessful Refresh! New token: {:?}", token); 152 | 153 | info!("You can now run ctrl+c to exit the program"); 154 | 155 | axum::Json(token) 156 | } 157 | -------------------------------------------------------------------------------- /libs/copper/src/client/assets.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::min, 3 | collections::HashMap, 4 | path::{Path, PathBuf}, 5 | sync::Arc, 6 | }; 7 | 8 | use error_stack::{report, Result, ResultExt}; 9 | use futures::{stream, StreamExt, TryStreamExt}; 10 | use sha1::{Digest, Sha1}; 11 | use tokio::{io::AsyncWriteExt, sync::mpsc}; 12 | 13 | use crate::{ 14 | api, 15 | downloader::{self, DownloadChannelSender, DownloadError, DownloadMessage}, 16 | }; 17 | 18 | #[derive(Debug, Clone)] 19 | /// An object in the asset index 20 | pub struct Object { 21 | /// The hash of the object 22 | hash: String, 23 | /// The size of the object in bytes 24 | size: u64, 25 | } 26 | 27 | /// Getter methods 28 | impl Object { 29 | #[must_use] 30 | ///T he hash of the object 31 | pub fn hash(&self) -> &str { 32 | &self.hash 33 | } 34 | 35 | #[must_use] 36 | /// The first two characters of the hash 37 | pub fn hash_start(&self) -> &str { 38 | &self.hash[..2] 39 | } 40 | 41 | #[must_use] 42 | /// The size of the object in bytes 43 | pub const fn size(&self) -> u64 { 44 | self.size 45 | } 46 | 47 | /// Downloads the object to the specified assets directory, 48 | /// appending the required path to the assets directory (`objects/{object.hash_start}/{object.hash}`) 49 | /// 50 | /// It will redownload the file if it already exists and the hash does not match 51 | /// 52 | /// # Errors 53 | /// Errors if the download fails or if the file cannot be created 54 | async fn download( 55 | &self, 56 | assets_path: &Path, 57 | client: &reqwest::Client, 58 | sender: &DownloadChannelSender, 59 | ) -> Result<(), DownloadError> { 60 | let path = assets_path 61 | .join("objects") 62 | .join(self.hash_start()) 63 | .join(&self.hash); 64 | 65 | if path.try_exists().change_context(DownloadError::IoError)? { 66 | let mut file = std::fs::File::open(&path).change_context(DownloadError::IoError)?; 67 | let mut hasher = Sha1::new(); 68 | std::io::copy(&mut file, &mut hasher).change_context(DownloadError::IoError)?; 69 | 70 | let file_hash = hasher.finalize(); 71 | 72 | if self.hash == hex::encode(file_hash) { 73 | return Ok(()); 74 | } 75 | } 76 | 77 | let url = format!( 78 | "https://resources.download.minecraft.net/{}/{}", 79 | self.hash_start(), 80 | self.hash 81 | ); 82 | 83 | let mut response = client 84 | .get(&url) 85 | .send() 86 | .await 87 | .change_context(DownloadError::ReqwestError)? 88 | .bytes_stream(); 89 | 90 | let parent_dir = path 91 | .parent() 92 | .ok_or_else(|| report!(DownloadError::IoError))?; 93 | 94 | tokio::fs::create_dir_all(parent_dir) 95 | .await 96 | .change_context(DownloadError::IoError)?; 97 | 98 | let mut file = tokio::fs::File::create(path) 99 | .await 100 | .change_context(DownloadError::IoError)?; 101 | 102 | let mut downloaded: u64 = 0; 103 | 104 | while let Some(item) = response.next().await { 105 | let item = item.change_context(DownloadError::ReqwestError)?; 106 | 107 | file.write_all(&item) 108 | .await 109 | .change_context(DownloadError::IoError)?; 110 | 111 | let new = min(downloaded + (item.len() as u64), self.size()); 112 | downloaded = new; 113 | 114 | sender 115 | .send(DownloadMessage::DownloadProgress(self.clone(), new)) 116 | .change_context(DownloadError::ChannelError)?; 117 | } 118 | 119 | Ok(()) 120 | } 121 | } 122 | 123 | impl From for Object { 124 | fn from(value: api::asset_index::Object) -> Self { 125 | Self { 126 | hash: value.hash, 127 | size: value.size, 128 | } 129 | } 130 | } 131 | 132 | #[derive(Debug, Clone)] 133 | pub struct Assets(HashMap); 134 | 135 | impl From for Assets { 136 | fn from(value: api::asset_index::Assets) -> Self { 137 | Self( 138 | value 139 | .objects 140 | .into_iter() 141 | .map(|(k, v)| (k, v.into())) 142 | .collect(), 143 | ) 144 | } 145 | } 146 | 147 | #[derive(Debug, Clone)] 148 | pub struct Downloader { 149 | assets: Assets, 150 | assets_directory: PathBuf, 151 | 152 | client: reqwest::Client, 153 | sender: Option>, 154 | 155 | max_concurrent_downloads: usize, 156 | } 157 | 158 | impl Downloader { 159 | #[must_use] 160 | pub const fn new( 161 | assets: Assets, 162 | assets_directory: PathBuf, 163 | client: reqwest::Client, 164 | max_concurrent_downloads: usize, 165 | ) -> Self { 166 | Self { 167 | assets, 168 | assets_directory, 169 | client, 170 | sender: None, 171 | max_concurrent_downloads, 172 | } 173 | } 174 | } 175 | 176 | impl downloader::Downloader for Downloader { 177 | type DownloadItem = Object; 178 | 179 | fn create_channel(&mut self) -> crate::downloader::DownloadChannelReceiver { 180 | let (sender, receiver) = mpsc::unbounded_channel(); 181 | self.sender = Some(sender); 182 | 183 | receiver 184 | } 185 | 186 | async fn download_all(self: Arc) -> Result<(), DownloadError> { 187 | let new_self = self.clone(); 188 | 189 | let tasks = stream::iter(self.assets.0.values().cloned()) 190 | .map(|object| { 191 | let cloned_self = self.clone(); 192 | tokio::spawn(async move { 193 | match object 194 | .download( 195 | &cloned_self.assets_directory, 196 | &cloned_self.client, 197 | cloned_self.sender.as_ref().unwrap(), 198 | ) 199 | .await 200 | { 201 | Ok(()) => cloned_self 202 | .sender 203 | .as_ref() 204 | .ok_or(DownloadError::ChannelError)? 205 | .send(DownloadMessage::Downloaded(object)) 206 | .change_context(DownloadError::ChannelError), 207 | Err(e) => Err(e), 208 | } 209 | }) 210 | }) 211 | .buffer_unordered(self.max_concurrent_downloads); 212 | 213 | tasks 214 | .try_collect::>() 215 | .await 216 | .change_context(DownloadError::JoinError)? 217 | .into_iter() 218 | .collect::>()?; 219 | 220 | new_self 221 | .sender 222 | .clone() 223 | .ok_or(DownloadError::ChannelError)? 224 | .send(DownloadMessage::DownloadedAll) 225 | .change_context(DownloadError::ChannelError)?; 226 | 227 | Ok(()) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /apps/glowsquid/src/auth/state.rs: -------------------------------------------------------------------------------- 1 | use chrono::DateTime; 2 | use copper::client::auth::{MicrosoftAuthenticator, MinecraftProfile, MinecraftToken}; 3 | use futures::future::join_all; 4 | use oauth2::{ 5 | basic::BasicTokenType, AccessToken, EmptyExtraTokenFields, RefreshToken, StandardTokenResponse, 6 | TokenResponse, 7 | }; 8 | use once_cell::sync::OnceCell; 9 | use prqlx::query; 10 | use serde::Serialize; 11 | use specta::Type; 12 | use sqlx::SqlitePool; 13 | use tauri::{AppHandle, Manager}; 14 | use tokio::net::TcpListener; 15 | 16 | use crate::database::DbState; 17 | 18 | // Glowsquids details. If you're making your own app, please do not use these. 19 | // These are just for ease of use/packaging. 20 | const CLIENT_ID: &str = "2aa32806-92e3-4242-babc-392ac0f0fd30"; 21 | const CLIENT_SECRET: &str = "nky8Q~8lORwTC0OjdxVsgZSs0hCdTcEdec3hNbaP"; 22 | 23 | #[derive(Clone)] 24 | pub struct State { 25 | /// If the user decided to enable authentication 26 | pub password_hash: OnceCell, 27 | pub pool: SqlitePool, 28 | pub frontend_state: tauri_plugin_state::State, 29 | /// Oauth handler 30 | pub oauth: MicrosoftAuthenticator, 31 | /// Port to listen for the oauth redirect 32 | /// 33 | /// Gotten from an available socket on launch by the system 34 | /// 35 | /// TODO: figure out a better way to make sure that this is always available 36 | pub port: u16, 37 | } 38 | 39 | #[derive(Clone, Serialize, Type)] 40 | #[serde(rename_all = "camelCase")] 41 | /// State of currently authenticated profiles 42 | pub struct AuthState { 43 | /// All the profiles that the user has added 44 | profiles: Vec, 45 | /// All currently encrypted profiles usernames 46 | encrypted_profiles: Vec, 47 | /// Index of the currently selected profile 48 | current_profile_index: Option, 49 | } 50 | 51 | impl State { 52 | pub async fn new(app_handle: &AppHandle) -> Self { 53 | let db_state: tauri::State = app_handle.state(); 54 | let pool = db_state.connection(); 55 | 56 | let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); 57 | let port = listener.local_addr().unwrap().port(); 58 | let redirect_uri = format!("http://localhost:{}/code", port); 59 | 60 | let oauth = MicrosoftAuthenticator::new( 61 | reqwest::Client::new(), 62 | redirect_uri, 63 | CLIENT_ID.to_string(), 64 | CLIENT_SECRET.to_string(), 65 | ) 66 | .expect("To be able to create client"); 67 | 68 | let db_users = query!( 69 | " 70 | from auth 71 | select { username, id, expires_at, access_token, ms_refresh_token, ms_access_token, encrypted_token } 72 | " 73 | ) 74 | .fetch_all(&pool) 75 | .await 76 | .expect("To be able to fetch users"); 77 | 78 | let (encrypted, non_encypted): (Vec<_>, Vec<_>) = 79 | db_users.into_iter().partition(|user| user.encrypted_token); 80 | 81 | let encrypted_profiles = encrypted.into_iter().map(|user| user.username).collect(); 82 | 83 | let profiles = join_all( 84 | non_encypted 85 | .into_iter() 86 | .map(|record| { 87 | let mut ms_token = StandardTokenResponse::new( 88 | AccessToken::new(record.ms_access_token), 89 | BasicTokenType::Bearer, 90 | EmptyExtraTokenFields {}, 91 | ); 92 | 93 | ms_token.set_refresh_token(Some(RefreshToken::new(record.ms_refresh_token))); 94 | 95 | MinecraftToken { 96 | username: record.username, 97 | id: record.id, 98 | access_token: record.access_token, 99 | expires_at: DateTime::parse_from_rfc3339(&record.expires_at) 100 | .expect("To be able to parse date") 101 | .to_utc(), 102 | ms_token, 103 | } 104 | }) 105 | .map(|mut token| async { 106 | if token.is_expired() { 107 | token 108 | .refresh(&oauth) 109 | .await 110 | .expect("To be able to refresh token") 111 | } 112 | 113 | token 114 | }) 115 | .map(|token| async { 116 | oauth 117 | .get_minecraft_profile(&token.await) 118 | .await 119 | .expect("To be able to get minecraft profile") 120 | }), 121 | ) 122 | .await; 123 | 124 | let frontend_state = AuthState { 125 | profiles, 126 | encrypted_profiles, 127 | current_profile_index: None, 128 | }; 129 | 130 | Self { 131 | password_hash: OnceCell::new(), 132 | pool, 133 | frontend_state: tauri_plugin_state::State::new( 134 | "auth", 135 | frontend_state, 136 | app_handle.clone(), 137 | ), 138 | oauth, 139 | port, 140 | } 141 | } 142 | 143 | pub async fn sync(&self) { 144 | self.frontend_state.sync().await; 145 | } 146 | 147 | pub async fn switch_to(&self, account_index: u8) -> bool { 148 | let mut state = self.frontend_state.lock().await; 149 | 150 | if account_index >= state.profiles.len() as u8 { 151 | return false; 152 | } 153 | 154 | state.current_profile_index = Some(account_index); 155 | 156 | true 157 | } 158 | 159 | pub async fn add_account(&self, token: MinecraftToken, profile: MinecraftProfile) -> u8 { 160 | let mut state = self.frontend_state.lock().await; 161 | 162 | // TODO: encryption 163 | 164 | let ms_access_token = token.ms_token.access_token().secret(); 165 | let ms_refresh_token = token.ms_token.refresh_token().unwrap().secret(); 166 | let expires_at = token.expires_at.to_rfc3339(); 167 | let username = profile.name(); 168 | let id = profile.id(); 169 | 170 | sqlx::query!( 171 | " 172 | INSERT INTO auth (username, id, expires_at, access_token, ms_refresh_token, ms_access_token, encrypted_token) 173 | VALUES (?, ?, ?, ?, ?, ?, ?) 174 | ", 175 | username, 176 | id, 177 | expires_at, 178 | token.access_token, 179 | ms_access_token, 180 | ms_refresh_token, 181 | false 182 | ) 183 | .execute(&self.pool) 184 | .await 185 | .expect("To be able to update database"); 186 | 187 | if let Some(idx) = state.profiles.iter().position(|p| p.id() == profile.id()) { 188 | return idx.try_into().unwrap(); 189 | } 190 | state.profiles.push(profile); 191 | 192 | state.profiles.len() as u8 - 1 193 | } 194 | 195 | pub async fn remove_account(&self, account_index: u8) { 196 | let mut state = self.frontend_state.lock().await; 197 | 198 | // TODO: error handling if account_index is out of bounds 199 | let profile = state.profiles.remove(account_index.into()); 200 | let id = profile.id(); 201 | 202 | sqlx::query!( 203 | " 204 | DELETE FROM auth 205 | WHERE id = ? 206 | ", 207 | id 208 | ) 209 | .execute(&self.pool) 210 | .await 211 | .expect("To be able to update database"); 212 | 213 | if let Some(current_index) = state.current_profile_index { 214 | match current_index.cmp(&account_index) { 215 | std::cmp::Ordering::Less => { 216 | state.current_profile_index = Some(current_index - 1); 217 | } 218 | std::cmp::Ordering::Equal => { 219 | state.current_profile_index = None; 220 | } 221 | std::cmp::Ordering::Greater => {} 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /libs/copper/src/client/library.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt, 4 | path::{Path, PathBuf}, 5 | sync::Arc, 6 | }; 7 | 8 | use error_stack::{report, Report, ResultExt}; 9 | use futures::{stream, StreamExt, TryStreamExt}; 10 | use tokio::{fs, sync::mpsc}; 11 | 12 | use crate::{ 13 | api::{self, client::Artifact}, 14 | downloader::{ 15 | self, DownloadChannelReceiver, DownloadChannelSender, DownloadError, DownloadMessage, 16 | }, 17 | }; 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct Library { 21 | /// The package/namespace of the library 22 | package: String, 23 | /// The name of the library 24 | name: String, 25 | /// The version of the library 26 | version: String, 27 | /// The downloads for the library 28 | downloads: api::client::LibraryDownloads, 29 | /// The rules for the library 30 | /// 31 | /// If the rules are not met, the library will not be processed 32 | rules: Vec, 33 | /// The natives for the library 34 | /// 35 | /// Depending on the operating system, the natives will be extracted 36 | natives: api::client::Natives, 37 | /// The extract information for the library 38 | /// If this is `None`, the library is not extracted 39 | extract: Option, 40 | } 41 | 42 | impl TryFrom for Library { 43 | type Error = Report; 44 | 45 | fn try_from(value: api::client::Library) -> Result { 46 | let (package, name, version) = value 47 | .parse_name() 48 | .ok_or(report!(LauncherLibraryError::InvalidName))?; 49 | 50 | Ok(Self { 51 | package: package.to_owned(), 52 | name: name.to_owned(), 53 | version: version.to_owned(), 54 | downloads: value.downloads, 55 | extract: value.extract, 56 | rules: value.rules.unwrap_or_default(), 57 | natives: value.natives.unwrap_or_default(), 58 | }) 59 | } 60 | } 61 | 62 | #[derive(Debug)] 63 | pub enum LauncherLibraryError { 64 | InvalidName, 65 | } 66 | 67 | impl fmt::Display for LauncherLibraryError { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | f.write_str(match self { 70 | Self::InvalidName => { 71 | "Invalid package name. It must be in the format: package:name:version" 72 | } 73 | }) 74 | } 75 | } 76 | 77 | impl Error for LauncherLibraryError {} 78 | 79 | impl Library { 80 | #[must_use] 81 | pub fn canonical_name(&self) -> String { 82 | format!("{}:{}:{}", self.package, self.name, self.version) 83 | } 84 | 85 | #[must_use] 86 | pub fn check_rules_passes(&self) -> bool { 87 | self.rules.iter().all(api::client::Rule::passes) 88 | } 89 | 90 | #[must_use] 91 | pub(crate) const fn downloads(&self) -> &api::client::LibraryDownloads { 92 | &self.downloads 93 | } 94 | 95 | /// Downloads the library artifacts and classifiers to the specified base directory, 96 | /// appending the required path to the base directory (`{artifact.path}/{artifact.name}`) 97 | /// 98 | /// # Errors 99 | /// Errors if: 100 | /// - The download fails. The server could be down, the request was blocked, etc. 101 | /// - The file cannot be created. This could be due to a lack of permissions, storage space, etc. 102 | /// - If there is a native, the native couldn't be extracted. This could be due to a lack of permissions, storage space, etc. 103 | pub async fn download( 104 | &self, 105 | base_path: &Path, 106 | client: &reqwest::Client, 107 | sender: &DownloadChannelSender, 108 | ) -> error_stack::Result<(), DownloadError> { 109 | if let Some(artifact) = &self.downloads.artifact { 110 | artifact.download(base_path, client, sender).await?; 111 | }; 112 | 113 | if let Some(native) = self.native_for_current_os() { 114 | native.download(base_path, client, sender).await?; 115 | self.extract_native(native, base_path).await?; 116 | } 117 | Ok(()) 118 | } 119 | 120 | #[must_use] 121 | /// Returns the native artifact for the current OS. 122 | pub fn native_for_current_os(&self) -> Option<&Artifact> { 123 | let classifiers = self.downloads.classifiers.as_ref()?; 124 | 125 | if cfg!(target_os = "windows") && self.natives.windows.is_some() { 126 | classifiers.windows() 127 | } else if cfg!(target_os = "macos") { 128 | classifiers.macos() 129 | } else if cfg!(target_os = "linux") && self.natives.linux.is_some() { 130 | classifiers.linux() 131 | } else { 132 | None 133 | } 134 | } 135 | 136 | async fn extract_native( 137 | &self, 138 | native: &Artifact, 139 | base_path: &Path, 140 | ) -> error_stack::Result<(), DownloadError> { 141 | if self 142 | .extract 143 | .as_ref() 144 | .is_some_and(|ex| !ex.exclude.is_empty()) 145 | { 146 | todo!("Handle extraction exclude rules"); 147 | } 148 | 149 | let path = base_path.join(native.path()); 150 | 151 | if !path.try_exists().change_context(DownloadError::IoError)? { 152 | return Err(report!(DownloadError::ExtractionError) 153 | .attach_printable(format!("Native does not exist at path: {}", path.display()))); 154 | } 155 | 156 | let file = std::fs::File::open(&path).change_context(DownloadError::IoError)?; 157 | 158 | let mut archive = 159 | zip::ZipArchive::new(file).change_context(DownloadError::ExtractionError)?; 160 | 161 | // format is {libraries_dir}/{os}/{arch} 162 | let os = std::env::consts::OS; 163 | let arch = std::env::consts::ARCH.replace("x86_64", "x64"); 164 | let native_extract_dir = base_path.join(os).join(arch); 165 | 166 | fs::create_dir_all(&native_extract_dir) 167 | .await 168 | .change_context(DownloadError::IoError)?; 169 | 170 | archive 171 | .extract(&native_extract_dir) 172 | .change_context(DownloadError::ExtractionError)?; 173 | 174 | Ok(()) 175 | } 176 | } 177 | 178 | #[derive(Debug, Clone)] 179 | pub struct Downloader { 180 | libraries: Vec, 181 | libraries_directory: PathBuf, 182 | 183 | client: reqwest::Client, 184 | sender: Option>, 185 | 186 | max_concurrent_downloads: usize, 187 | } 188 | 189 | impl Downloader { 190 | #[must_use] 191 | pub fn new( 192 | libraries: Vec, 193 | libraries_directory: PathBuf, 194 | client: reqwest::Client, 195 | max_concurrent_downloads: usize, 196 | ) -> Self { 197 | Self { 198 | libraries, 199 | client, 200 | libraries_directory, 201 | sender: None, 202 | max_concurrent_downloads, 203 | } 204 | } 205 | } 206 | 207 | impl downloader::Downloader for Downloader { 208 | type DownloadItem = Artifact; 209 | 210 | fn create_channel(&mut self) -> DownloadChannelReceiver { 211 | let (sender, receiver) = mpsc::unbounded_channel(); 212 | self.sender = Some(sender); 213 | 214 | receiver 215 | } 216 | 217 | async fn download_all(self: Arc) -> error_stack::Result<(), DownloadError> { 218 | let new_self = self.clone(); 219 | 220 | // bug: this doesn't work if you filter first 221 | #[allow(clippy::iter_overeager_cloned)] 222 | let libraries = new_self 223 | .libraries 224 | .iter() 225 | .cloned() 226 | .filter(Library::check_rules_passes); 227 | 228 | let tasks = stream::iter(libraries) 229 | .map(|library| { 230 | let cloned_self = self.clone(); 231 | tokio::spawn(async move { 232 | match library 233 | .download( 234 | &cloned_self.libraries_directory, 235 | &cloned_self.client, 236 | cloned_self.sender.as_ref().unwrap(), 237 | ) 238 | .await 239 | { 240 | Ok(()) => (), 241 | Err(e) => return Err(e), 242 | }; 243 | 244 | Ok(()) 245 | }) 246 | }) 247 | .buffer_unordered(self.max_concurrent_downloads); 248 | 249 | tasks 250 | .try_collect::>() 251 | .await 252 | .change_context(DownloadError::JoinError)? 253 | .into_iter() 254 | .collect::>()?; 255 | 256 | new_self 257 | .sender 258 | .clone() 259 | .ok_or(DownloadError::ChannelError)? 260 | .send(DownloadMessage::DownloadedAll) 261 | .change_context(DownloadError::ChannelError)?; 262 | 263 | Ok(()) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /libs/copper/src/parser.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | 3 | use itertools::Itertools; 4 | use tracing::debug; 5 | 6 | use crate::{ 7 | api::client::{Action, Artifact, GameArgument, GameRule, JvmArgument, Rule, Value}, 8 | client, 9 | launcher::{Launcher, Quickplay}, 10 | }; 11 | 12 | pub struct JvmArgs<'a> { 13 | launcher: &'a Launcher, 14 | manifest: &'a client::manifest::Manifest, 15 | } 16 | 17 | impl<'a> JvmArgs<'a> { 18 | #[must_use] 19 | pub const fn new(launcher: &'a Launcher, manifest: &'a client::manifest::Manifest) -> Self { 20 | Self { launcher, manifest } 21 | } 22 | 23 | #[must_use] 24 | pub fn parse_jvm_args(&self) -> Vec { 25 | let args = self.manifest.arguments(); 26 | 27 | let jvm = args.jvm(); 28 | jvm.iter() 29 | .map(|arg| match arg { 30 | JvmArgument::String(arg) => self.parse_java_arg_str(arg), 31 | JvmArgument::Class(class) => { 32 | let passes = class.rules.iter().all(Rule::passes); 33 | 34 | if !passes { 35 | return String::new(); 36 | }; 37 | 38 | match &class.value { 39 | Value::String(s) => self.parse_java_arg_str(s), 40 | Value::StringArray(a) => { 41 | a.iter().map(|v| self.parse_java_arg_str(v)).join(" ") 42 | } 43 | } 44 | } 45 | }) 46 | .filter(|s| !s.is_empty()) 47 | .collect() 48 | } 49 | 50 | fn parse_java_arg_str(&self, arg: &str) -> String { 51 | arg.replace( 52 | "${natives_directory}", 53 | self.launcher 54 | .libraries_directory() 55 | .join("natives") 56 | .to_str() 57 | .unwrap_or_default(), 58 | ) 59 | .replace("${launcher_name}", self.launcher.launcher_name()) 60 | .replace("${launcher_version}", self.launcher.launcher_version()) 61 | .replace("${classpath}", &self.get_classpath()) 62 | } 63 | 64 | fn get_classpath(&self) -> String { 65 | let libraries = self.manifest.libraries(); 66 | 67 | libraries 68 | .iter() 69 | .filter(|lib| lib.check_rules_passes()) 70 | .flat_map(|lib| self.get_lib_path(lib)) 71 | .chain(iter::once( 72 | self.launcher.jar_path().to_str().unwrap().to_string(), 73 | )) 74 | .join(if cfg!(windows) { ";" } else { ":" }) 75 | } 76 | 77 | fn get_lib_path(&self, lib: &client::library::Library) -> Vec { 78 | let paths = [ 79 | lib.native_for_current_os(), 80 | lib.downloads().artifact.as_ref(), 81 | ]; 82 | 83 | paths 84 | .into_iter() 85 | .flatten() 86 | .map(Artifact::path) 87 | .map(|path| self.launcher.libraries_directory().join(path)) 88 | .filter_map(|path| dunce::canonicalize(path).ok()) 89 | .filter_map(|path| path.to_str().map(ToString::to_string)) 90 | .collect() 91 | } 92 | } 93 | 94 | pub struct MinecraftArgs<'a> { 95 | launcher: &'a Launcher, 96 | manifest: &'a client::manifest::Manifest, 97 | } 98 | 99 | impl<'a> MinecraftArgs<'a> { 100 | #[must_use] 101 | pub const fn new(launcher: &'a Launcher, manifest: &'a client::manifest::Manifest) -> Self { 102 | Self { launcher, manifest } 103 | } 104 | 105 | fn is_excluded_minecraft_arg(arg_str: &str) -> bool { 106 | let excluded = ["--xuid", "${auth_xuid}", "--clientId", "${clientid}"]; 107 | 108 | excluded.contains(&arg_str) 109 | } 110 | 111 | #[must_use] 112 | #[tracing::instrument(skip(self))] 113 | pub fn parse_minecraft_args(&self) -> Vec { 114 | debug!("Parsing minecraft args"); 115 | self.manifest 116 | .arguments() 117 | .game() 118 | .iter() 119 | .map(|arg| match arg { 120 | GameArgument::GameClass(class) => { 121 | let passes = class 122 | .rules() 123 | .iter() 124 | .all(|rule| self.minecraft_rule_passes(rule)); 125 | 126 | if !passes { 127 | return String::new(); 128 | }; 129 | 130 | match class.value() { 131 | Value::String(s) => self.parse_minecraft_arg_str(s), 132 | Value::StringArray(a) => { 133 | a.iter().map(|v| self.parse_minecraft_arg_str(v)).join(" ") 134 | } 135 | } 136 | } 137 | GameArgument::String(arg) => self.parse_minecraft_arg_str(arg), 138 | }) 139 | .filter(|s| !s.is_empty()) 140 | .collect() 141 | } 142 | 143 | #[tracing::instrument(skip(self))] 144 | fn parse_minecraft_arg_str(&self, minecraft_arg: &str) -> String { 145 | debug!("Parsing minecraft arg: {}", minecraft_arg); 146 | 147 | // if excluded return emptry String 148 | if Self::is_excluded_minecraft_arg(minecraft_arg) { 149 | return String::new(); 150 | } 151 | 152 | minecraft_arg 153 | .replace( 154 | "${auth_player_name}", 155 | &self 156 | .launcher 157 | .authentication_details() 158 | .auth_details() 159 | .username, 160 | ) 161 | .replace( 162 | "${version_name}", 163 | &self.launcher.version_name().replace([' ', ':'], "_"), 164 | ) 165 | .replace( 166 | "${game_directory}", 167 | self.launcher.game_directory().to_str().unwrap_or_default(), 168 | ) 169 | .replace( 170 | "${assets_root}", 171 | self.launcher 172 | .assets_directory() 173 | .to_str() 174 | .unwrap_or_default(), 175 | ) 176 | .replace("${assets_index_name}", self.launcher.manifest().assets()) 177 | .replace( 178 | "${auth_uuid}", 179 | &self.launcher.authentication_details().auth_details().id, 180 | ) 181 | .replace( 182 | "${auth_access_token}", 183 | &self 184 | .launcher 185 | .authentication_details() 186 | .auth_details() 187 | .access_token, 188 | ) 189 | .replace("${user_type}", "msa") // copper only supports MSA 190 | .replace( 191 | "${version_type}", 192 | if self.launcher.is_snapshot() { 193 | "snapshot" 194 | } else { 195 | "release" 196 | }, 197 | ) 198 | .replace( 199 | "${resolution_width}", 200 | &self 201 | .launcher 202 | .custom_resolution() 203 | .map(|r| r.width.to_string()) 204 | .unwrap_or_default(), 205 | ) 206 | .replace( 207 | "${resolution_height}", 208 | &self 209 | .launcher 210 | .custom_resolution() 211 | .as_ref() 212 | .map(|r| r.height.to_string()) 213 | .unwrap_or_default(), 214 | ) 215 | } 216 | 217 | fn quickplay_check bool>(&self, x: bool, qp: T) -> bool { 218 | x == self.launcher.quickplay().is_some_and(qp) 219 | } 220 | 221 | fn minecraft_rule_passes(&self, rule: &GameRule) -> bool { 222 | match rule.action() { 223 | Action::Allow => { 224 | let features = rule.features(); 225 | 226 | let demo_check = features.demo_user().map_or(true, |demo_user| { 227 | demo_user == self.launcher.authentication_details().is_demo_user() 228 | }); 229 | 230 | let support_check = 231 | features 232 | .quick_plays_support() 233 | .map_or(true, |quick_plays_support| { 234 | quick_plays_support == self.launcher.quickplay().is_some() 235 | }); 236 | 237 | let quickplay_singleplayer_check = 238 | features.quick_play_singleplayer().map_or(true, |x| { 239 | self.quickplay_check(x, Quickplay::is_singleplayer) 240 | }); 241 | 242 | let quickplay_multiplayer_check = features 243 | .quick_play_multiplayer() 244 | .map_or(true, |x| self.quickplay_check(x, Quickplay::is_multiplayer)); 245 | 246 | let quickplay_realms_check = features 247 | .quick_play_realms() 248 | .map_or(true, |x| self.quickplay_check(x, Quickplay::is_realms)); 249 | 250 | let custom_resolution_check = features 251 | .custom_resolution() 252 | .map_or(true, |x| x == self.launcher.custom_resolution().is_some()); 253 | 254 | demo_check 255 | && support_check 256 | && quickplay_singleplayer_check 257 | && quickplay_multiplayer_check 258 | && quickplay_realms_check 259 | && custom_resolution_check 260 | } 261 | Action::Disallow => { 262 | todo!("disallow rules are not supported yet, as none exist") 263 | } 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /libs/tauri-plugin-state/permissions/schemas/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "PermissionFile", 4 | "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", 5 | "type": "object", 6 | "properties": { 7 | "default": { 8 | "description": "The default permission set for the plugin", 9 | "anyOf": [ 10 | { 11 | "$ref": "#/definitions/DefaultPermission" 12 | }, 13 | { 14 | "type": "null" 15 | } 16 | ] 17 | }, 18 | "set": { 19 | "description": "A list of permissions sets defined", 20 | "type": "array", 21 | "items": { 22 | "$ref": "#/definitions/PermissionSet" 23 | } 24 | }, 25 | "permission": { 26 | "description": "A list of inlined permissions", 27 | "default": [], 28 | "type": "array", 29 | "items": { 30 | "$ref": "#/definitions/Permission" 31 | } 32 | } 33 | }, 34 | "definitions": { 35 | "DefaultPermission": { 36 | "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", 37 | "type": "object", 38 | "required": [ 39 | "permissions" 40 | ], 41 | "properties": { 42 | "version": { 43 | "description": "The version of the permission.", 44 | "type": [ 45 | "integer", 46 | "null" 47 | ], 48 | "format": "uint64", 49 | "minimum": 1.0 50 | }, 51 | "description": { 52 | "description": "Human-readable description of what the permission does.", 53 | "type": [ 54 | "string", 55 | "null" 56 | ] 57 | }, 58 | "permissions": { 59 | "description": "All permissions this set contains.", 60 | "type": "array", 61 | "items": { 62 | "type": "string" 63 | } 64 | } 65 | } 66 | }, 67 | "PermissionSet": { 68 | "description": "A set of direct permissions grouped together under a new name.", 69 | "type": "object", 70 | "required": [ 71 | "description", 72 | "identifier", 73 | "permissions" 74 | ], 75 | "properties": { 76 | "identifier": { 77 | "description": "A unique identifier for the permission.", 78 | "type": "string" 79 | }, 80 | "description": { 81 | "description": "Human-readable description of what the permission does.", 82 | "type": "string" 83 | }, 84 | "permissions": { 85 | "description": "All permissions this set contains.", 86 | "type": "array", 87 | "items": { 88 | "$ref": "#/definitions/PermissionKind" 89 | } 90 | } 91 | } 92 | }, 93 | "Permission": { 94 | "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", 95 | "type": "object", 96 | "required": [ 97 | "identifier" 98 | ], 99 | "properties": { 100 | "version": { 101 | "description": "The version of the permission.", 102 | "type": [ 103 | "integer", 104 | "null" 105 | ], 106 | "format": "uint64", 107 | "minimum": 1.0 108 | }, 109 | "identifier": { 110 | "description": "A unique identifier for the permission.", 111 | "type": "string" 112 | }, 113 | "description": { 114 | "description": "Human-readable description of what the permission does.", 115 | "type": [ 116 | "string", 117 | "null" 118 | ] 119 | }, 120 | "commands": { 121 | "description": "Allowed or denied commands when using this permission.", 122 | "default": { 123 | "allow": [], 124 | "deny": [] 125 | }, 126 | "allOf": [ 127 | { 128 | "$ref": "#/definitions/Commands" 129 | } 130 | ] 131 | }, 132 | "scope": { 133 | "description": "Allowed or denied scoped when using this permission.", 134 | "allOf": [ 135 | { 136 | "$ref": "#/definitions/Scopes" 137 | } 138 | ] 139 | }, 140 | "platforms": { 141 | "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", 142 | "type": [ 143 | "array", 144 | "null" 145 | ], 146 | "items": { 147 | "$ref": "#/definitions/Target" 148 | } 149 | } 150 | } 151 | }, 152 | "Commands": { 153 | "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", 154 | "type": "object", 155 | "properties": { 156 | "allow": { 157 | "description": "Allowed command.", 158 | "default": [], 159 | "type": "array", 160 | "items": { 161 | "type": "string" 162 | } 163 | }, 164 | "deny": { 165 | "description": "Denied command, which takes priority.", 166 | "default": [], 167 | "type": "array", 168 | "items": { 169 | "type": "string" 170 | } 171 | } 172 | } 173 | }, 174 | "Scopes": { 175 | "description": "A restriction of the command/endpoint functionality.\n\nIt can be of any serde serializable type and is used for allowing or preventing certain actions inside a Tauri command.\n\nThe scope is passed to the command and handled/enforced by the command itself.", 176 | "type": "object", 177 | "properties": { 178 | "allow": { 179 | "description": "Data that defines what is allowed by the scope.", 180 | "type": [ 181 | "array", 182 | "null" 183 | ], 184 | "items": { 185 | "$ref": "#/definitions/Value" 186 | } 187 | }, 188 | "deny": { 189 | "description": "Data that defines what is denied by the scope.", 190 | "type": [ 191 | "array", 192 | "null" 193 | ], 194 | "items": { 195 | "$ref": "#/definitions/Value" 196 | } 197 | } 198 | } 199 | }, 200 | "Value": { 201 | "description": "All supported ACL values.", 202 | "anyOf": [ 203 | { 204 | "description": "Represents a null JSON value.", 205 | "type": "null" 206 | }, 207 | { 208 | "description": "Represents a [`bool`].", 209 | "type": "boolean" 210 | }, 211 | { 212 | "description": "Represents a valid ACL [`Number`].", 213 | "allOf": [ 214 | { 215 | "$ref": "#/definitions/Number" 216 | } 217 | ] 218 | }, 219 | { 220 | "description": "Represents a [`String`].", 221 | "type": "string" 222 | }, 223 | { 224 | "description": "Represents a list of other [`Value`]s.", 225 | "type": "array", 226 | "items": { 227 | "$ref": "#/definitions/Value" 228 | } 229 | }, 230 | { 231 | "description": "Represents a map of [`String`] keys to [`Value`]s.", 232 | "type": "object", 233 | "additionalProperties": { 234 | "$ref": "#/definitions/Value" 235 | } 236 | } 237 | ] 238 | }, 239 | "Number": { 240 | "description": "A valid ACL number.", 241 | "anyOf": [ 242 | { 243 | "description": "Represents an [`i64`].", 244 | "type": "integer", 245 | "format": "int64" 246 | }, 247 | { 248 | "description": "Represents a [`f64`].", 249 | "type": "number", 250 | "format": "double" 251 | } 252 | ] 253 | }, 254 | "Target": { 255 | "description": "Platform target.", 256 | "oneOf": [ 257 | { 258 | "description": "MacOS.", 259 | "type": "string", 260 | "enum": [ 261 | "macOS" 262 | ] 263 | }, 264 | { 265 | "description": "Windows.", 266 | "type": "string", 267 | "enum": [ 268 | "windows" 269 | ] 270 | }, 271 | { 272 | "description": "Linux.", 273 | "type": "string", 274 | "enum": [ 275 | "linux" 276 | ] 277 | }, 278 | { 279 | "description": "Android.", 280 | "type": "string", 281 | "enum": [ 282 | "android" 283 | ] 284 | }, 285 | { 286 | "description": "iOS.", 287 | "type": "string", 288 | "enum": [ 289 | "iOS" 290 | ] 291 | } 292 | ] 293 | }, 294 | "PermissionKind": { 295 | "type": "string", 296 | "oneOf": [ 297 | { 298 | "description": "allow-execute -> Enables the execute command without any pre-configured scope.", 299 | "type": "string", 300 | "enum": [ 301 | "allow-execute" 302 | ] 303 | }, 304 | { 305 | "description": "deny-execute -> Denies the execute command without any pre-configured scope.", 306 | "type": "string", 307 | "enum": [ 308 | "deny-execute" 309 | ] 310 | }, 311 | { 312 | "description": "allow-ping -> Enables the ping command without any pre-configured scope.", 313 | "type": "string", 314 | "enum": [ 315 | "allow-ping" 316 | ] 317 | }, 318 | { 319 | "description": "deny-ping -> Denies the ping command without any pre-configured scope.", 320 | "type": "string", 321 | "enum": [ 322 | "deny-ping" 323 | ] 324 | } 325 | ] 326 | } 327 | } 328 | } -------------------------------------------------------------------------------- /libs/copper/examples/full_launch.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | future::IntoFuture, 4 | net::SocketAddr, 5 | path::PathBuf, 6 | sync::{Arc, OnceLock}, 7 | }; 8 | 9 | use axum::{ 10 | extract::{Query, State}, 11 | routing::get, 12 | Router, 13 | }; 14 | 15 | use copper::{ 16 | api::version, 17 | client::{ 18 | self, 19 | auth::{AuthenticationInfo, MicrosoftAuthenticator, MinecraftToken, OauthCode}, 20 | }, 21 | downloader::{DownloadMessage, Downloader}, 22 | launcher::{self, AuthenticationDetails, LauncherBuilder, RamSize}, 23 | }; 24 | use error_stack::Report; 25 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 26 | use oauth2::{CsrfToken, PkceCodeVerifier}; 27 | use tokio::{io::AsyncBufReadExt, net::TcpListener, sync::mpsc, task}; 28 | use tracing::{error, info}; 29 | use tracing_subscriber::{fmt::format::PrettyFields, prelude::*}; 30 | 31 | extern crate axum; 32 | extern crate copper; 33 | extern crate error_stack; 34 | extern crate indicatif; 35 | extern crate java_locator; 36 | extern crate tokio; 37 | extern crate tracing; 38 | extern crate tracing_subscriber; 39 | 40 | // Glowsquids details. If you are going to make your own app, please do not use these. 41 | // these are just for ease of use. 42 | const CLIENT_ID: &str = "2aa32806-92e3-4242-babc-392ac0f0fd30"; 43 | const CLIENT_SECRET: &str = "nky8Q~8lORwTC0OjdxVsgZSs0hCdTcEdec3hNbaP"; 44 | 45 | static CSRF_TOKEN: OnceLock = OnceLock::new(); 46 | static PKCE_VERIFIER: OnceLock = OnceLock::new(); 47 | static TOKEN: OnceLock = OnceLock::new(); 48 | 49 | #[derive(Clone)] 50 | struct AppState { 51 | oauth: MicrosoftAuthenticator, 52 | shutdown_send: Arc>, 53 | } 54 | 55 | #[tokio::main] 56 | async fn main() { 57 | // some setup for logging 58 | Report::set_color_mode(error_stack::fmt::ColorMode::Color); 59 | 60 | let error_handler = tracing_error::ErrorLayer::new(PrettyFields::new()); 61 | 62 | let (shutdown_sender, mut shutdown_recv) = mpsc::channel(1); 63 | tracing_subscriber::fmt() 64 | .pretty() 65 | .with_env_filter( 66 | tracing_subscriber::EnvFilter::builder() 67 | .with_default_directive(tracing::Level::INFO.into()) 68 | .from_env_lossy(), 69 | ) 70 | .finish() 71 | .with(error_handler) 72 | .init(); 73 | 74 | let client = reqwest::Client::new(); 75 | 76 | info!("Setting up auth"); 77 | 78 | let cwd = std::env::current_dir().unwrap(); 79 | let minecraft_dir = cwd.join(".minecraft"); 80 | let token_path = minecraft_dir.join("token.json"); 81 | 82 | // now lets start auth! 83 | // we need a redirect uri so that the oauth server can redirect the user back to our server 84 | info!("Initializing oauth2 client..."); 85 | let redirect_uri = "http://localhost:3000/code".to_string(); 86 | let oauth = MicrosoftAuthenticator::new( 87 | client.clone(), 88 | redirect_uri, 89 | CLIENT_ID.to_string(), 90 | CLIENT_SECRET.to_string(), 91 | ) 92 | .expect("To be able to create client"); 93 | 94 | let auth_info = oauth.create_auth_info(); 95 | 96 | // Check if the token.json file already exists. If it does, we can just load the token from disk 97 | if token_path.exists() { 98 | info!("Token exists, loading from disk"); 99 | let token = std::fs::read_to_string(token_path).unwrap(); 100 | let mut token: MinecraftToken = serde_json::from_str(&token).unwrap(); 101 | token.refresh(&oauth).await.unwrap(); 102 | TOKEN.set(token).expect("To be able to set token"); 103 | } else { 104 | info!("Initializing server..."); 105 | 106 | // Start the server and wait for the user to authenticate 107 | let router = Router::new() 108 | .route("/code", get(get_code)) 109 | .with_state(AppState { 110 | oauth: oauth.clone(), 111 | shutdown_send: Arc::new(shutdown_sender), 112 | }); 113 | 114 | let socket_addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 115 | let listener = TcpListener::bind(&socket_addr).await.unwrap(); 116 | let server = task::spawn( 117 | axum::serve(listener, router.into_make_service()) 118 | .with_graceful_shutdown(async move { shutdown_recv.recv().await.unwrap() }) 119 | .into_future(), 120 | ); 121 | 122 | info!("Server initialized on port 3000!..."); 123 | 124 | // get the auth url and csrf token 125 | // the csrf token is to prevent Cross Site Request Forgery 126 | // read more about it at https://en.wikipedia.org/wiki/Cross-site_request_forgery 127 | // the tl;dr is that it is a security measure to prevent malicious websites from impersonating 128 | // us 129 | let AuthenticationInfo { 130 | url, 131 | csrf_token, 132 | pkce_verifier, 133 | } = auth_info; 134 | 135 | // now that we have the verifier and token, we can set it so it can be accessed by the server 136 | // this is not the best way to do it, but it is the easiest and works for this example 137 | 138 | PKCE_VERIFIER 139 | .set(pkce_verifier) 140 | .expect("To be able to set pkce verifier"); 141 | 142 | CSRF_TOKEN 143 | .set(csrf_token) 144 | .expect("To be able to set csrf token"); 145 | 146 | info!("Please go to this url to authenticate: {}", url); 147 | 148 | tokio::spawn(server); 149 | 150 | // cursed way to wait for the token to be set 151 | while TOKEN.get().is_none() { 152 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 153 | } 154 | 155 | info!("Saving token to disk. Please delete this once you're done testing"); 156 | 157 | // Please do not do this in production. This is just for ease of use 158 | // In production, you should store the token in a secure way so that it is not exposed in plaintext 159 | let as_json = serde_json::to_string_pretty(&TOKEN.get().unwrap()).unwrap(); 160 | 161 | std::fs::write(token_path, as_json).unwrap(); 162 | } 163 | 164 | info!("Authenticated! Token: {:?}", TOKEN.get()); 165 | 166 | // We need to download some metadata about the latest game version before we can launch the game 167 | info!("Downloading minecraft manifest"); 168 | let manifest = version::Manifest::get().await.unwrap(); 169 | 170 | info!("Downloaded minecraft manifest"); 171 | 172 | let latest = manifest.latest_release(); 173 | info!("Downloading manifest for latest release: {}", latest.id()); 174 | 175 | // This is the actual version manifest file that contains all the information about the version 176 | // Such as the libraries, assets, java arguments, etc 177 | let manifest = latest.download().await.unwrap(); 178 | 179 | let manifest_dir = minecraft_dir.join("versions").join(latest.id()); 180 | 181 | manifest 182 | .save_to_disk(&manifest_dir.join(format!("{}.json", latest.id()))) 183 | .await 184 | .unwrap(); 185 | 186 | let token = TOKEN.get().unwrap(); 187 | 188 | let auth_details = AuthenticationDetails::new(oauth, token.clone(), false); 189 | 190 | // We need to locate the java executable so that we can launch the game 191 | // In a real application, you would probably want to make sure that the java install is valid for the version 192 | // and allow the user to specify their own java path if they want to 193 | let java_home = PathBuf::from(java_locator::locate_java_home().expect("Java not found")); 194 | let java_path = match std::env::consts::OS { 195 | // On windows, we need to use javaw.exe to prevent a console window from opening 196 | "windows" => java_home.join("bin").join("javaw.exe"), 197 | _ => java_home.join("bin").join("java"), 198 | }; 199 | 200 | // We need to convert the manifest into a format that the launcher can understand 201 | // This removes a lot of the unnecessary information and makes it easier to work with 202 | let launcher_manifest: client::manifest::Manifest = manifest.try_into().unwrap(); 203 | 204 | // now we can create the launcher and prepare to launch the game 205 | let mut launcher = LauncherBuilder::default() 206 | .authentication_details(auth_details) 207 | .custom_resolution(None) 208 | .jar_path(manifest_dir.join(format!("{}.jar", latest.id()))) 209 | .game_directory(minecraft_dir.clone()) 210 | .assets_directory(minecraft_dir.join("assets")) 211 | .libraries_directory(minecraft_dir.join("libraries")) 212 | .version_manifest_path(manifest_dir.join(format!("{}.json", latest.id()))) 213 | // You should probably get this from the original version manifest, but for now we'll just hardcode it since we're only using the latest stable version 214 | .is_snapshot(false) 215 | .version_name(latest.id()) 216 | .ram_size(RamSize { 217 | min: "1024Mb".to_string(), 218 | max: "4096Mb".to_string(), 219 | }) 220 | .java_path(java_path) 221 | // these should reflect the name and version of your launcher 222 | .launcher_name("copper") 223 | .launcher_version(env!("CARGO_PKG_VERSION").to_string()) 224 | // This is for quickplay, which is a feature that allows you to quickly launch the game without any additional setup 225 | // 226 | // You could use this to instantly open up a world or server. Pretty handy if you want to have a more intergrated launcher experience 227 | .quickplay(None) 228 | // This is a reqwest client that is used to download additional files the game needs 229 | .http_client(client.clone()) 230 | .manifest(launcher_manifest.clone()) 231 | .build() 232 | .unwrap(); 233 | 234 | // Once the launcher is made, we can add additional arguments to the JVM 235 | // Here are some examples for debugging and custom wayland support 236 | 237 | // launcher.add_jvm_argument(copper::assets::client::Jvm::String( 238 | // "-Dorg.lwjgl.util.Debug=true".to_string() 239 | // )); 240 | 241 | // launcher.add_jvm_argument(copper::assets::client::Jvm::String( 242 | // "-Dorg.lwjgl.glfw.libname=glfw_wayland".to_string() 243 | // )); 244 | 245 | info!("Launcher created. Downloading files"); 246 | 247 | // the asset index is a list of all the assets the game needs to run 248 | let index = launcher_manifest.asset_index().download().await.unwrap(); 249 | 250 | // we also need to save the index, since the game uses it to locate the assets on disk 251 | let index_dir = minecraft_dir.join("assets").join("indexes"); 252 | index 253 | .save_to_disk(&index_dir.join(format!("{}.json", launcher_manifest.assets()))) 254 | .await 255 | .unwrap(); 256 | 257 | // We can now start downloading everything the game needs 258 | let mut downloader = launcher::Downloader::new(&launcher, index.into(), 16); 259 | 260 | // We need to create a channel to watch for download progress 261 | let mut reciever = downloader.create_channel(); 262 | 263 | // Just some download tracking. You can do this however you want 264 | let mut bars = HashMap::new(); 265 | let m = MultiProgress::new(); 266 | let sty = ProgressStyle::with_template( 267 | "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})" 268 | ) 269 | .unwrap() 270 | .progress_chars("##-"); 271 | 272 | let downloader = Arc::new(downloader); 273 | 274 | // offload the download to a separate task to prevent blocking the main thread, and therefore the progress bars 275 | let task_downloader = downloader.clone(); 276 | let task = tokio::task::spawn(async move { 277 | task_downloader.download_all().await.unwrap(); 278 | }); 279 | 280 | info!("Watching for messages"); 281 | // every time we get a message, update the progress trackers 282 | while let Some(dl) = reciever.recv().await { 283 | match dl { 284 | DownloadMessage::Downloaded(object) => { 285 | let download_name = match object { 286 | launcher::DownloadItem::Asset(asset) => asset.hash().to_owned(), 287 | launcher::DownloadItem::Library(artifact) => artifact.sha1().to_owned(), 288 | launcher::DownloadItem::Client(_) => "client".to_owned(), 289 | }; 290 | 291 | let Some((_, bar)): Option<(String, ProgressBar)> = bars.remove(&download_name) 292 | else { 293 | continue; 294 | }; 295 | 296 | bar.finish(); 297 | m.remove(&bar); 298 | m.println(format!("Downloaded {}", download_name)).unwrap(); 299 | } 300 | DownloadMessage::DownloadedAll => { 301 | // This is a final message that tells us that all the files have been downloaded. 302 | // Useful for when you want to do something after all the files have been downloaded 303 | m.println("Downloaded all files required. Joining tasks") 304 | .unwrap(); 305 | 306 | // in our case, we're done, so make sure to break out of the loop 307 | break; 308 | } 309 | DownloadMessage::DownloadProgress(object, how_much) => { 310 | let download_name = match object { 311 | launcher::DownloadItem::Asset(ref asset) => asset.hash().to_owned(), 312 | launcher::DownloadItem::Library(ref artifact) => artifact.sha1().to_owned(), 313 | launcher::DownloadItem::Client(_) => "client".to_owned(), 314 | }; 315 | 316 | let size = match object { 317 | launcher::DownloadItem::Asset(asset) => asset.size(), 318 | launcher::DownloadItem::Library(artifact) => artifact.size(), 319 | launcher::DownloadItem::Client(client) => client.size(), 320 | }; 321 | 322 | let bar = bars.entry(download_name.clone()).or_insert_with(|| { 323 | let bar = m.add(indicatif::ProgressBar::new(size)); 324 | bar.set_style(sty.clone()); 325 | bar.set_message(download_name.clone()); 326 | 327 | (download_name.clone(), bar) 328 | }); 329 | 330 | bar.1.set_position(how_much); 331 | } 332 | } 333 | } 334 | 335 | task.await.unwrap(); 336 | 337 | info!("Done downloading files. Launching the game"); 338 | 339 | // now we can actually launch the game! 340 | 341 | let game_manager = launcher.launch().await.unwrap(); 342 | 343 | // These tasks forward the stdout and stderr of the game to the logger 344 | let stdout_task = tokio::spawn(async move { 345 | let mut lines = game_manager.stdout.lines(); 346 | while let Some(buf) = lines.next_line().await.unwrap() { 347 | info!("STDOUT: {}", buf); 348 | } 349 | }); 350 | 351 | let stderr_task = tokio::spawn(async move { 352 | let mut lines = game_manager.stderr.lines(); 353 | while let Some(line) = lines.next_line().await.unwrap() { 354 | error!("STDERR: {}", line); 355 | } 356 | }); 357 | 358 | info!("Game launched. Waiting for it to exit"); 359 | 360 | stdout_task.await.unwrap(); 361 | stderr_task.await.unwrap(); 362 | 363 | // This notifies us when the game has actually exited 364 | game_manager.exit_handle.await.unwrap(); 365 | } 366 | 367 | async fn get_code(Query(code): Query, State(state): State) { 368 | info!("Received code: {:?}. Authenticating...", code); 369 | 370 | let csrf = CSRF_TOKEN 371 | .get() 372 | .expect("To be able to get CSRF token") 373 | .clone(); 374 | 375 | let pkce = PKCE_VERIFIER 376 | .get() 377 | .expect("To be able to get pkce verifier"); 378 | 379 | let ms_token = state 380 | .oauth 381 | .get_ms_access_token(code, csrf, pkce) 382 | .await 383 | .expect("To be able to get token"); 384 | 385 | let token = state 386 | .oauth 387 | .get_minecraft_token(ms_token.clone()) 388 | .await 389 | .expect("To be able to get token"); 390 | 391 | TOKEN.set(token).expect("To be able to set token"); 392 | 393 | info!("Got token. Returning to process"); 394 | state 395 | .shutdown_send 396 | .send(()) 397 | .await 398 | .expect("To be able to send shutdown signal"); 399 | } 400 | --------------------------------------------------------------------------------