refactor arch
This commit is contained in:
parent
a9238b6731
commit
76ec291f47
0
.cache/.gitkeep
Normal file
0
.cache/.gitkeep
Normal file
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,10 +1,18 @@
|
||||
# Node.js
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# Config
|
||||
config.json
|
||||
|
||||
# Credential
|
||||
credential.json
|
||||
session.json
|
||||
credentials.json
|
||||
|
||||
# Session Cache Folder
|
||||
.cache/*
|
||||
!.cache/.gitkeep
|
||||
|
25
README.md
25
README.md
@ -1,19 +1,34 @@
|
||||
# Compass Bot
|
||||
|
||||
## 登录
|
||||
## 安装
|
||||
|
||||
运行下面命令安装 Node.JS 的依赖包:
|
||||
|
||||
```sh
|
||||
npm i
|
||||
```
|
||||
|
||||
## 配置认证数据
|
||||
|
||||
在项目根目录下创建 `credentials.json`,包含如下内容:
|
||||
|
||||
```json
|
||||
{
|
||||
"endpoint": "Yggdrasil API的调用点",
|
||||
"username": "用户名",
|
||||
"password": "密码"
|
||||
"endpoint": "Yggdrasil API 调用点",
|
||||
"endpoint_auth": "Yggdrasil API 用户服务器调用点(可选)",
|
||||
"endpoint_session": "Yggdrasil API 会话服务器调用点(可选)",
|
||||
"accounts": [{
|
||||
"handle": "登录名(在支持非邮箱登录的 Yggdrasil 服务器上可选)",
|
||||
"password": "密码",
|
||||
"profiles": ["角色名1", "角色名2"]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
运行下面命令启动:
|
||||
|
||||
```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
184
auth/authlib.mjs
Normal 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
|
||||
};
|
109
authlib.js
109
authlib.js
@ -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 };
|
75
index.js
75
index.js
@ -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
69
index.mjs
Normal 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);
|
||||
});
|
12
package.json
12
package.json
@ -1,10 +1,22 @@
|
||||
{
|
||||
"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": {
|
||||
"axios": "^1.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"mineflayer": "^4.14.0",
|
||||
"mineflayer-event-promise": "file:plugin/event-promise",
|
||||
"mineflayer-pathfinder": "^2.4.5",
|
||||
"prismarine-viewer": "^1.25.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
|
11
plugin/event-promise/index.mjs
Normal file
11
plugin/event-promise/index.mjs
Normal 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);
|
||||
});
|
||||
};
|
||||
}
|
9
plugin/event-promise/package.json
Normal file
9
plugin/event-promise/package.json
Normal 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"
|
||||
}
|
||||
}
|
22
utils.js
22
utils.js
@ -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
29
utils/index.mjs
Normal 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 };
|
Loading…
x
Reference in New Issue
Block a user