refactor arch

This commit is contained in:
方而静 2023-10-29 20:44:36 +08:00
parent a9238b6731
commit 76ec291f47
Signed by: szTom
GPG Key ID: 072D999D60C6473C
12 changed files with 343 additions and 212 deletions

0
.cache/.gitkeep Normal file
View File

10
.gitignore vendored
View File

@ -1,10 +1,18 @@
# Node.js # Node.js
node_modules/ node_modules/
package-lock.json
# Editor # Editor
.vscode/ .vscode/
*.swp *.swp
# Config
config.json
# Credential # Credential
credential.json credential.json
session.json credentials.json
# Session Cache Folder
.cache/*
!.cache/.gitkeep

View File

@ -1,19 +1,34 @@
# Compass Bot # Compass Bot
## 登录 ## 安装
运行下面命令安装 Node.JS 的依赖包:
```sh
npm i
```
## 配置认证数据
在项目根目录下创建 `credentials.json`,包含如下内容: 在项目根目录下创建 `credentials.json`,包含如下内容:
```json ```json
{ {
"endpoint": "Yggdrasil API的调用点", "endpoint": "Yggdrasil API 调用点",
"username": "用户名", "endpoint_auth": "Yggdrasil API 用户服务器调用点(可选)",
"password": "密码" "endpoint_session": "Yggdrasil API 会话服务器调用点(可选)",
"accounts": [{
"handle": "登录名(在支持非邮箱登录的 Yggdrasil 服务器上可选)",
"password": "密码",
"profiles": ["角色名1", "角色名2"]
}]
} }
``` ```
## 运行 ## 运行
运行下面命令启动:
```sh ```sh
node index.js <server_ip:server_port> --version=<minecraft_version> --owner=<bot_owner> node index.mjs profile@host:port
``` ```

184
auth/authlib.mjs Normal file
View File

@ -0,0 +1,184 @@
import axios from 'axios';
import { readJsonFile, writeJsonFile } from '../utils/index.mjs';
export class NoCredentialError extends Error {
constructor(profile) {
super(`No credentials configured for profile ${profile}.`);
}
};
export class ProfileNotSelectedError extends Error {
constructor() {
super('No profile was selected.');
}
};
export class ProfileAlreadySelectedError extends Error {
constructor(profile) {
super(`Profile was already selected to ${profile}.`);
}
};
export class YggdrasilEndpoint {
constructor(endpoint, endpoint_auth, endpoint_session) {
this.endpoint = endpoint;
this.endpoint_auth = endpoint_auth || this.endpoint + '/authserver';
this.endpoint_session = endpoint_session || this.endpoint + '/sessionserver';
}
auth_url(path) {
return this.endpoint_auth + '/' + path;
}
};
export class Account {
constructor(handle, password) {
this.handle = handle;
this.password = password;
}
async auth(endpoint) {
let auth_info = (await axios.post(endpoint.auth_url('authenticate'), {
username: this.handle,
password: this.password,
})).data;
return new SessionCache(auth_info.accessToken, auth_info.clientToken
, auth_info.selectedProfile, auth_info.availableProfiles);
}
};
export class Credentials {
constructor(options) {
this.endpoint = new YggdrasilEndpoint(options.endpoint, options.endpoint_auth, options.endpoint_session);
this.profiles = new Map();
this.accounts = [];
for (let account_option of options.accounts) {
let account = new Account(account_option.handle || account_option.profiles[0]
, account_option.password);
this.accounts.push(account);
for (let profile of account_option.profiles) {
this.profiles.set(profile, account);
}
}
}
credentialOf(profile) {
return this.profiles.get(profile);
}
static async fromFile(path) {
let options = await readJsonFile(path);
return new Credentials(options);
}
async authProfile(profile) {
let sc = await SessionCache.load(profile);
if (sc != null && !(await sc.validate(this.endpoint))) { sc = null; }
if (sc == null) {
let account = this.credentialOf(profile);
if (account == null) { throw new NoCredentialError(profile); }
sc = await account.auth(this.endpoint);
await sc.selectProfile(this.endpoint, profile);
await sc.store();
}
return sc;
}
};
export class SessionCache {
constructor(accessToken, clientToken, selectedProfile, availableProfiles) {
this.accessToken = accessToken;
this.clientToken = clientToken;
this.selectedProfile = selectedProfile;
this.availableProfiles = availableProfiles;
}
name() {
return this.selectedProfile.name;
}
session() {
return {
accessToken: this.accessToken,
clientToken: this.clientToken,
selectedProfile: this.selectedProfile,
};
}
async store() {
if (this.selectedProfile == null) { throw new ProfileNotSelectedError(); }
await writeJsonFile(`.cache/${this.name()}.json`, this.session());
}
static async load(profile) {
let cache_data = await readJsonFile(`.cache/${profile}.json`);
if (cache_data == null) { return null; }
return new SessionCache(cache_data.accessToken, cache_data.clientToken, cache_data.selectedProfile);
}
async validate(endpoint) {
let vres = await axios.post(endpoint.auth_url('validate'), {
accessToken: this.accessToken,
clientToken: this.clientToken,
}, { validateStatus: (status) => (status === 403 || status === 204) });
return vres.status == 204;
}
async selectProfile(endpoint, profile) {
if (this.selectedProfile != null) {
if (this.name() != profile) {
throw new ProfileAlreadySelectedError(this.name());
} else { return; }
}
if (this.availableProfiles == null) {
throw new NoCredentialError(profile);
}
let profile_info = null;
for (let info of this.availableProfiles) {
if (info.name == profile) {
profile_info = info;
break;
}
}
if (profile_info == null) { throw new NoCredentialError(profile); }
let session_info = (await axios.post(endpoint.auth_url('refresh'), {
accessToken: this.accessToken,
clientToken: this.clientToken,
selectedProfile: profile_info,
})).data;
this.selectedProfile = session_info.selectedProfile;
this.accessToken = session_info.accessToken;
this.availableProfiles = null;
}
mineflayer(endpoint) {
if (this.selectedProfile == null) { throw new ProfileNotSelectedError(); }
return {
username: this.name(),
authServer: endpoint.endpoint_auth,
sessionServer: endpoint.endpoint_session,
auth: (client, options) => {
client.username = this.name();
client.session = this.session();
options.accessToken = this.accessToken;
options.clientToken = this.clientToken;
options.haveCredentials = true;
client.emit('session', client.session);
options.connect(client);
},
};
}
};
export default {
NoCredentialError, ProfileNotSelectedError, ProfileAlreadySelectedError
, YggdrasilEndpoint, Account, Credentials, SessionCache
};

View File

@ -1,109 +0,0 @@
const axios = require('axios');
const { readJsonFile, writeJsonFile } = require('./utils');
async function readCredentials() {
let credentials = await readJsonFile('credential.json');
if (!credentials.endpoint_auth) {
credentials.endpoint_auth = credentials.endpoint + '/authserver';
}
if (!credentials.endpoint_session) {
credentials.endpoint_session = credentials.endpoint + '/sessionserver';
}
if (!credentials.handle) {
credentials.handle = credentials.username;
}
return credentials;
}
async function readSessionCache() {
return readJsonFile('session.json');
}
async function storeSessionCache(data) {
return writeJsonFile('session.json', data);
}
async function yggdrailLogin(credentials) {
let account_info = (await axios.post(`${credentials.endpoint_auth}/authenticate`, {
username: credentials.handle,
password: credentials.password,
})).data;
let profile_info = null;
for (let info of account_info.availableProfiles) {
if (info.name == credentials.username) {
profile_info = info;
break;
}
}
let session_info = (await axios.post(`${credentials.endpoint_auth}/refresh`, {
accessToken: account_info.accessToken,
clientToken: account_info.clientToken,
selectedProfile: profile_info,
})).data;
return {
accessToken: session_info.accessToken,
clientToken: session_info.clientToken,
selectedProfile: session_info.selectedProfile,
};
}
async function yggdrasilAuth(credentials) {
if (credentials == null) {
credentials = await readCredentials();
}
let cache = await readSessionCache();
if (cache != null && cache.selectedProfile?.name != credentials.username) {
cache = null;
}
if (cache != null) {
let vres = await axios.post(`${credentials.endpoint_auth}/validate`, {
accessToken: cache.accessToken,
clientToken: cache.clientToken,
}, {
validateStatus: function (status) {
return status === 403 || status === 204;
}
});
if (vres.status == 403) {
cache = null;
}
}
let session_info = cache;
if (session_info == null) {
session_info = await yggdrailLogin(credentials);
storeSessionCache(session_info);
}
return session_info;
}
async function mineflayer() {
let credentials = await readCredentials();
let session_info = await yggdrasilAuth(credentials);
return {
username: credentials.username,
authServer: credentials.endpoint_auth,
sessionServer: credentials.endpoint_session,
auth: (client, options) => {
client.username = credentials.username;
client.session = session_info;
options.accessToken = session_info.accessToken;
options.clientToken = session_info.clientToken;
options.haveCredentials = true;
client.emit('session', session_info);
options.connect(client);
},
};
}
module.exports = { readCredentials, yggdrasilAuth, mineflayer };

View File

@ -1,75 +0,0 @@
const authlib = require('./authlib');
const mineflayer = require('mineflayer');
const { pathfinder, Movements } = require('mineflayer-pathfinder');
const { GoalNear } = require('mineflayer-pathfinder').goals
const repl = require('repl');
const domain = require('domain');
const yargs = require('yargs');
const { parseURL } = require('./utils');
const args = yargs.option('protocal', {
description: 'minecraft server version',
type: 'string',
requiresArg: false,
}).option('owner', {
description: 'bot owner name',
type: 'string',
requiresArg: false
}).help().alias('help', 'h').argv;
const [host, port] = parseURL(args._[0]);
const version = args.protocal;
const repl_domain = domain.create();
repl_domain.on('error', (err) => {
console.error('Caught error:', err);
});
async function main() {
let authinfo = await authlib.mineflayer();
let bot = mineflayer.createBot({
host, port, version,
...authinfo,
});
bot.loadPlugin(pathfinder);
bot.on('kicked', console.warn);
bot.on('error', console.warn);
bot.once('spawn', () => {
repl_domain.run(() => {
let r = repl.start({
prompt: "bot > ",
input: process.stdin,
output: process.stdout,
color: true,
terminal: true,
});
const deafult_tactic = new Movements(bot);
const peaceful_tactic = new Movements(bot);
peaceful_tactic.canDig = false;
peaceful_tactic.scafoldingBlocks = [];
r.context.deafult_tactic = deafult_tactic;
r.context.peaceful_tactic = peaceful_tactic;
r.context.bot = bot;
r.context.authinfo = authinfo;
r.context.Movements = Movements;
r.context.GoalNear = GoalNear;
r.context.mineflayer = mineflayer;
r.context.owner = () => {
if (!args.owner) {
return null;
}
return bot.players[args.owner];
};
r.context.control = require('./control');
r.context.Vec3 = require('vec3').Vec3;
});
});
}
main();

69
index.mjs Normal file
View File

@ -0,0 +1,69 @@
import authlib from './auth/authlib.mjs';
import mineflayer from 'mineflayer';
import yargs from 'yargs';
import { parseLogin, waitEvent } from './utils/index.mjs';
import repl from 'node:repl';
import vm from 'node:vm';
async function main() {
const args = yargs((await import('yargs/helpers')).hideBin(process.argv))
.option('protocal', {
description: 'Minecraft server version',
type: 'string',
requiresArg: false,
}).option('owner', {
description: 'Bot\'s owner name.',
type: 'string',
requiresArg: false,
}).option('no-repl', {
description: 'Disable bot REPL control.',
type: "boolean",
}).option('credentials-lib', {
description: 'Credentials\' library file.',
type: "string",
default: "credentials.json",
}).usage('Uasge: profile@host:port').help().alias('help', 'h').argv;
let login_info = args._[0];
if (login_info == null) { return; }
const [name, host, port] = parseLogin(login_info);
const credential_info = await authlib.Credentials.fromFile(args.credentialsLib);
if (credential_info == null) {
throw new Error(`Cannot load credential ${args.credentialsLib}`);
}
const session = await credential_info.authProfile(name);
const bot = mineflayer.createBot({
host, port, version: args.protocal,
...session.mineflayer(credential_info.endpoint)
});
await waitEvent(bot, 'inject_allowed');
bot.loadPlugin((await import('mineflayer-event-promise')).default);
await bot.waitEvent('spawn');
let context = vm.createContext();
context.bot = bot;
context.Vec3 = (await import('vec3')).Vec3;
context.mineflayer = mineflayer;
context.owner = () => {
if (!args.owner) { return null; }
return bot.players[args.owner];
};
if (!args.noRepl) {
let r = repl.start({
prompt: 'local > ',
input: process.stdin,
output: process.stdout,
color: true,
terminal: true,
ignoreUndefined: true,
});
r.context = context;
}
}
main().catch(err => {
console.error('Error: ', err);
process.exit(1);
});

View File

@ -1,10 +1,22 @@
{ {
"name": "compass-bot", "name": "compass-bot",
"description": "Create minecraft bot swarms and control them easily.",
"main": "index.mjs",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/szdytom/compass-bot.git"
},
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",
"debug": "^4.3.4",
"mineflayer": "^4.14.0", "mineflayer": "^4.14.0",
"mineflayer-event-promise": "file:plugin/event-promise",
"mineflayer-pathfinder": "^2.4.5", "mineflayer-pathfinder": "^2.4.5",
"prismarine-viewer": "^1.25.0", "prismarine-viewer": "^1.25.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"
},
"engines": {
"node": ">=20"
} }
} }

View File

@ -0,0 +1,11 @@
import debug from 'debug';
const log = debug('mineflayer-event-promise');
export default function inject(bot) {
debug('Injected!');
bot.waitEvent = (event) => {
return new Promise((resolve, _reject) => {
bot.once(event, resolve);
});
};
}

View File

@ -0,0 +1,9 @@
{
"name": "mineflayer-event-promise",
"description": "Promise-styled event system for mineflayer bot",
"type": "module",
"main": "index.mjs",
"dependencies": {
"debug": "^4.3.4"
}
}

View File

@ -1,22 +0,0 @@
const fs = require('fs/promises');
async function readJsonFile(path) {
try {
const data = await fs.readFile(path, 'utf8');
return JSON.parse(data);
} catch (error) {
return null;
}
}
function writeJsonFile(path, data) {
const json_string = JSON.stringify(data);
return fs.writeFile(path, json_string, 'utf8');
}
function parseURL(url) {
const [host, port] = url.split(':');
return [host, port ? parseInt(port) : undefined];
}
module.exports = { readJsonFile, writeJsonFile, parseURL };

29
utils/index.mjs Normal file
View File

@ -0,0 +1,29 @@
import fs from 'node:fs/promises';
export async function readJsonFile(path) {
try {
const data = await fs.readFile(path, 'utf8');
return JSON.parse(data);
} catch (error) {
return null;
}
}
export function writeJsonFile(path, data) {
const json_string = JSON.stringify(data);
return fs.writeFile(path, json_string, 'utf8');
}
export function parseLogin(url) {
const [profile_host, port] = url.split(':');
const [profile, host] = profile_host.split('@');
return [profile, host, port ? parseInt(port) : undefined];
}
export function waitEvent(em, event) {
return new Promise((resolve, _reject) => {
em.once(event, resolve);
});
}
export default { readJsonFile, writeJsonFile, parseLogin, waitEvent };