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 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
+ [![NPM version](https://img.shields.io/npm/v/bedrock-portal.svg)](http://npmjs.com/package/bedrock-portal)
3
+ [![Discord](https://img.shields.io/badge/chat-on%20discord-brightgreen.svg)](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
@@ -0,0 +1,5 @@
1
+ process.env.DEBUG = '*';
2
+
3
+ module.exports = {
4
+ BedrockPortal: require('./src/index'),
5
+ };
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
+ };