bedrock-portal 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +45 -0
- package/.github/workflows/publish.yml +32 -0
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/examples/events.js +31 -0
- package/index.d.ts +296 -0
- package/index.js +5 -0
- package/package.json +29 -0
- package/src/common/constants.js +29 -0
- package/src/index.js +246 -0
- package/src/modules.js +37 -0
- package/src/rest.js +93 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "eslint:recommended",
|
|
3
|
+
"env": {
|
|
4
|
+
"node": true,
|
|
5
|
+
"es2022": true
|
|
6
|
+
},
|
|
7
|
+
"rules": {
|
|
8
|
+
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
|
|
9
|
+
"comma-dangle": ["error", "always-multiline"],
|
|
10
|
+
"comma-spacing": "error",
|
|
11
|
+
"comma-style": "error",
|
|
12
|
+
"curly": ["error", "multi-line", "consistent"],
|
|
13
|
+
"dot-location": ["error", "property"],
|
|
14
|
+
"handle-callback-err": "off",
|
|
15
|
+
"indent": ["error", "tab", { "SwitchCase": 1 }],
|
|
16
|
+
"max-nested-callbacks": ["error", { "max": 4 }],
|
|
17
|
+
"max-statements-per-line": ["error", { "max": 2 }],
|
|
18
|
+
"no-console": "off",
|
|
19
|
+
"quote-props": ["error", "consistent-as-needed"],
|
|
20
|
+
"no-empty-function": "off",
|
|
21
|
+
"no-floating-decimal": "error",
|
|
22
|
+
"no-inline-comments": "off",
|
|
23
|
+
"no-lonely-if": "error",
|
|
24
|
+
"no-multi-spaces": "error",
|
|
25
|
+
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
|
|
26
|
+
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
|
|
27
|
+
"no-trailing-spaces": ["error"],
|
|
28
|
+
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
|
29
|
+
"no-var": "error",
|
|
30
|
+
"object-curly-spacing": ["error", "always"],
|
|
31
|
+
"prefer-const": "error",
|
|
32
|
+
"quotes": ["error", "single"],
|
|
33
|
+
"semi": ["error", "always"],
|
|
34
|
+
"space-before-blocks": "error",
|
|
35
|
+
"space-before-function-paren": ["error", {
|
|
36
|
+
"anonymous": "never",
|
|
37
|
+
"named": "never",
|
|
38
|
+
"asyncArrow": "always"
|
|
39
|
+
}],
|
|
40
|
+
"space-in-parens": "error",
|
|
41
|
+
"space-infix-ops": "error",
|
|
42
|
+
"space-unary-ops": "error",
|
|
43
|
+
"spaced-comment": "error"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: npm-publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
jobs:
|
|
7
|
+
npm-publish:
|
|
8
|
+
name: npm-publish
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- name: Checkout repository
|
|
12
|
+
uses: actions/checkout@master
|
|
13
|
+
- name: Set up Node.js
|
|
14
|
+
uses: actions/setup-node@master
|
|
15
|
+
with:
|
|
16
|
+
node-version: 14.0.0
|
|
17
|
+
- id: publish
|
|
18
|
+
uses: JS-DevTools/npm-publish@v1
|
|
19
|
+
with:
|
|
20
|
+
token: ${{ secrets.NPM_AUTH_TOKEN }}
|
|
21
|
+
- name: Create Release
|
|
22
|
+
if: steps.publish.outputs.type != 'none'
|
|
23
|
+
id: create_release
|
|
24
|
+
uses: actions/create-release@v1
|
|
25
|
+
env:
|
|
26
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
27
|
+
with:
|
|
28
|
+
tag_name: ${{ steps.publish.outputs.version }}
|
|
29
|
+
release_name: Release ${{ steps.publish.outputs.version }}
|
|
30
|
+
body: ${{ steps.publish.outputs.version }}
|
|
31
|
+
draft: false
|
|
32
|
+
prerelease: false
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# BedrockPortal
|
|
2
|
+
[](http://npmjs.com/package/bedrock-portal)
|
|
3
|
+
[](https://discord.gg/KTyd9HWuBD)
|
|
4
|
+
|
|
5
|
+
Handles and creates a Minecraft Bedrock game session which will redirect players to the specified server
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
```shell
|
|
9
|
+
npm install bedrock-portal
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Warning
|
|
13
|
+
This package is not meant to be used with your main account. It is meant to be used with alt accounts. If you use this package with your main account, you may be banned from the XSAPI. This package is not affiliated with Mojang or Microsoft.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### BedrockPortal(authflow)
|
|
18
|
+
**Parameters**
|
|
19
|
+
- authflow - Takes an **Authflow** instance from [prismarine-auth](https://github.com/PrismarineJS/prismarine-auth), you can see the documentation for this [here.](https://github.com/PrismarineJS/prismarine-auth#authflow)
|
|
20
|
+
- options
|
|
21
|
+
- **ip** - The IP address of the server to redirect players to
|
|
22
|
+
- **port** - The port of the server to redirect players to
|
|
23
|
+
- **disableAltCheck** - Disables the alt check.
|
|
24
|
+
- **joinability** - The joinability of the session, Can be 'friends_of_friends', 'friends_only', 'invite_only'
|
|
25
|
+
- **world** - The world config to use for the session. Changes the session card which is displayed in the Minecraft client.
|
|
26
|
+
- **hostName** - string
|
|
27
|
+
- **name** - string
|
|
28
|
+
- **version** - string
|
|
29
|
+
- **memberCount** - number
|
|
30
|
+
- **maxMemberCount** - number
|
|
31
|
+
- **modules** - An object containing modules to enable. See [Modules](#modules) for more information.
|
|
32
|
+
|
|
33
|
+
### Create a session redirect to a server
|
|
34
|
+
```js
|
|
35
|
+
const { BedrockPortal } = require('bedrock-portal');
|
|
36
|
+
const { Authflow, Titles } = require('prismarine-auth');
|
|
37
|
+
|
|
38
|
+
const main = async () => {
|
|
39
|
+
const auth = new Authflow('example', './', { authTitle: Titles.MinecraftNintendoSwitch, deviceType: 'Nintendo' });
|
|
40
|
+
|
|
41
|
+
const session = new BedrockPortal(auth, {
|
|
42
|
+
ip: 'geyserconnect.net', // The server IP & port to redirect players to
|
|
43
|
+
port: 19132,
|
|
44
|
+
joinability: 'friends_of_friends', // The joinability of the session. Can be 'friends_of_friends', 'friends_only', 'invite_only'
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await session.start();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
main();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Modules
|
|
54
|
+
|
|
55
|
+
Modules are used to extend the functionality of the BedrockPortal class. You can find a list of modules below.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
new BedrockPortal(auth, {
|
|
59
|
+
...,
|
|
60
|
+
modules: {
|
|
61
|
+
autoFriendAdd: true, // Automatically add friends to the session
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### autoFriendAdd
|
|
67
|
+
|
|
68
|
+
Automatically adds the account's followers as friends so the session can be joined by them.
|
|
69
|
+
* Add `autoFriendAdd: true` to the modules object in the BedrockPortal constructor.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Debugging
|
|
73
|
+
|
|
74
|
+
You can enable some debugging output using the `DEBUG` enviroment variable. Through node.js, you can add `process.env.DEBUG = 'bedrock-portal'` at the top of your code.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { BedrockPortal } = require('bedrock-portal');
|
|
2
|
+
const { Authflow, Titles } = require('prismarine-auth');
|
|
3
|
+
|
|
4
|
+
const main = async () => {
|
|
5
|
+
const auth = new Authflow('example', './', { authTitle: Titles.MinecraftNintendoSwitch, deviceType: 'Nintendo' });
|
|
6
|
+
|
|
7
|
+
const session = new BedrockPortal(auth, {
|
|
8
|
+
ip: 'geyserconnect.net',
|
|
9
|
+
port: 19132,
|
|
10
|
+
joinability: 'friends_of_friends',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
await session.start();
|
|
14
|
+
|
|
15
|
+
await session.invitePlayer('p3');
|
|
16
|
+
|
|
17
|
+
session.on('rtaEvent', (event) => {
|
|
18
|
+
console.log(event);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
session.on('playersAdded', (players) => {
|
|
22
|
+
console.log(players);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
session.on('playersRemoved', (players) => {
|
|
26
|
+
console.log(players);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
main();
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
declare module 'bedrock-portal' {
|
|
4
|
+
export class BedrockPortal extends EventEmitter {
|
|
5
|
+
/**
|
|
6
|
+
* Handles and creates a Minecraft Bedrock game session which will redirect players to the specified server.
|
|
7
|
+
* @param authflow An Authflow instance from [prismarine-auth](https://github.com/PrismarineJS/prismarine-auth).
|
|
8
|
+
* @param options Options for the BedrockPortal instance
|
|
9
|
+
*/
|
|
10
|
+
constructor(authflow: Authflow, options: BedrockPortalOptions = {})
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Starts the BedrockPortal instance.
|
|
14
|
+
*/
|
|
15
|
+
start(): Promise<SessionResponse>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ends the BedrockPortal instance.
|
|
19
|
+
*/
|
|
20
|
+
end(): Promise<void>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns the current members in the session.
|
|
24
|
+
*/
|
|
25
|
+
getSessionMembers(): Promise<SessionMember[]>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Invites a player to the BedrockPortal instance.
|
|
29
|
+
* @param identifyer The player's gamertag or XUID.
|
|
30
|
+
*/
|
|
31
|
+
invitePlayer(identifyer: string): Promise<void>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Updates the current member count which is displayed in the Minecraft client.
|
|
35
|
+
* @param count The new member count.
|
|
36
|
+
*/
|
|
37
|
+
updateMemberCount(count: number): Promise<void>
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Updates the connectionId of the BedrockPortal instance. Used internally on reconnect.
|
|
41
|
+
* @param connectionId The new connectionId.
|
|
42
|
+
*/
|
|
43
|
+
updateConnection(connectionId: string): Promise<void>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Gets the current session of the BedrockPortal instance.
|
|
47
|
+
*/
|
|
48
|
+
getSession(): Promise<SessionResponse>
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Updates the current session of the BedrockPortal instance with the specified payload.
|
|
52
|
+
* @param payload The payload to update the session with.
|
|
53
|
+
*/
|
|
54
|
+
updateSession(payload: SessionRequest): Promise<void>
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Used to set the session for the user's current activity and to invite session members if required
|
|
58
|
+
* @param payload The payload
|
|
59
|
+
*/
|
|
60
|
+
updateHandle(payload: HandlePayload): Promise<void>
|
|
61
|
+
|
|
62
|
+
on(event: 'playersAdded', listener: (data: SessionMember[]) => void): this;
|
|
63
|
+
on(event: 'playersRemoved', listener: (data: SessionMember[]) => void): this;
|
|
64
|
+
on(event: 'rtaEvent', listener: (data: RTAEvent) => void): this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RTAEvent {
|
|
68
|
+
type: number;
|
|
69
|
+
subId: number;
|
|
70
|
+
data: {
|
|
71
|
+
ncid: string,
|
|
72
|
+
shoulderTaps: {
|
|
73
|
+
timestamp: string,
|
|
74
|
+
subscription: string,
|
|
75
|
+
resourceType: string,
|
|
76
|
+
resource: string,
|
|
77
|
+
branch: string,
|
|
78
|
+
changeNumber: number
|
|
79
|
+
}[]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SessionMember {
|
|
84
|
+
profile: {
|
|
85
|
+
xuid: string
|
|
86
|
+
gamertag: string
|
|
87
|
+
avatar: string
|
|
88
|
+
gamerscore: string
|
|
89
|
+
colour: {
|
|
90
|
+
primaryColor: string
|
|
91
|
+
secondaryColor: string
|
|
92
|
+
tertiaryColor: string
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
session: {
|
|
96
|
+
titleId: string
|
|
97
|
+
joinTime: string
|
|
98
|
+
index: string
|
|
99
|
+
connectionId: string
|
|
100
|
+
subscriptionId: string
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface BedrockPortalOptions {
|
|
105
|
+
ip: string
|
|
106
|
+
port: number
|
|
107
|
+
disableAltCheck?: boolean
|
|
108
|
+
joinability?: 'invite_only' | 'friends_only' | 'friends_of_friends'
|
|
109
|
+
world?: {
|
|
110
|
+
hostName?: string
|
|
111
|
+
name?: string
|
|
112
|
+
version?: string
|
|
113
|
+
memberCount?: number
|
|
114
|
+
maxMemberCount?: number
|
|
115
|
+
}
|
|
116
|
+
modules?: {
|
|
117
|
+
autoFriendAdd?: boolean
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface HandlePayload {
|
|
122
|
+
version: number,
|
|
123
|
+
type: 'activity' | 'invite',
|
|
124
|
+
sessionRef: {
|
|
125
|
+
scid: string,
|
|
126
|
+
templateName: string,
|
|
127
|
+
name: string,
|
|
128
|
+
},
|
|
129
|
+
inviteAttributes?: {
|
|
130
|
+
titleId: string
|
|
131
|
+
context: string
|
|
132
|
+
contextString: string
|
|
133
|
+
senderString: string
|
|
134
|
+
},
|
|
135
|
+
invitedXuid?: string,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface SessionResponse {
|
|
139
|
+
membersInfo: {
|
|
140
|
+
first: number,
|
|
141
|
+
next: number,
|
|
142
|
+
count: number,
|
|
143
|
+
accepted: number,
|
|
144
|
+
active: number
|
|
145
|
+
},
|
|
146
|
+
constants: {
|
|
147
|
+
system: {
|
|
148
|
+
readyRemovalTimeout: number,
|
|
149
|
+
reservedRemovalTimeout: number,
|
|
150
|
+
sessionEmptyTimeout: number,
|
|
151
|
+
inactiveRemovalTimeout: number,
|
|
152
|
+
version: number,
|
|
153
|
+
maxMembersCount: number,
|
|
154
|
+
visibility: string,
|
|
155
|
+
capabilities: {
|
|
156
|
+
connectivity: boolean,
|
|
157
|
+
connectionRequiredForActiveMembers: boolean,
|
|
158
|
+
gameplay: boolean,
|
|
159
|
+
crossPlay: boolean,
|
|
160
|
+
userAuthorizationStyle: boolean
|
|
161
|
+
},
|
|
162
|
+
inviteProtocol: string,
|
|
163
|
+
memberInitialization: {
|
|
164
|
+
membersNeededToStart: number,
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
custom: {}
|
|
168
|
+
},
|
|
169
|
+
properties: {
|
|
170
|
+
system: {
|
|
171
|
+
joinRestriction: 'followed' | 'local',
|
|
172
|
+
readRestriction: string,
|
|
173
|
+
turn: []
|
|
174
|
+
},
|
|
175
|
+
custom: {
|
|
176
|
+
Joinability: string,
|
|
177
|
+
hostName: string,
|
|
178
|
+
ownerId: string,
|
|
179
|
+
rakNetGUID: string,
|
|
180
|
+
version: string,
|
|
181
|
+
worldName: string,
|
|
182
|
+
worldType: string,
|
|
183
|
+
protocol: number,
|
|
184
|
+
MemberCount: number,
|
|
185
|
+
MaxMemberCount: number,
|
|
186
|
+
BroadcastSetting: number,
|
|
187
|
+
UsesWebSocketsWebRTCSignaling: boolean,
|
|
188
|
+
UsesMPSDWebRTCSignaling: boolean,
|
|
189
|
+
netherNetEnabled: boolean,
|
|
190
|
+
OnlineCrossPlatformGame: boolean,
|
|
191
|
+
CrossPlayDisabled: boolean,
|
|
192
|
+
TitleId: number,
|
|
193
|
+
SupportedConnections: SessionConnection[],
|
|
194
|
+
levelId: string,
|
|
195
|
+
LanGame: boolean
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
servers: {},
|
|
199
|
+
|
|
200
|
+
// all objects withing this object have the same structure
|
|
201
|
+
members: {
|
|
202
|
+
[index: number]: SessionMember
|
|
203
|
+
}
|
|
204
|
+
correlationId: string,
|
|
205
|
+
contractVersion: number,
|
|
206
|
+
branch: string,
|
|
207
|
+
changeNumber: number,
|
|
208
|
+
startTime: string
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface SessionRequest {
|
|
212
|
+
properties: {
|
|
213
|
+
system: {
|
|
214
|
+
joinRestriction: 'followed' | 'local',
|
|
215
|
+
readRestriction: string,
|
|
216
|
+
closed: boolean,
|
|
217
|
+
},
|
|
218
|
+
custom: {
|
|
219
|
+
Joinability: string,
|
|
220
|
+
hostName: string,
|
|
221
|
+
ownerId: string,
|
|
222
|
+
rakNetGUID: string,
|
|
223
|
+
version: string,
|
|
224
|
+
levelId: string,
|
|
225
|
+
worldName: string,
|
|
226
|
+
worldType: string,
|
|
227
|
+
protocol: number,
|
|
228
|
+
MemberCount: number,
|
|
229
|
+
MaxMemberCount: number,
|
|
230
|
+
BroadcastSetting: number,
|
|
231
|
+
LanGame: boolean,
|
|
232
|
+
TransportLayer: number,
|
|
233
|
+
WebRTCNetworkId: number,
|
|
234
|
+
OnlineCrossPlatformGame: boolean,
|
|
235
|
+
CrossPlayDisabled: boolean,
|
|
236
|
+
TitleId: number,
|
|
237
|
+
SupportedConnections: SessionConnection[],
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
members: {
|
|
241
|
+
me: {
|
|
242
|
+
constants: {
|
|
243
|
+
system: {
|
|
244
|
+
xuid: string,
|
|
245
|
+
initialize: boolean
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
properties: {
|
|
249
|
+
system: {
|
|
250
|
+
active: boolean,
|
|
251
|
+
connection: string,
|
|
252
|
+
subscription: {
|
|
253
|
+
id: string,
|
|
254
|
+
changeTypes: string[]
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface SessionMember {
|
|
263
|
+
next: number,
|
|
264
|
+
joinTime: string,
|
|
265
|
+
constants: {
|
|
266
|
+
system: {
|
|
267
|
+
initialize: boolean,
|
|
268
|
+
xuid: string,
|
|
269
|
+
index: number
|
|
270
|
+
|
|
271
|
+
},
|
|
272
|
+
custom: {}
|
|
273
|
+
},
|
|
274
|
+
properties: {
|
|
275
|
+
system: {
|
|
276
|
+
subscription: {
|
|
277
|
+
id: string,
|
|
278
|
+
changeTypes: string[]
|
|
279
|
+
},
|
|
280
|
+
active: boolean,
|
|
281
|
+
connection: string
|
|
282
|
+
},
|
|
283
|
+
custom: {}
|
|
284
|
+
},
|
|
285
|
+
gamertag: string,
|
|
286
|
+
activeTitleId: string
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface SessionConnection {
|
|
290
|
+
ConnectionType: number,
|
|
291
|
+
HostIpAddress: string,
|
|
292
|
+
HostPort: number,
|
|
293
|
+
RakNetGUID: string,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
}
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bedrock-portal",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Creates a joinable session in Minecraft Bedrock Edition that can route players to their a specified host",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/LucienHH/bedrock-portal.git"
|
|
12
|
+
},
|
|
13
|
+
"author": "Lucienn",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/LucienHH/bedrock-portal/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/LucienHH/bedrock-portal#readme",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"axios": "^1.0.0",
|
|
21
|
+
"uuid": "^9.0.0",
|
|
22
|
+
"xbox-rta": "^0.0.4"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"bedrock-portal": "file:.",
|
|
26
|
+
"debug": "^4.3.4",
|
|
27
|
+
"prismarine-auth": "^1.7.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
Endpoints: {
|
|
3
|
+
Handle: 'https://sessiondirectory.xboxlive.com/handles',
|
|
4
|
+
ServiceConfigs: 'https://sessiondirectory.xboxlive.com/serviceconfigs',
|
|
5
|
+
},
|
|
6
|
+
SessionConfig: {
|
|
7
|
+
MinecraftTitleID: '896928775',
|
|
8
|
+
MinecraftSCID: '4fc10100-5f7a-4470-899b-280835760c07',
|
|
9
|
+
MinecraftTemplateName: 'MinecraftLobby',
|
|
10
|
+
MiencraftProtocolVersion: 554,
|
|
11
|
+
},
|
|
12
|
+
Joinability: {
|
|
13
|
+
invite_only: {
|
|
14
|
+
joinability: 'invite_only',
|
|
15
|
+
joinRestriction: 'local',
|
|
16
|
+
broadcastSetting: 1,
|
|
17
|
+
},
|
|
18
|
+
friends_only: {
|
|
19
|
+
joinability: 'joinable_by_friends',
|
|
20
|
+
joinRestriction: 'followed',
|
|
21
|
+
broadcastSetting: 2,
|
|
22
|
+
},
|
|
23
|
+
friends_of_friends: {
|
|
24
|
+
joinability: 'joinable_by_friends',
|
|
25
|
+
joinRestriction: 'followed',
|
|
26
|
+
broadcastSetting: 3,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
const debug = require('debug')('bedrock-portal');
|
|
2
|
+
const { v4: uuidV4 } = require('uuid');
|
|
3
|
+
const { XboxRTA } = require('xbox-rta');
|
|
4
|
+
const { EventEmitter } = require('events');
|
|
5
|
+
|
|
6
|
+
const { autoFriendAdd, altCheck } = require('./modules');
|
|
7
|
+
const { SessionConfig, Endpoints, Joinability } = require('./common/constants');
|
|
8
|
+
|
|
9
|
+
const Rest = require('./rest');
|
|
10
|
+
|
|
11
|
+
const genRaknetGUID = () => {
|
|
12
|
+
const chars = '0123456789';
|
|
13
|
+
let result = '';
|
|
14
|
+
for (let i = 20; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
|
|
15
|
+
return result;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = class BedrockPortal extends EventEmitter {
|
|
19
|
+
#rest;#rta;
|
|
20
|
+
constructor(authflow, options = {}) {
|
|
21
|
+
super();
|
|
22
|
+
const name = options.sessionName || uuidV4();
|
|
23
|
+
this.validateOptions(options);
|
|
24
|
+
this.#rest = new Rest(authflow);
|
|
25
|
+
this.#rta = new XboxRTA(authflow);
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.session = {
|
|
28
|
+
url: `https://sessiondirectory.xboxlive.com/serviceconfigs/${SessionConfig.MinecraftSCID}/sessionTemplates/${SessionConfig.MinecraftTemplateName}/sessions/${name}`,
|
|
29
|
+
name,
|
|
30
|
+
subscriptionId: uuidV4(),
|
|
31
|
+
};
|
|
32
|
+
this.players = [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
validateOptions(options) {
|
|
36
|
+
if (!options.ip) throw new Error('No IP provided');
|
|
37
|
+
if (!options.port) throw new Error('No port provided');
|
|
38
|
+
if (options.joinability && !Object.keys(Joinability).includes(options.joinability)) throw new Error('Invalid joinability - Expected one of ' + Object.keys(Joinability).join(', '));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async start() {
|
|
42
|
+
|
|
43
|
+
this.sessionOwner = await this.#rest.getXboxProfile('me');
|
|
44
|
+
|
|
45
|
+
if (!this.options.disableAltCheck) {
|
|
46
|
+
const { isAlt, reason } = await altCheck(this.#rest, this.sessionOwner);
|
|
47
|
+
if (!isAlt) throw new Error('Genuine account detected - ' + reason);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await this.#rta.connect();
|
|
51
|
+
|
|
52
|
+
const connectionId = await this.#rta.subscribe('https://sessiondirectory.xboxlive.com/connections/').then(e => e.data.ConnectionId);
|
|
53
|
+
|
|
54
|
+
await this.updateSession(this.#createSessionBody(connectionId));
|
|
55
|
+
|
|
56
|
+
debug(`Created session, name: ${this.session.name}`);
|
|
57
|
+
|
|
58
|
+
await this.updateHandle(this.#createHandleBody('activity'));
|
|
59
|
+
|
|
60
|
+
const session = await this.getSession();
|
|
61
|
+
|
|
62
|
+
await this.updateSession({ properties: session.properties });
|
|
63
|
+
|
|
64
|
+
await this.#handleSessionEvents();
|
|
65
|
+
|
|
66
|
+
debug(`Published session, name: ${this.session.name}`);
|
|
67
|
+
|
|
68
|
+
return session;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async end() {
|
|
72
|
+
await this.#rta.disconnect();
|
|
73
|
+
|
|
74
|
+
await this.updateSession({ members: { me: null } });
|
|
75
|
+
|
|
76
|
+
debug(`Abandoned session, name: ${this.session.name}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getSessionMembers() {
|
|
80
|
+
return this.players;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async invitePlayer(identifier) {
|
|
84
|
+
const profile = await this.#rest.getXboxProfile(identifier);
|
|
85
|
+
const invitePayload = {
|
|
86
|
+
invitedXuid: String(profile.xuid),
|
|
87
|
+
inviteAttributes: { titleId: SessionConfig.MinecraftTitleID },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
await this.updateHandle(this.#createHandleBody('invite', invitePayload));
|
|
91
|
+
|
|
92
|
+
debug(`Invited player, xuid: ${profile.xuid}`);
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async updateMemberCount(count) {
|
|
97
|
+
await this.updateSession({ properties: { custom: { MemberCount: Number(count) } } });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async updateConnection(connectionId) {
|
|
101
|
+
await this.updateSession({
|
|
102
|
+
members: {
|
|
103
|
+
me: {
|
|
104
|
+
properties: {
|
|
105
|
+
system: {
|
|
106
|
+
active: true,
|
|
107
|
+
connectionId,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getSession() {
|
|
116
|
+
return await this.#rest.get(this.session.url, {
|
|
117
|
+
contractVersion: 107,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async updateSession(payload) {
|
|
122
|
+
await this.#rest.put(this.session.url, {
|
|
123
|
+
data: { ...payload },
|
|
124
|
+
contractVersion: 107,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async updateHandle(payload) {
|
|
129
|
+
await this.#rest.post(Endpoints.Handle, {
|
|
130
|
+
data: { ...payload },
|
|
131
|
+
contractVersion: 107,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async #handleSessionEvents() {
|
|
136
|
+
this.#rta.on('reconnect', async () => {
|
|
137
|
+
const connectionId = await this.#rta.subscribe('https://sessiondirectory.xboxlive.com/connections/').then(e => e.data.ConnectionId);
|
|
138
|
+
await this.updateConnection(connectionId);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
this.#rta.on('event', async ({ type, subId, data }) => {
|
|
142
|
+
this.emit('rtaEvent', { type, subId, data });
|
|
143
|
+
const session = await this.getSession();
|
|
144
|
+
|
|
145
|
+
const sessionMembers = Object.keys(session.members).map(key => session.members[key]).filter(member => member.constants.system.xuid !== this.sessionOwner.xuid);
|
|
146
|
+
const xuids = sessionMembers.map(e => e.constants.system.xuid);
|
|
147
|
+
|
|
148
|
+
const profiles = await this.#rest.getxboxProfileBatch(xuids);
|
|
149
|
+
|
|
150
|
+
const players = sessionMembers.map(e => {
|
|
151
|
+
const { xuid, gamertag, displayPicRaw: avatar, gamerScore: gamerscore, preferredColor: colour } = profiles.find(p => p.xuid === e.constants.system.xuid);
|
|
152
|
+
return {
|
|
153
|
+
profile: { xuid, gamertag, avatar, gamerscore, colour },
|
|
154
|
+
session: {
|
|
155
|
+
titleId: e.activeTitleId,
|
|
156
|
+
joinTime: e.joinTime,
|
|
157
|
+
index: e.constants.system.index,
|
|
158
|
+
connectionId: e.properties.system.connection,
|
|
159
|
+
subscriptionId: e.properties.system.subscription.id,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const newPlayers = players.filter(player => !this.players.find(p => p.profile.xuid === player.profile.xuid));
|
|
165
|
+
if (newPlayers.length) this.emit('playersAdded', newPlayers);
|
|
166
|
+
|
|
167
|
+
const removedPlayers = this.players.filter(player => !players.find(p => p.profile.xuid === player.profile.xuid));
|
|
168
|
+
if (removedPlayers.length) this.emit('playersRemoved', removedPlayers);
|
|
169
|
+
|
|
170
|
+
this.players = players;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (this.options.modules?.autoFriendAdd) autoFriendAdd(this.#rest);
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#createHandleBody(type, additional = {}) {
|
|
178
|
+
return {
|
|
179
|
+
version: 1,
|
|
180
|
+
type,
|
|
181
|
+
sessionRef: {
|
|
182
|
+
scid: SessionConfig.MinecraftSCID,
|
|
183
|
+
templateName: SessionConfig.MinecraftTemplateName,
|
|
184
|
+
name: this.session.name,
|
|
185
|
+
},
|
|
186
|
+
...additional,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#createSessionBody(connectionId) {
|
|
191
|
+
const joinability = Joinability[this.options.joinability ?? 'friends_of_friends'];
|
|
192
|
+
return {
|
|
193
|
+
properties: {
|
|
194
|
+
system: {
|
|
195
|
+
joinRestriction: joinability.joinRestriction,
|
|
196
|
+
readRestriction: 'followed',
|
|
197
|
+
closed: false,
|
|
198
|
+
},
|
|
199
|
+
custom: {
|
|
200
|
+
hostName: String(this.options.world?.hostName) ?? `${this.sessionOwner.gamertag}'s Portal`,
|
|
201
|
+
worldName: String(this.options.world?.name) ?? 'BedrockPortal',
|
|
202
|
+
version: String(this.options.world?.version) ?? require('../package.json').version,
|
|
203
|
+
MemberCount: Number(this.options.world?.memberCount) ?? 0,
|
|
204
|
+
MaxMemberCount: Number(this.options.world?.maxMemberCount) ?? 10,
|
|
205
|
+
Joinability: joinability.joinability,
|
|
206
|
+
ownerId: this.sessionOwner.xuid,
|
|
207
|
+
rakNetGUID: genRaknetGUID(),
|
|
208
|
+
worldType: 'Survival',
|
|
209
|
+
protocol: SessionConfig.MiencraftProtocolVersion,
|
|
210
|
+
BroadcastSetting: joinability.broadcastSetting,
|
|
211
|
+
OnlineCrossPlatformGame: true,
|
|
212
|
+
CrossPlayDisabled: false,
|
|
213
|
+
TitleId: 0,
|
|
214
|
+
SupportedConnections: [
|
|
215
|
+
{
|
|
216
|
+
ConnectionType: 6,
|
|
217
|
+
HostIpAddress: this.options.ip,
|
|
218
|
+
HostPort: Number(this.options.port),
|
|
219
|
+
RakNetGUID: '',
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
members: {
|
|
225
|
+
me: {
|
|
226
|
+
constants: {
|
|
227
|
+
system: {
|
|
228
|
+
xuid: this.sessionOwner.xuid,
|
|
229
|
+
initialize: true,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
properties: {
|
|
233
|
+
system: {
|
|
234
|
+
active: true,
|
|
235
|
+
connection: connectionId,
|
|
236
|
+
subscription: {
|
|
237
|
+
id: this.session.subscriptionId,
|
|
238
|
+
changeTypes: ['everything'],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
};
|
package/src/modules.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const debug = require('debug')('bedrock-portal');
|
|
2
|
+
|
|
3
|
+
// Rough alt check, can be improved
|
|
4
|
+
const altCheck = async (api, owner) => {
|
|
5
|
+
if (owner.tier === 'Gold') return { isAlt: false, reason: 'Account is subscribed to Xbox Gold' };
|
|
6
|
+
if (owner.gamescore > 1000) return { isAlt: false, reason: `High Gamerscore. Account has ${owner.gamescore}, expected less than 1000` };
|
|
7
|
+
|
|
8
|
+
const lastplayed = await api.get(`https://titlehub.xboxlive.com/users/xuid(${owner.xuid})/titles/titlehistory/decoration/detail?maxItems=5000`, { contractVersion: 2 }).then(res => res.titles);
|
|
9
|
+
|
|
10
|
+
if (lastplayed.length > 10) return { isAlt: false, reason: `High number of games played. Account has ${lastplayed.length}, expected less than 10` };
|
|
11
|
+
|
|
12
|
+
return { isAlt: true, reason: 'Account is likely an alt' };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const autoFriendAdd = async (api) => {
|
|
16
|
+
setInterval(async () => {
|
|
17
|
+
|
|
18
|
+
const friends = await api.get('https://peoplehub.xboxlive.com/users/me/people/social/decoration/details').then(res => res.people.map(e => e.xuid));
|
|
19
|
+
const followers = await api.get('https://peoplehub.xboxlive.com/users/me/people/followers/decoration/details').then(res => res.people);
|
|
20
|
+
|
|
21
|
+
const needsAdding = followers.filter(res => !friends.includes(res.xuid));
|
|
22
|
+
|
|
23
|
+
if (!needsAdding.length) return;
|
|
24
|
+
|
|
25
|
+
debug(`Adding ${needsAdding.length} account(s) [${needsAdding.map(res => res.gamertag).join(', ')}]`);
|
|
26
|
+
|
|
27
|
+
for (const account of needsAdding) {
|
|
28
|
+
await api.put(`https://social.xboxlive.com/users/me/people/xuid(${account.xuid})`, { contractVersion: 2 }).catch(_ => debug(`Couldn't add ${account.xuid}`));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
}, 30000);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
autoFriendAdd,
|
|
36
|
+
altCheck,
|
|
37
|
+
};
|
package/src/rest.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
module.exports = class Rest {
|
|
4
|
+
constructor(authflow, options = {}) {
|
|
5
|
+
this.auth = authflow;
|
|
6
|
+
this.options = options;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async get(url, config = {}) {
|
|
10
|
+
return await this._request('GET', { url, ...config });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async post(url, config = {}) {
|
|
14
|
+
return await this._request('POST', { url, ...config });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async put(url, config = {}) {
|
|
18
|
+
return await this._request('PUT', { url, ...config });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async delete(url, config = {}) {
|
|
22
|
+
return await this._request('DELETE', { url, ...config });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async _request(method, config) {
|
|
26
|
+
const auth = await this.auth.getXboxToken('http://xboxlive.com');
|
|
27
|
+
|
|
28
|
+
const payload = {
|
|
29
|
+
method,
|
|
30
|
+
url: config.url,
|
|
31
|
+
headers: {
|
|
32
|
+
'Authorization': `XBL3.0 x=${auth.userHash};${auth.XSTSToken}`,
|
|
33
|
+
'accept-language': 'en-US',
|
|
34
|
+
...(this.options.headers || {}),
|
|
35
|
+
...(config.headers || {}),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (config.contractVersion) payload.headers['x-xbl-contract-version'] = config.contractVersion;
|
|
40
|
+
if (config.params) payload.params = config.params;
|
|
41
|
+
if (config.data) payload.data = config.data;
|
|
42
|
+
|
|
43
|
+
return await axios(payload).then(e => e.data);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getxboxProfileBatch(xuids) {
|
|
47
|
+
const response = await this.post('https://peoplehub.xboxlive.com/users/me/people/batch/decoration/detail,preferredcolor', { data: { xuids }, contractVersion: 5 }).then(e => e.people).catch(e => []);
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getXboxProfile(input) {
|
|
52
|
+
const settings = 'GameDisplayPicRaw,Gamerscore,Gamertag,AccountTier,XboxOneRep,PreferredColor,RealName,Bio,Location,ModernGamertag,ModernGamertagSuffix,UniqueModernGamertag,RealNameOverride,TenureLevel,Watermarks,IsQuarantined,DisplayedLinkedAccounts';
|
|
53
|
+
|
|
54
|
+
let target = '';
|
|
55
|
+
if (input === 'me') {
|
|
56
|
+
target = 'me';
|
|
57
|
+
}
|
|
58
|
+
else if (input.length === 16 && !isNaN(input)) {
|
|
59
|
+
target = `xuid(${input})`;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
target = `gt(${encodeURIComponent(input)})`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const response = await this.get(`https://profile.xboxlive.com/users/${target}/profile/settings`, { params: { settings }, contractVersion: 2 });
|
|
66
|
+
|
|
67
|
+
const [avatar, gamerscore, gamertag, tier, reputation, colour, realname, bio, location, modernGamertag, modernGamertagSuffix, uniqueModernGamertag, realnameOverride, tenureLevel, watermarks, isQuarantined, linkedAccounts] = response.profileUsers[0].settings.map(e=> e.value);
|
|
68
|
+
const colourData = await axios.get(colour).then(e => e.data);
|
|
69
|
+
|
|
70
|
+
const result = {
|
|
71
|
+
xuid: response.profileUsers[0].id,
|
|
72
|
+
avatar: avatar.replace(/&background=0xababab&mode=Padding&format=png/g, ''),
|
|
73
|
+
gamerscore,
|
|
74
|
+
gamertag,
|
|
75
|
+
tier,
|
|
76
|
+
reputation,
|
|
77
|
+
colour: colourData,
|
|
78
|
+
realname,
|
|
79
|
+
bio,
|
|
80
|
+
location,
|
|
81
|
+
modernGamertag,
|
|
82
|
+
modernGamertagSuffix,
|
|
83
|
+
uniqueModernGamertag,
|
|
84
|
+
realnameOverride,
|
|
85
|
+
tenureLevel,
|
|
86
|
+
watermarks,
|
|
87
|
+
isQuarantined,
|
|
88
|
+
linkedAccounts,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
};
|