mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-28 12:59:21 +00:00
Implement proper handbook authentication (pt. 1)
This commit is contained in:
parent
79d417c3ca
commit
f1cf6da178
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -41,7 +41,7 @@ jobs:
|
|||||||
uses: suisei-cn/actions-download-file@v1.4.0
|
uses: suisei-cn/actions-download-file@v1.4.0
|
||||||
with:
|
with:
|
||||||
url: https://api.grasscutter.io/static/handbook.html
|
url: https://api.grasscutter.io/static/handbook.html
|
||||||
target: src/main/resources/
|
target: src/main/resources/html/
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: ./gradlew && ./gradlew jar
|
run: ./gradlew && ./gradlew jar
|
||||||
- name: Upload build
|
- name: Upload build
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -66,6 +66,8 @@ tmp/
|
|||||||
/*.sh
|
/*.sh
|
||||||
|
|
||||||
GM Handbook*.txt
|
GM Handbook*.txt
|
||||||
|
handbook.html
|
||||||
|
|
||||||
config.json
|
config.json
|
||||||
mitmdump.exe
|
mitmdump.exe
|
||||||
mongod.exe
|
mongod.exe
|
||||||
@ -75,8 +77,6 @@ mappings.js
|
|||||||
BuildConfig.java
|
BuildConfig.java
|
||||||
data/hk4e/announcement/
|
data/hk4e/announcement/
|
||||||
|
|
||||||
src/main/resources/handbook.html
|
|
||||||
|
|
||||||
# lombok
|
# lombok
|
||||||
/.apt_generated/
|
/.apt_generated/
|
||||||
|
|
||||||
|
@ -1,22 +1,86 @@
|
|||||||
import type { CommandResponse } from "@backend/types";
|
import type { CommandResponse } from "@backend/types";
|
||||||
import emitter from "@backend/events";
|
import emitter from "@backend/events";
|
||||||
|
|
||||||
let targetPlayer = 0; // The UID of the target player.
|
let playerToken: string | null = null; // The session token for the player.
|
||||||
|
export let targetPlayer = 0; // The UID of the target player.
|
||||||
|
|
||||||
|
// The server's address and port.
|
||||||
|
export let address: string = "127.0.0.1", port: string = "443";
|
||||||
|
export let encrypted: boolean = true;
|
||||||
|
|
||||||
|
export let lockedPlayer = false; // Whether the UID field is locked.
|
||||||
export let connected = false; // Whether the server is connected.
|
export let connected = false; // Whether the server is connected.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the server details from local storage.
|
||||||
|
*/
|
||||||
|
export function setup(): void {
|
||||||
|
// Load the server details from local storage.
|
||||||
|
const storedAddress = localStorage.getItem("address");
|
||||||
|
const storedPort = localStorage.getItem("port");
|
||||||
|
|
||||||
|
// Set the server details.
|
||||||
|
if (storedAddress) address = storedAddress;
|
||||||
|
if (storedPort) port = storedPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the formed URL.
|
||||||
|
* This assumes that the server upgrades to HTTPS.
|
||||||
|
*/
|
||||||
|
export function url(): string {
|
||||||
|
// noinspection HttpUrlsUsage
|
||||||
|
return `http${window.isSecureContext || encrypted ? "s" : ""}://${address}:${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the target player.
|
* Sets the target player.
|
||||||
*
|
*
|
||||||
* @param player The UID of the target player.
|
* @param player The UID of the target player.
|
||||||
|
* @param token The session token for the player.
|
||||||
*/
|
*/
|
||||||
export function setTargetPlayer(player: number): void {
|
export function setTargetPlayer(player: number, token: string | null = null): void {
|
||||||
|
playerToken = token;
|
||||||
targetPlayer = player;
|
targetPlayer = player;
|
||||||
|
|
||||||
|
// Determine connected status.
|
||||||
connected = !isNaN(player) && player > 0;
|
connected = !isNaN(player) && player > 0;
|
||||||
|
// Determine locked status.
|
||||||
|
lockedPlayer = connected && token != null;
|
||||||
|
|
||||||
// Emit the connected event.
|
// Emit the connected event.
|
||||||
emitter.emit("connected", connected);
|
emitter.emit("connected", connected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the server details.
|
||||||
|
*
|
||||||
|
* @param newAddress The server's address.
|
||||||
|
* @param newPort The server's port.
|
||||||
|
*/
|
||||||
|
export async function setServerDetails(newAddress: string | null, newPort: string | null): Promise<void> {
|
||||||
|
// Apply the new details.
|
||||||
|
if (newAddress != null) {
|
||||||
|
address = newAddress;
|
||||||
|
localStorage.setItem("address", newAddress);
|
||||||
|
}
|
||||||
|
if (newPort != null) {
|
||||||
|
port = newPort;
|
||||||
|
localStorage.setItem("port", newPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the server is encrypted.
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
encrypted = true;
|
||||||
|
fetch(`${url()}`)
|
||||||
|
.catch(() => {
|
||||||
|
encrypted = false;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.then(() => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates a number.
|
* Validates a number.
|
||||||
*
|
*
|
||||||
@ -44,9 +108,10 @@ export async function grantAvatar(
|
|||||||
if (invalid(avatar) || invalid(level) || invalid(constellations) || invalid(talents))
|
if (invalid(avatar) || invalid(level) || invalid(constellations) || invalid(talents))
|
||||||
return { status: -1, message: "Invalid arguments." };
|
return { status: -1, message: "Invalid arguments." };
|
||||||
|
|
||||||
return await fetch(`https://localhost:443/handbook/avatar`, {
|
return await fetch(`${url()}/handbook/avatar`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
playerToken,
|
||||||
player: targetPlayer.toString(),
|
player: targetPlayer.toString(),
|
||||||
avatar: avatar.toString(),
|
avatar: avatar.toString(),
|
||||||
level,
|
level,
|
||||||
@ -68,9 +133,10 @@ export async function giveItem(item: number, amount = 1): Promise<CommandRespons
|
|||||||
// Validate the number.
|
// Validate the number.
|
||||||
if (isNaN(amount) || amount < 1) return { status: -1, message: "Invalid amount." };
|
if (isNaN(amount) || amount < 1) return { status: -1, message: "Invalid amount." };
|
||||||
|
|
||||||
return await fetch(`https://localhost:443/handbook/item`, {
|
return await fetch(`${url()}/handbook/item`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
playerToken,
|
||||||
player: targetPlayer.toString(),
|
player: targetPlayer.toString(),
|
||||||
item: item.toString(),
|
item: item.toString(),
|
||||||
amount
|
amount
|
||||||
@ -87,9 +153,10 @@ export async function teleportTo(scene: number): Promise<CommandResponse> {
|
|||||||
// Validate the number.
|
// Validate the number.
|
||||||
if (isNaN(scene) || scene < 1) return { status: -1, message: "Invalid scene." };
|
if (isNaN(scene) || scene < 1) return { status: -1, message: "Invalid scene." };
|
||||||
|
|
||||||
return await fetch(`https://localhost:443/handbook/teleport`, {
|
return await fetch(`${url()}/handbook/teleport`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
playerToken,
|
||||||
player: targetPlayer.toString(),
|
player: targetPlayer.toString(),
|
||||||
scene: scene.toString()
|
scene: scene.toString()
|
||||||
})
|
})
|
||||||
@ -108,9 +175,10 @@ export async function spawnEntity(entity: number, amount = 1, level = 1): Promis
|
|||||||
if (isNaN(entity) || isNaN(amount) || isNaN(level) || amount < 1 || level < 1 || level > 200)
|
if (isNaN(entity) || isNaN(amount) || isNaN(level) || amount < 1 || level < 1 || level > 200)
|
||||||
return { status: -1, message: "Invalid arguments." };
|
return { status: -1, message: "Invalid arguments." };
|
||||||
|
|
||||||
return await fetch(`https://localhost:443/handbook/spawn`, {
|
return await fetch(`${url()}/handbook/spawn`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
playerToken,
|
||||||
player: targetPlayer.toString(),
|
player: targetPlayer.toString(),
|
||||||
entity: entity.toString(),
|
entity: entity.toString(),
|
||||||
amount,
|
amount,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export type Page = "Home" | "Commands" | "Avatars" | "Items" | "Entities" | "Scenes";
|
export type Page = "Home" | "Commands" | "Avatars" | "Items" | "Entities" | "Scenes";
|
||||||
|
export type Overlays = "None" | "ServerSettings";
|
||||||
export type Days = "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday";
|
export type Days = "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday";
|
||||||
|
|
||||||
export type Command = {
|
export type Command = {
|
||||||
|
@ -46,6 +46,12 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.EntitiesPage_Input {
|
.EntitiesPage_Input {
|
||||||
background-color: transparent;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
|
12
src/handbook/src/css/views/Overlay.scss
Normal file
12
src/handbook/src/css/views/Overlay.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.Overlay {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
background-color: rgb(0, 0, 0, 0.35);
|
||||||
|
}
|
@ -72,3 +72,9 @@
|
|||||||
color: var(--text-secondary-color);
|
color: var(--text-secondary-color);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.SideBar_Input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
border-radius: 10px ;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
117
src/handbook/src/css/widgets/ServerSettings.scss
Normal file
117
src/handbook/src/css/widgets/ServerSettings.scss
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
.ServerSettings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 620px;
|
||||||
|
max-height: 400px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ServerSettings_Content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ServerSettings_Top {
|
||||||
|
height: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ServerSettings_Frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ServerSettings_Title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 34px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ServerSettings_Details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 590px;
|
||||||
|
max-height: 50px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
|
||||||
|
&:focus, &:active {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ServerSettings_Authenticate {
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 210px;
|
||||||
|
max-height: 46px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ServerSettings_Save {
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 46px;
|
||||||
|
max-width: 120px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
}
|
@ -3,12 +3,14 @@ import { createRoot } from "react-dom/client";
|
|||||||
|
|
||||||
import * as data from "@backend/data";
|
import * as data from "@backend/data";
|
||||||
import * as events from "@backend/events";
|
import * as events from "@backend/events";
|
||||||
|
import * as server from "@backend/server";
|
||||||
|
|
||||||
import App from "@ui/App";
|
import App from "@ui/App";
|
||||||
|
|
||||||
// Call initial setup functions.
|
// Call initial setup functions.
|
||||||
data.setup();
|
data.setup();
|
||||||
events.setup();
|
events.setup();
|
||||||
|
server.setup();
|
||||||
|
|
||||||
// Render the application.
|
// Render the application.
|
||||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
@ -2,6 +2,7 @@ import React from "react";
|
|||||||
|
|
||||||
import SideBar from "@views/SideBar";
|
import SideBar from "@views/SideBar";
|
||||||
import Content from "@views/Content";
|
import Content from "@views/Content";
|
||||||
|
import Overlay from "@views/Overlay";
|
||||||
import PlainText from "@views/PlainText";
|
import PlainText from "@views/PlainText";
|
||||||
|
|
||||||
import type { Page } from "@backend/types";
|
import type { Page } from "@backend/types";
|
||||||
@ -25,7 +26,6 @@ class App extends React.Component<{}, IState> {
|
|||||||
// Check if the window's href is a page.
|
// Check if the window's href is a page.
|
||||||
let targetPage = null;
|
let targetPage = null;
|
||||||
const page = window.location.href.split("/").pop();
|
const page = window.location.href.split("/").pop();
|
||||||
console.log(page);
|
|
||||||
|
|
||||||
if (page != undefined && page != "") {
|
if (page != undefined && page != "") {
|
||||||
// Convert the page to a Page type.
|
// Convert the page to a Page type.
|
||||||
@ -44,7 +44,16 @@ class App extends React.Component<{}, IState> {
|
|||||||
return (
|
return (
|
||||||
<div className={"App"}>
|
<div className={"App"}>
|
||||||
<SideBar />
|
<SideBar />
|
||||||
{this.state.plain ? <PlainText /> : <Content initial={this.state.initial} />}
|
|
||||||
|
{
|
||||||
|
this.state.plain ?
|
||||||
|
<PlainText /> :
|
||||||
|
<Content
|
||||||
|
initial={this.state.initial}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Overlay />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
61
src/handbook/src/ui/views/Overlay.tsx
Normal file
61
src/handbook/src/ui/views/Overlay.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import ServerSettings from "@widgets/ServerSettings";
|
||||||
|
|
||||||
|
import type { Overlays } from "@backend/types";
|
||||||
|
|
||||||
|
import "@css/views/Overlay.scss";
|
||||||
|
import events from "@backend/events";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
page: Overlays;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Overlay extends React.Component<{}, IState> {
|
||||||
|
constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
page: "None"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the page to display.
|
||||||
|
*
|
||||||
|
* @param page The page to display.
|
||||||
|
*/
|
||||||
|
private setPage(page: Overlays): void {
|
||||||
|
this.setState({ page });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the page to display.
|
||||||
|
*/
|
||||||
|
private getPage(): React.ReactNode {
|
||||||
|
switch (this.state.page) {
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
case "ServerSettings":
|
||||||
|
return <ServerSettings />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
events.on("overlay", this.setPage.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
events.off("overlay", this.setPage.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.state.page != "None" ? (
|
||||||
|
<div className={"Overlay"}>
|
||||||
|
{this.getPage()}
|
||||||
|
</div>
|
||||||
|
) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Overlay;
|
@ -10,13 +10,14 @@ import Icon_Map from "@assets/Icon_Map.webp";
|
|||||||
import Icon_Quests from "@assets/Icon_Quests.webp";
|
import Icon_Quests from "@assets/Icon_Quests.webp";
|
||||||
import Icon_Achievements from "@assets/Icon_Achievements.webp";
|
import Icon_Achievements from "@assets/Icon_Achievements.webp";
|
||||||
|
|
||||||
import { navigate } from "@backend/events";
|
import events, { navigate } from "@backend/events";
|
||||||
import { setTargetPlayer } from "@backend/server";
|
import { targetPlayer, lockedPlayer, setTargetPlayer } from "@backend/server";
|
||||||
|
|
||||||
import "@css/views/SideBar.scss";
|
import "@css/views/SideBar.scss";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
uid: string | null;
|
uid: string | null;
|
||||||
|
uidLocked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SideBar extends React.Component<{}, IState> {
|
class SideBar extends React.Component<{}, IState> {
|
||||||
@ -24,10 +25,22 @@ class SideBar extends React.Component<{}, IState> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
uid: null
|
uid: targetPlayer > 0 ? targetPlayer.toString() : null,
|
||||||
|
uidLocked: lockedPlayer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the player's UID changes.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private updateUid(): void {
|
||||||
|
this.setState({
|
||||||
|
uid: targetPlayer > 0 ? targetPlayer.toString() : null,
|
||||||
|
uidLocked: lockedPlayer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked when the UID input changes.
|
* Invoked when the UID input changes.
|
||||||
*
|
*
|
||||||
@ -39,10 +52,32 @@ class SideBar extends React.Component<{}, IState> {
|
|||||||
const uid = input == "" ? null : input;
|
const uid = input == "" ? null : input;
|
||||||
if (uid && uid.length > 10) return;
|
if (uid && uid.length > 10) return;
|
||||||
|
|
||||||
this.setState({ uid });
|
|
||||||
setTargetPlayer(parseInt(uid ?? "0"));
|
setTargetPlayer(parseInt(uid ?? "0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the UID input is right-clicked.
|
||||||
|
*
|
||||||
|
* @param event The event.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private onRightClick(event: React.MouseEvent<HTMLInputElement, MouseEvent>): void {
|
||||||
|
// Remove focus from the input.
|
||||||
|
event.currentTarget.blur();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Open the server settings overlay.
|
||||||
|
events.emit("overlay", "ServerSettings");
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
events.on("connected", this.updateUid.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
events.off("connected", this.updateUid.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={"SideBar"}>
|
<div className={"SideBar"}>
|
||||||
@ -74,7 +109,9 @@ class SideBar extends React.Component<{}, IState> {
|
|||||||
className={"SideBar_Input"}
|
className={"SideBar_Input"}
|
||||||
placeholder={"Enter UID..."}
|
placeholder={"Enter UID..."}
|
||||||
value={this.state.uid ?? undefined}
|
value={this.state.uid ?? undefined}
|
||||||
|
disabled={this.state.uidLocked}
|
||||||
onChange={this.onChange.bind(this)}
|
onChange={this.onChange.bind(this)}
|
||||||
|
onContextMenu={this.onRightClick.bind(this)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
176
src/handbook/src/ui/widgets/ServerSettings.tsx
Normal file
176
src/handbook/src/ui/widgets/ServerSettings.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import emitter from "@backend/events";
|
||||||
|
import {
|
||||||
|
targetPlayer, address, port,
|
||||||
|
setServerDetails, url, setTargetPlayer
|
||||||
|
} from "@backend/server";
|
||||||
|
|
||||||
|
import "@css/widgets/ServerSettings.scss";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
webview: boolean;
|
||||||
|
|
||||||
|
address: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerSettings extends React.Component<{}, IState> {
|
||||||
|
constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
webview: false,
|
||||||
|
address: address,
|
||||||
|
port: Number(port)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener("keyup", this.escapeListener.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener("keyup", this.escapeListener.bind(this));
|
||||||
|
window.removeEventListener("message", this.handleAuthentication.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the escape key is pressed.
|
||||||
|
*
|
||||||
|
* @param e The keyboard event.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private escapeListener(e: KeyboardEvent): void {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
// Hide the overlay.
|
||||||
|
emitter.emit("overlay", "None");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the component tries to authenticate.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private authenticate(): void {
|
||||||
|
setServerDetails(null, null).then(() => {
|
||||||
|
this.setState({ webview: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the event listener for authentication.
|
||||||
|
window.addEventListener("message", this.handleAuthentication.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finishes the authentication process.
|
||||||
|
*
|
||||||
|
* @param e The message event.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private handleAuthentication(e: MessageEvent): void {
|
||||||
|
const data = e.data; // The data sent from the server.
|
||||||
|
if (data == null) return; // If the data is null, return.
|
||||||
|
|
||||||
|
// Check if the data is an object.
|
||||||
|
if (typeof data != "object") return;
|
||||||
|
// Get the data type.
|
||||||
|
const type = data["type"] ?? null;
|
||||||
|
if (type != "handbook-auth") return;
|
||||||
|
|
||||||
|
// Get the data.
|
||||||
|
const uid = data["uid"] ?? null;
|
||||||
|
const token = data["token"] ?? null;
|
||||||
|
|
||||||
|
// Hide the overlay.
|
||||||
|
emitter.emit("overlay", "None");
|
||||||
|
// Set the token and user ID.
|
||||||
|
setTargetPlayer(Number(uid), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the save button is clicked.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private save(): void {
|
||||||
|
// Hide the overlay.
|
||||||
|
emitter.emit("overlay", "None");
|
||||||
|
|
||||||
|
// Save the server settings.
|
||||||
|
setServerDetails(
|
||||||
|
this.state.address,
|
||||||
|
this.state.port.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={"ServerSettings"}>
|
||||||
|
{
|
||||||
|
this.state.webview ? (
|
||||||
|
<iframe
|
||||||
|
className={"ServerSettings_Frame"}
|
||||||
|
src={`${url()}/handbook/authenticate?uid=${targetPlayer}`}
|
||||||
|
/>
|
||||||
|
) : <>
|
||||||
|
<div className={"ServerSettings_Content ServerSettings_Top"}>
|
||||||
|
<h1 className={"ServerSettings_Title"}>Server Settings</h1>
|
||||||
|
|
||||||
|
<div className={"ServerSettings_Details"}>
|
||||||
|
<div>
|
||||||
|
<p>Address:</p>
|
||||||
|
<input
|
||||||
|
type={"text"}
|
||||||
|
value={this.state.address}
|
||||||
|
onChange={(e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
this.setState({ address: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Port:</p>
|
||||||
|
<input
|
||||||
|
type={"text"}
|
||||||
|
value={this.state.port == 0 ? "" : this.state.port}
|
||||||
|
onChange={(e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
if (isNaN(Number(value))
|
||||||
|
|| value.length > 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ port: Number(value) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={"ServerSettings_Authenticate"}
|
||||||
|
onClick={this.authenticate.bind(this)}
|
||||||
|
>
|
||||||
|
Authenticate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={"ServerSettings_Content"}>
|
||||||
|
<button
|
||||||
|
className={"ServerSettings_Save"}
|
||||||
|
onClick={this.save.bind(this)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServerSettings;
|
@ -4,11 +4,12 @@ import emu.grasscutter.game.Account;
|
|||||||
import emu.grasscutter.server.http.objects.*;
|
import emu.grasscutter.server.http.objects.*;
|
||||||
import emu.grasscutter.utils.DispatchUtils;
|
import emu.grasscutter.utils.DispatchUtils;
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/** Defines an authenticator for the server. Can be changed by plugins. */
|
/** Defines an authenticator for the server. Can be changed by plugins. */
|
||||||
public interface AuthenticationSystem {
|
public interface AuthenticationSystem {
|
||||||
|
|
||||||
@ -130,6 +131,13 @@ public interface AuthenticationSystem {
|
|||||||
*/
|
*/
|
||||||
OAuthAuthenticator getOAuthAuthenticator();
|
OAuthAuthenticator getOAuthAuthenticator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the authenticator used for handling handbook authentication requests.
|
||||||
|
*
|
||||||
|
* @return An authenticator.
|
||||||
|
*/
|
||||||
|
HandbookAuthenticator getHandbookAuthenticator();
|
||||||
|
|
||||||
/** A data container that holds relevant data for authenticating a client. */
|
/** A data container that holds relevant data for authenticating a client. */
|
||||||
@Builder
|
@Builder
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package emu.grasscutter.auth;
|
package emu.grasscutter.auth;
|
||||||
|
|
||||||
import static emu.grasscutter.config.Configuration.ACCOUNT;
|
|
||||||
import static emu.grasscutter.utils.Language.translate;
|
|
||||||
|
|
||||||
import emu.grasscutter.Grasscutter;
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.auth.DefaultAuthenticators.*;
|
import emu.grasscutter.auth.DefaultAuthenticators.*;
|
||||||
import emu.grasscutter.game.Account;
|
import emu.grasscutter.game.Account;
|
||||||
import emu.grasscutter.server.http.objects.ComboTokenResJson;
|
import emu.grasscutter.server.http.objects.ComboTokenResJson;
|
||||||
import emu.grasscutter.server.http.objects.LoginResultJson;
|
import emu.grasscutter.server.http.objects.LoginResultJson;
|
||||||
|
|
||||||
|
import static emu.grasscutter.config.Configuration.ACCOUNT;
|
||||||
|
import static emu.grasscutter.utils.Language.translate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default Grasscutter authentication implementation. Allows all users to access any account.
|
* The default Grasscutter authentication implementation. Allows all users to access any account.
|
||||||
*/
|
*/
|
||||||
@ -20,6 +20,7 @@ public final class DefaultAuthentication implements AuthenticationSystem {
|
|||||||
private final Authenticator<Account> sessionTokenValidator = new SessionTokenValidator();
|
private final Authenticator<Account> sessionTokenValidator = new SessionTokenValidator();
|
||||||
private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication();
|
private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication();
|
||||||
private final OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication();
|
private final OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication();
|
||||||
|
private final HandbookAuthenticator handbookAuthenticator = new HandbookAuthentication();
|
||||||
|
|
||||||
public DefaultAuthentication() {
|
public DefaultAuthentication() {
|
||||||
if (ACCOUNT.EXPERIMENTAL_RealPassword) {
|
if (ACCOUNT.EXPERIMENTAL_RealPassword) {
|
||||||
@ -75,4 +76,9 @@ public final class DefaultAuthentication implements AuthenticationSystem {
|
|||||||
public OAuthAuthenticator getOAuthAuthenticator() {
|
public OAuthAuthenticator getOAuthAuthenticator() {
|
||||||
return this.oAuthAuthenticator;
|
return this.oAuthAuthenticator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandbookAuthenticator getHandbookAuthenticator() {
|
||||||
|
return this.handbookAuthenticator;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
package emu.grasscutter.auth;
|
package emu.grasscutter.auth;
|
||||||
|
|
||||||
import static emu.grasscutter.config.Configuration.ACCOUNT;
|
|
||||||
import static emu.grasscutter.utils.Language.translate;
|
|
||||||
|
|
||||||
import at.favre.lib.crypto.bcrypt.BCrypt;
|
import at.favre.lib.crypto.bcrypt.BCrypt;
|
||||||
import emu.grasscutter.Grasscutter;
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
||||||
@ -12,15 +9,21 @@ import emu.grasscutter.server.dispatch.IDispatcher;
|
|||||||
import emu.grasscutter.server.dispatch.PacketIds;
|
import emu.grasscutter.server.dispatch.PacketIds;
|
||||||
import emu.grasscutter.server.http.objects.ComboTokenResJson;
|
import emu.grasscutter.server.http.objects.ComboTokenResJson;
|
||||||
import emu.grasscutter.server.http.objects.LoginResultJson;
|
import emu.grasscutter.server.http.objects.LoginResultJson;
|
||||||
|
import emu.grasscutter.utils.DispatchUtils;
|
||||||
import emu.grasscutter.utils.FileUtils;
|
import emu.grasscutter.utils.FileUtils;
|
||||||
import emu.grasscutter.utils.Utils;
|
import emu.grasscutter.utils.Utils;
|
||||||
|
import io.javalin.http.ContentType;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import javax.crypto.Cipher;
|
|
||||||
|
import static emu.grasscutter.config.Configuration.ACCOUNT;
|
||||||
|
import static emu.grasscutter.utils.Language.translate;
|
||||||
|
|
||||||
/** A class containing default authenticators. */
|
/** A class containing default authenticators. */
|
||||||
public final class DefaultAuthenticators {
|
public final class DefaultAuthenticators {
|
||||||
@ -372,4 +375,61 @@ public final class DefaultAuthenticators {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Handles authentication for the web GM Handbook. */
|
||||||
|
public static class HandbookAuthentication implements HandbookAuthenticator {
|
||||||
|
private final String authPage;
|
||||||
|
|
||||||
|
public HandbookAuthentication() {
|
||||||
|
try {
|
||||||
|
this.authPage = new String(
|
||||||
|
FileUtils.readResource("/html/handbook_auth.html"));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
throw new RuntimeException("Failed to load handbook auth page.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void presentPage(AuthenticationRequest request) {
|
||||||
|
var ctx = request.getContext();
|
||||||
|
if (ctx == null) return;
|
||||||
|
|
||||||
|
// Respond with the handbook auth page.
|
||||||
|
ctx.contentType(ContentType.TEXT_HTML)
|
||||||
|
.result(this.authPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response authenticate(AuthenticationRequest request) {
|
||||||
|
var ctx = request.getContext();
|
||||||
|
if (ctx == null) return null;
|
||||||
|
|
||||||
|
// Get the body data.
|
||||||
|
var playerId = ctx.formParam("playerid");
|
||||||
|
if (playerId == null) {
|
||||||
|
return Response.builder().status(400)
|
||||||
|
.body("Invalid player ID.").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the player's session token.
|
||||||
|
var sessionKey = DispatchUtils.fetchSessionKey(
|
||||||
|
Integer.parseInt(playerId));
|
||||||
|
if (sessionKey == null) {
|
||||||
|
return Response.builder().status(400)
|
||||||
|
.body("Invalid player ID.").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the account is banned.
|
||||||
|
return Response.builder().status(200)
|
||||||
|
.body(this.authPage.replace("{{VALUE}}", "true")
|
||||||
|
.replace("{{SESSION_TOKEN}}", sessionKey)
|
||||||
|
.replace("{{PLAYER_ID}}", playerId))
|
||||||
|
.build();
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return Response.builder().status(500)
|
||||||
|
.body("Invalid player ID.").build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package emu.grasscutter.auth;
|
||||||
|
|
||||||
|
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** Handles player authentication for the web GM handbook. */
|
||||||
|
public interface HandbookAuthenticator {
|
||||||
|
@Getter @Builder
|
||||||
|
class Response {
|
||||||
|
private final int status;
|
||||||
|
private final String body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the user requests to authenticate.
|
||||||
|
* This should respond with a page that allows the user to authenticate.
|
||||||
|
*
|
||||||
|
* @route GET /handbook/authenticate
|
||||||
|
* @param request The authentication request.
|
||||||
|
*/
|
||||||
|
void presentPage(AuthenticationRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the user requests to authenticate.
|
||||||
|
* This is called when the user submits the authentication form.
|
||||||
|
* This should respond with HTML that sends a message to the GM Handbook.
|
||||||
|
* See the default handbook authentication page for an example.
|
||||||
|
*
|
||||||
|
* @param request The authentication request.
|
||||||
|
* @return The response to send to the client.
|
||||||
|
*/
|
||||||
|
Response authenticate(AuthenticationRequest request);
|
||||||
|
}
|
@ -94,7 +94,8 @@ import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
|||||||
@Entity(value = "players", useDiscriminator = false)
|
@Entity(value = "players", useDiscriminator = false)
|
||||||
public class Player implements PlayerHook {
|
public class Player implements PlayerHook {
|
||||||
@Id private int id;
|
@Id private int id;
|
||||||
@Indexed(options = @IndexOptions(unique = true)) private String accountId;
|
@Indexed(options = @IndexOptions(unique = true))
|
||||||
|
@Getter private String accountId;
|
||||||
@Setter private transient Account account;
|
@Setter private transient Account account;
|
||||||
@Getter @Setter private transient GameSession session;
|
@Getter @Setter private transient GameSession session;
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package emu.grasscutter.server.dispatch;
|
package emu.grasscutter.server.dispatch;
|
||||||
|
|
||||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import emu.grasscutter.Grasscutter;
|
import emu.grasscutter.Grasscutter;
|
||||||
@ -12,21 +10,25 @@ import emu.grasscutter.utils.Crypto;
|
|||||||
import emu.grasscutter.utils.DispatchUtils;
|
import emu.grasscutter.utils.DispatchUtils;
|
||||||
import emu.grasscutter.utils.JsonUtils;
|
import emu.grasscutter.utils.JsonUtils;
|
||||||
import emu.grasscutter.utils.objects.HandbookBody;
|
import emu.grasscutter.utils.objects.HandbookBody;
|
||||||
import java.net.ConnectException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.java_websocket.WebSocket;
|
import org.java_websocket.WebSocket;
|
||||||
import org.java_websocket.client.WebSocketClient;
|
import org.java_websocket.client.WebSocketClient;
|
||||||
import org.java_websocket.handshake.ServerHandshake;
|
import org.java_websocket.handshake.ServerHandshake;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.net.ConnectException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||||
|
|
||||||
public final class DispatchClient extends WebSocketClient implements IDispatcher {
|
public final class DispatchClient extends WebSocketClient implements IDispatcher {
|
||||||
@Getter private final Logger logger = Grasscutter.getLogger();
|
@Getter private final Logger logger = Grasscutter.getLogger();
|
||||||
@Getter private final Map<Integer, BiConsumer<WebSocket, JsonElement>> handlers = new HashMap<>();
|
@Getter private final Map<Integer, BiConsumer<WebSocket, JsonElement>> handlers = new HashMap<>();
|
||||||
@ -41,6 +43,7 @@ public final class DispatchClient extends WebSocketClient implements IDispatcher
|
|||||||
|
|
||||||
this.registerHandler(PacketIds.GachaHistoryReq, this::fetchGachaHistory);
|
this.registerHandler(PacketIds.GachaHistoryReq, this::fetchGachaHistory);
|
||||||
this.registerHandler(PacketIds.GmTalkReq, this::handleHandbookAction);
|
this.registerHandler(PacketIds.GmTalkReq, this::handleHandbookAction);
|
||||||
|
this.registerHandler(PacketIds.GetPlayerFieldsReq, this::fetchPlayerFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,6 +108,32 @@ public final class DispatchClient extends WebSocketClient implements IDispatcher
|
|||||||
this.sendMessage(PacketIds.GmTalkRsp, response);
|
this.sendMessage(PacketIds.GmTalkRsp, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the fields of an online player.
|
||||||
|
*
|
||||||
|
* @param socket The socket the packet was received from.
|
||||||
|
* @param object The packet data.
|
||||||
|
*/
|
||||||
|
private void fetchPlayerFields(WebSocket socket, JsonElement object) {
|
||||||
|
var message = IDispatcher.decode(object);
|
||||||
|
var playerId = message.get("playerId").getAsInt();
|
||||||
|
var fieldsRaw = message.get("fields").getAsJsonArray();
|
||||||
|
|
||||||
|
// Get the player with the specified ID.
|
||||||
|
var player = Grasscutter.getGameServer().getPlayerByUid(playerId, true);
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
// Convert the fields array.
|
||||||
|
var fieldsList = new ArrayList<String>();
|
||||||
|
for (var field : fieldsRaw)
|
||||||
|
fieldsList.add(field.getAsString());
|
||||||
|
var fields = fieldsList.toArray(new String[0]);
|
||||||
|
|
||||||
|
// Return the response object.
|
||||||
|
this.sendMessage(PacketIds.GetPlayerFieldsRsp,
|
||||||
|
DispatchUtils.getPlayerFields(playerId, fields));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a serialized encrypted message to the server.
|
* Sends a serialized encrypted message to the server.
|
||||||
*
|
*
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package emu.grasscutter.server.dispatch;
|
package emu.grasscutter.server.dispatch;
|
||||||
|
|
||||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import emu.grasscutter.Grasscutter;
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.database.DatabaseHelper;
|
import emu.grasscutter.database.DatabaseHelper;
|
||||||
import emu.grasscutter.utils.Crypto;
|
import emu.grasscutter.utils.Crypto;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.java_websocket.WebSocket;
|
||||||
|
import org.java_websocket.handshake.ClientHandshake;
|
||||||
|
import org.java_websocket.server.WebSocketServer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -15,11 +19,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import lombok.Getter;
|
|
||||||
import org.java_websocket.WebSocket;
|
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||||
import org.java_websocket.handshake.ClientHandshake;
|
|
||||||
import org.java_websocket.server.WebSocketServer;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
|
|
||||||
/* Internal communications server. */
|
/* Internal communications server. */
|
||||||
public final class DispatchServer extends WebSocketServer implements IDispatcher {
|
public final class DispatchServer extends WebSocketServer implements IDispatcher {
|
||||||
@ -39,6 +40,7 @@ public final class DispatchServer extends WebSocketServer implements IDispatcher
|
|||||||
|
|
||||||
this.registerHandler(PacketIds.LoginNotify, this::handleLogin);
|
this.registerHandler(PacketIds.LoginNotify, this::handleLogin);
|
||||||
this.registerHandler(PacketIds.TokenValidateReq, this::validateToken);
|
this.registerHandler(PacketIds.TokenValidateReq, this::validateToken);
|
||||||
|
this.registerHandler(PacketIds.GetAccountReq, this::fetchAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,6 +86,23 @@ public final class DispatchServer extends WebSocketServer implements IDispatcher
|
|||||||
this.sendMessage(socket, PacketIds.TokenValidateRsp, response);
|
this.sendMessage(socket, PacketIds.TokenValidateRsp, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches an account by its ID.
|
||||||
|
*
|
||||||
|
* @param socket The socket the packet was received from.
|
||||||
|
* @param object The packet data.
|
||||||
|
*/
|
||||||
|
private void fetchAccount(WebSocket socket, JsonElement object) {
|
||||||
|
var message = IDispatcher.decode(object);
|
||||||
|
var accountId = message.get("accountId").getAsString();
|
||||||
|
|
||||||
|
// Get the account from the database.
|
||||||
|
var account = DatabaseHelper.getAccountById(accountId);
|
||||||
|
// Send the account.
|
||||||
|
this.sendMessage(socket, PacketIds.GetAccountRsp,
|
||||||
|
JSON.toJsonTree(account));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts an encrypted message to all connected clients.
|
* Broadcasts an encrypted message to all connected clients.
|
||||||
*
|
*
|
||||||
|
@ -1,21 +1,25 @@
|
|||||||
package emu.grasscutter.server.dispatch;
|
package emu.grasscutter.server.dispatch;
|
||||||
|
|
||||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import emu.grasscutter.utils.Crypto;
|
import emu.grasscutter.utils.Crypto;
|
||||||
import emu.grasscutter.utils.JsonAdapters.ByteArrayAdapter;
|
import emu.grasscutter.utils.JsonAdapters.ByteArrayAdapter;
|
||||||
|
import org.java_websocket.WebSocket;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import org.java_websocket.WebSocket;
|
import java.util.function.Function;
|
||||||
import org.slf4j.Logger;
|
|
||||||
|
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||||
|
|
||||||
public interface IDispatcher {
|
public interface IDispatcher {
|
||||||
Gson JSON =
|
Gson JSON =
|
||||||
@ -24,6 +28,9 @@ public interface IDispatcher {
|
|||||||
.registerTypeAdapter(byte[].class, new ByteArrayAdapter())
|
.registerTypeAdapter(byte[].class, new ByteArrayAdapter())
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
|
Function<JsonElement, JsonObject> DEFAULT_PARSER = (packet) ->
|
||||||
|
IDispatcher.decode(packet, JsonObject.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes an escaped JSON message.
|
* Decodes an escaped JSON message.
|
||||||
*
|
*
|
||||||
@ -61,6 +68,75 @@ public interface IDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a request from the other server to be fulfilled.
|
||||||
|
*
|
||||||
|
* @param request The request data.
|
||||||
|
* @param requestId The request packet ID.
|
||||||
|
* @param responseId the response packet ID.
|
||||||
|
* @param parser The parser for the response data.
|
||||||
|
* @return The fulfilled data, or null.
|
||||||
|
* @param <T> The type of data to be returned.
|
||||||
|
*/
|
||||||
|
default <T> T await(JsonObject request, int requestId, int responseId,
|
||||||
|
Function<JsonElement, T> parser) {
|
||||||
|
// Perform the setup for the request.
|
||||||
|
var future = this.async(request, requestId, responseId, parser);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to return the value.
|
||||||
|
return future.get(5L, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback for a packet to be received.
|
||||||
|
* Sends a packet with the provided request.
|
||||||
|
*
|
||||||
|
* @param request The request object.
|
||||||
|
* @param requestId The packet ID of the request packet.
|
||||||
|
* @param responseId The packet ID of the response packet.
|
||||||
|
* @return A promise containing the parsed JSON data.
|
||||||
|
*/
|
||||||
|
default CompletableFuture<JsonObject> async(JsonObject request, int requestId, int responseId) {
|
||||||
|
return this.async(request, requestId, responseId, DEFAULT_PARSER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback for a packet to be received.
|
||||||
|
* Sends a packet with the provided request.
|
||||||
|
*
|
||||||
|
* @param request The request object.
|
||||||
|
* @param requestId The packet ID of the request packet.
|
||||||
|
* @param responseId The packet ID of the response packet.
|
||||||
|
* @param parser The parser for the received data.
|
||||||
|
* @return A promise containing the parsed JSON data.
|
||||||
|
*/
|
||||||
|
default <T> CompletableFuture<T> async(
|
||||||
|
JsonObject request, int requestId, int responseId,
|
||||||
|
Function<JsonElement, T> parser
|
||||||
|
) {
|
||||||
|
// Create the future.
|
||||||
|
var future = new CompletableFuture<T>();
|
||||||
|
// Listen for the response.
|
||||||
|
this.registerCallback(responseId, packet ->
|
||||||
|
future.complete(parser.apply(packet)));
|
||||||
|
// Broadcast the packet.
|
||||||
|
this.sendMessage(requestId, request);
|
||||||
|
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internally used method to broadcast a packet.
|
||||||
|
*
|
||||||
|
* @param packetId The packet ID.
|
||||||
|
* @param message The packet data.
|
||||||
|
*/
|
||||||
|
void sendMessage(int packetId, Object message);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes a message from the client.
|
* Decodes a message from the client.
|
||||||
*
|
*
|
||||||
|
@ -11,4 +11,8 @@ public interface PacketIds {
|
|||||||
int GachaHistoryRsp = 5;
|
int GachaHistoryRsp = 5;
|
||||||
int GmTalkReq = PacketOpcodes.GmTalkReq;
|
int GmTalkReq = PacketOpcodes.GmTalkReq;
|
||||||
int GmTalkRsp = PacketOpcodes.GmTalkRsp;
|
int GmTalkRsp = PacketOpcodes.GmTalkRsp;
|
||||||
|
int GetAccountReq = 6;
|
||||||
|
int GetAccountRsp = 7;
|
||||||
|
int GetPlayerFieldsReq = 8;
|
||||||
|
int GetPlayerFieldsRsp = 9;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import emu.grasscutter.Grasscutter.ServerDebugMode;
|
|||||||
import emu.grasscutter.utils.FileUtils;
|
import emu.grasscutter.utils.FileUtils;
|
||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
import io.javalin.http.ContentType;
|
import io.javalin.http.ContentType;
|
||||||
|
import io.javalin.json.JavalinGson;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
import org.eclipse.jetty.server.ServerConnector;
|
import org.eclipse.jetty.server.ServerConnector;
|
||||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||||
@ -57,6 +58,9 @@ public final class HttpServer {
|
|||||||
if (DISPATCH_INFO.logRequests == ServerDebugMode.ALL)
|
if (DISPATCH_INFO.logRequests == ServerDebugMode.ALL)
|
||||||
config.plugins.enableDevLogging();
|
config.plugins.enableDevLogging();
|
||||||
|
|
||||||
|
// Set the JSON mapper.
|
||||||
|
config.jsonMapper(new JavalinGson());
|
||||||
|
|
||||||
// Static files should be added like this https://javalin.io/documentation#static-files
|
// Static files should be added like this https://javalin.io/documentation#static-files
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
package emu.grasscutter.server.http.documentation;
|
package emu.grasscutter.server.http.documentation;
|
||||||
|
|
||||||
import static emu.grasscutter.config.Configuration.HANDBOOK;
|
import emu.grasscutter.Grasscutter;
|
||||||
|
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
||||||
import emu.grasscutter.server.http.Router;
|
import emu.grasscutter.server.http.Router;
|
||||||
import emu.grasscutter.utils.DispatchUtils;
|
import emu.grasscutter.utils.DispatchUtils;
|
||||||
import emu.grasscutter.utils.FileUtils;
|
import emu.grasscutter.utils.FileUtils;
|
||||||
import emu.grasscutter.utils.objects.HandbookBody;
|
import emu.grasscutter.utils.objects.HandbookBody;
|
||||||
import emu.grasscutter.utils.objects.HandbookBody.Action;
|
import emu.grasscutter.utils.objects.HandbookBody.Action;
|
||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
|
import io.javalin.http.ContentType;
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
||||||
|
|
||||||
|
import static emu.grasscutter.config.Configuration.HANDBOOK;
|
||||||
|
|
||||||
/** Handles requests for the new GM Handbook. */
|
/** Handles requests for the new GM Handbook. */
|
||||||
public final class HandbookHandler implements Router {
|
public final class HandbookHandler implements Router {
|
||||||
private final byte[] handbook;
|
private final byte[] handbook;
|
||||||
@ -20,7 +23,7 @@ public final class HandbookHandler implements Router {
|
|||||||
* found.
|
* found.
|
||||||
*/
|
*/
|
||||||
public HandbookHandler() {
|
public HandbookHandler() {
|
||||||
this.handbook = FileUtils.readResource("/handbook.html");
|
this.handbook = FileUtils.readResource("/html/handbook.html");
|
||||||
this.serve = HANDBOOK.enable && this.handbook.length > 0;
|
this.serve = HANDBOOK.enable && this.handbook.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,6 +33,9 @@ public final class HandbookHandler implements Router {
|
|||||||
|
|
||||||
// The handbook content. (built from src/handbook)
|
// The handbook content. (built from src/handbook)
|
||||||
javalin.get("/handbook", this::serveHandbook);
|
javalin.get("/handbook", this::serveHandbook);
|
||||||
|
// The handbook authentication page.
|
||||||
|
javalin.get("/handbook/authenticate", this::authenticate);
|
||||||
|
javalin.post("/handbook/authenticate", this::performAuthentication);
|
||||||
|
|
||||||
// Handbook control routes.
|
// Handbook control routes.
|
||||||
javalin.post("/handbook/avatar", this::grantAvatar);
|
javalin.post("/handbook/avatar", this::grantAvatar);
|
||||||
@ -59,6 +65,49 @@ public final class HandbookHandler implements Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves the handbook authentication page.
|
||||||
|
*
|
||||||
|
* @route GET /handbook/authenticate
|
||||||
|
* @param ctx The Javalin request context.
|
||||||
|
*/
|
||||||
|
private void authenticate(Context ctx) {
|
||||||
|
if (!this.serve) {
|
||||||
|
ctx.status(500).result("Handbook not found.");
|
||||||
|
} else {
|
||||||
|
// Pass the request to the authenticator.
|
||||||
|
Grasscutter.getAuthenticationSystem()
|
||||||
|
.getHandbookAuthenticator().presentPage(
|
||||||
|
AuthenticationRequest.builder().context(ctx).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs authentication for the handbook.
|
||||||
|
*
|
||||||
|
* @route POST /handbook/authenticate
|
||||||
|
* @param ctx The Javalin request context.
|
||||||
|
*/
|
||||||
|
private void performAuthentication(Context ctx) {
|
||||||
|
if (!this.serve) {
|
||||||
|
ctx.status(500).result("Handbook not found.");
|
||||||
|
} else {
|
||||||
|
// Pass the request to the authenticator.
|
||||||
|
var result = Grasscutter.getAuthenticationSystem()
|
||||||
|
.getHandbookAuthenticator().authenticate(
|
||||||
|
AuthenticationRequest.builder().context(ctx).build());
|
||||||
|
if (result == null) {
|
||||||
|
ctx.status(500).result("Authentication failed.");
|
||||||
|
} else {
|
||||||
|
ctx
|
||||||
|
.status(result.getStatus())
|
||||||
|
.result(result.getBody())
|
||||||
|
.contentType(result.getBody().contains("html") ?
|
||||||
|
ContentType.TEXT_HTML : ContentType.TEXT_PLAIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grants the avatar to the user.
|
* Grants the avatar to the user.
|
||||||
*
|
*
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package emu.grasscutter.utils;
|
package emu.grasscutter.utils;
|
||||||
|
|
||||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
import com.google.gson.JsonNull;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import emu.grasscutter.Grasscutter;
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
||||||
@ -14,10 +13,16 @@ import emu.grasscutter.server.http.handlers.GachaHandler;
|
|||||||
import emu.grasscutter.server.http.objects.LoginTokenRequestJson;
|
import emu.grasscutter.server.http.objects.LoginTokenRequestJson;
|
||||||
import emu.grasscutter.utils.objects.HandbookBody;
|
import emu.grasscutter.utils.objects.HandbookBody;
|
||||||
import emu.grasscutter.utils.objects.HandbookBody.*;
|
import emu.grasscutter.utils.objects.HandbookBody.*;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||||
|
|
||||||
public interface DispatchUtils {
|
public interface DispatchUtils {
|
||||||
/** HTTP client used for dispatch queries. */
|
/** HTTP client used for dispatch queries. */
|
||||||
@ -62,6 +67,132 @@ public interface DispatchUtils {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the session key for the specified player ID.
|
||||||
|
*
|
||||||
|
* @param playerId The player ID.
|
||||||
|
* @return The session key.
|
||||||
|
*/
|
||||||
|
@Nullable static String fetchSessionKey(int playerId) {
|
||||||
|
return switch (Grasscutter.getRunMode()) {
|
||||||
|
case GAME_ONLY -> {
|
||||||
|
// Fetch the player from the game server.
|
||||||
|
var player = DatabaseHelper.getPlayerByUid(playerId);
|
||||||
|
if (player == null) yield null;
|
||||||
|
|
||||||
|
// Fetch the account from the dispatch server.
|
||||||
|
var accountId = player.getAccountId();
|
||||||
|
var account = DispatchUtils.getAccountById(accountId);
|
||||||
|
|
||||||
|
// Return the session key.
|
||||||
|
yield account == null ? null : account.getSessionKey();
|
||||||
|
}
|
||||||
|
case DISPATCH_ONLY -> {
|
||||||
|
// Fetch the player's account ID from the game server.
|
||||||
|
var playerFields = DispatchUtils.getPlayerFields(playerId, "accountId");
|
||||||
|
if (playerFields == null) yield null;
|
||||||
|
|
||||||
|
// Get the account ID.
|
||||||
|
var accountId = playerFields.get("accountId").getAsString();
|
||||||
|
if (accountId == null) yield null;
|
||||||
|
|
||||||
|
// Fetch the account from the dispatch server.
|
||||||
|
var account = DatabaseHelper.getAccountById(accountId);
|
||||||
|
// Return the session key.
|
||||||
|
yield account == null ? null : account.getSessionKey();
|
||||||
|
}
|
||||||
|
case HYBRID -> {
|
||||||
|
// Fetch the player from the game server.
|
||||||
|
var player = DatabaseHelper.getPlayerByUid(playerId);
|
||||||
|
if (player == null) yield null;
|
||||||
|
|
||||||
|
// Fetch the account from the database.
|
||||||
|
var account = player.getAccount();
|
||||||
|
// Return the session key.
|
||||||
|
yield account == null ? null : account.getSessionKey();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches an account by its ID.
|
||||||
|
*
|
||||||
|
* @param accountId The account ID.
|
||||||
|
* @return The account.
|
||||||
|
*/
|
||||||
|
@Nullable static Account getAccountById(String accountId) {
|
||||||
|
return switch (Grasscutter.getRunMode()) {
|
||||||
|
case GAME_ONLY -> {
|
||||||
|
// Create a request for account information.
|
||||||
|
var request = new JsonObject();
|
||||||
|
request.addProperty("accountId", accountId);
|
||||||
|
|
||||||
|
// Wait for the request to complete.
|
||||||
|
yield Grasscutter.getGameServer().getDispatchClient()
|
||||||
|
.await(request, PacketIds.GetAccountReq, PacketIds.GetAccountRsp,
|
||||||
|
packet -> IDispatcher.decode(packet, Account.class));
|
||||||
|
}
|
||||||
|
case HYBRID, DISPATCH_ONLY ->
|
||||||
|
DatabaseHelper.getAccountById(accountId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the values of fields for a player.
|
||||||
|
*
|
||||||
|
* @param playerId The player's ID.
|
||||||
|
* @param fields The fields to fetch.
|
||||||
|
* @return An object holding the field values.
|
||||||
|
*/
|
||||||
|
@Nullable static JsonObject getPlayerFields(int playerId, String... fields) {
|
||||||
|
return switch (Grasscutter.getRunMode()) {
|
||||||
|
case DISPATCH_ONLY -> {
|
||||||
|
// Create a request for player fields.
|
||||||
|
var request = new JsonObject();
|
||||||
|
request.addProperty("playerId", playerId);
|
||||||
|
request.add("fields", IDispatcher.JSON.toJsonTree(fields));
|
||||||
|
|
||||||
|
// Wait for the request to complete.
|
||||||
|
yield Grasscutter.getDispatchServer()
|
||||||
|
.await(request, PacketIds.GetPlayerFieldsReq, PacketIds.GetPlayerFieldsRsp,
|
||||||
|
IDispatcher.DEFAULT_PARSER);
|
||||||
|
}
|
||||||
|
case HYBRID, GAME_ONLY -> {
|
||||||
|
// Get the player by ID.
|
||||||
|
var player = Grasscutter.getGameServer()
|
||||||
|
.getPlayerByUid(playerId, true);
|
||||||
|
if (player == null) yield null;
|
||||||
|
|
||||||
|
// Prepare field properties.
|
||||||
|
var fieldValues = new JsonObject();
|
||||||
|
var fieldMap = new HashMap<String, Field>();
|
||||||
|
Arrays.stream(player.getClass().getDeclaredFields())
|
||||||
|
.forEach(field -> fieldMap.put(field.getName(), field));
|
||||||
|
|
||||||
|
// Find the values of all requested fields.
|
||||||
|
for (var fieldName : fields) {
|
||||||
|
try {
|
||||||
|
var field = fieldMap.get(fieldName);
|
||||||
|
if (field == null)
|
||||||
|
fieldValues.add(fieldName, JsonNull.INSTANCE);
|
||||||
|
else {
|
||||||
|
var wasAccessible = field.canAccess(player);
|
||||||
|
field.setAccessible(true);
|
||||||
|
fieldValues.add(fieldName,
|
||||||
|
IDispatcher.JSON.toJsonTree(field.get(player)));
|
||||||
|
field.setAccessible(wasAccessible);
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
exception.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the values.
|
||||||
|
yield fieldValues;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the gacha history for the specified account.
|
* Fetches the gacha history for the specified account.
|
||||||
*
|
*
|
||||||
|
@ -23,6 +23,7 @@ public interface HandbookBody {
|
|||||||
@Getter
|
@Getter
|
||||||
class GrantAvatar {
|
class GrantAvatar {
|
||||||
private String player; // Parse into online player ID.
|
private String player; // Parse into online player ID.
|
||||||
|
private String playerToken; // Parse into session token.
|
||||||
private String avatar; // Parse into avatar ID.
|
private String avatar; // Parse into avatar ID.
|
||||||
|
|
||||||
private int level = 90; // Range between 1 - 90.
|
private int level = 90; // Range between 1 - 90.
|
||||||
@ -33,6 +34,7 @@ public interface HandbookBody {
|
|||||||
@Getter
|
@Getter
|
||||||
class GiveItem {
|
class GiveItem {
|
||||||
private String player; // Parse into online player ID.
|
private String player; // Parse into online player ID.
|
||||||
|
private String playerToken; // Parse into session token.
|
||||||
private String item; // Parse into item ID.
|
private String item; // Parse into item ID.
|
||||||
|
|
||||||
private long amount = 1; // Range between 1 - Long.MAX_VALUE.
|
private long amount = 1; // Range between 1 - Long.MAX_VALUE.
|
||||||
@ -41,12 +43,14 @@ public interface HandbookBody {
|
|||||||
@Getter
|
@Getter
|
||||||
class TeleportTo {
|
class TeleportTo {
|
||||||
private String player; // Parse into online player ID.
|
private String player; // Parse into online player ID.
|
||||||
|
private String playerToken; // Parse into session token.
|
||||||
private String scene; // Parse into a scene ID.
|
private String scene; // Parse into a scene ID.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
class SpawnEntity {
|
class SpawnEntity {
|
||||||
private String player; // Parse into online player ID.
|
private String player; // Parse into online player ID.
|
||||||
|
private String playerToken; // Parse into session token.
|
||||||
private String entity; // Parse into entity ID.
|
private String entity; // Parse into entity ID.
|
||||||
|
|
||||||
private long amount = 1; // Range between 1 - Long.MAX_VALUE.
|
private long amount = 1; // Range between 1 - Long.MAX_VALUE.
|
||||||
|
46
src/main/resources/html/handbook_auth.html
Normal file
46
src/main/resources/html/handbook_auth.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title>Handbook Authentication</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script type="application/javascript">
|
||||||
|
if ("{{VALUE}}" === "true") {
|
||||||
|
parent.postMessage({
|
||||||
|
type: "handbook-auth",
|
||||||
|
token: "{{SESSION_TOKEN}}",
|
||||||
|
uid: "{{PLAYER_ID}}"
|
||||||
|
}, "*");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>Input your Player UID here.</p>
|
||||||
|
<form method="post">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
name="playerid"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="submit" />
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user