diff --git a/package.json b/package.json index a8c285ed..50ceb564 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "authentication": "yarn workspace @greenlight/authentication", "storeapi": "yarn workspace @greenlight/storeapi", "xcloudapi": "yarn workspace @greenlight/xcloudapi", + "webapi": "yarn workspace @greenlight/webapi", "test": "yarn logger test && yarn authentication test && yarn storeapi test && yarn xcloudapi test" }, "workspaces": [ diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 75675d70..f7299db2 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -9,8 +9,8 @@ "license": "MIT", "main": "dist/index", "scripts": { - "start": "yarn build && DEBUG='*' node dist/bin/example.js", - "build": "yarn build:deps && tsc --build", + "start": "yarn build:deps && yarn build && DEBUG='*' node dist/bin/example.js", + "build": "tsc --build", "build:deps": "yarn workspace @greenlight/logger build", "clean": "rm -rf dist/ && rm -rf *.tsbuildinfo", "test": "yarn build && mocha -r ../../node_modules/ts-node/register tests/**.ts tests/**/*.ts" diff --git a/packages/storeapi/package.json b/packages/storeapi/package.json index 3ca9bb54..25d28528 100644 --- a/packages/storeapi/package.json +++ b/packages/storeapi/package.json @@ -4,8 +4,8 @@ "license": "MIT", "main": "dist/index", "scripts": { - "start": "yarn build && DEBUG='*' node dist/bin/example.js", - "build": "yarn build:deps && tsc --build", + "start": "yarn build:deps && yarn build && DEBUG='*' node dist/bin/example.js", + "build": "tsc --build", "build:deps": "yarn workspace @greenlight/authentication build && yarn workspace @greenlight/logger build && yarn workspace @greenlight/xcloudapi build", "clean": "rm -rf dist/ && rm -rf *.tsbuildinfo", "test": "yarn build && mocha -r ../../node_modules/ts-node/register tests/**.ts tests/**/*.ts" diff --git a/packages/webapi/README.md b/packages/webapi/README.md new file mode 100644 index 00000000..ec296d5f --- /dev/null +++ b/packages/webapi/README.md @@ -0,0 +1,23 @@ +# @greenlight/storeapi + +This package is the storeapi. It will communicate with the gamepass catalog and fetch all products and keep the information in a in-memory sql lite database + +## Class: StoreApi + +### constructor(config:{ market: '' }):void + +### loadProductIds(titles:string[]):boolean + +Returns true if the supplied product id's have been loaded. + +### findTitle():Promise + +Returns a promise with ItemResponse object. This contains an id, title_name and title_data. + +## Class: xCloudApi + +### constructor(host:string, token:string):void + +### getTitles():Promise + +Returns data with the titleID included. This is mostly used to test the functionality with a bigger chunk of data. \ No newline at end of file diff --git a/packages/webapi/package.json b/packages/webapi/package.json new file mode 100644 index 00000000..b9769f11 --- /dev/null +++ b/packages/webapi/package.json @@ -0,0 +1,27 @@ +{ + "name": "@greenlight/webapi", + "version": "1.0.0", + "license": "MIT", + "main": "dist/index", + "scripts": { + "start": "yarn build:deps && yarn build && DEBUG='*' node dist/bin/example.js", + "build": "tsc --build", + "build:deps": "yarn workspace @greenlight/authentication build && yarn workspace @greenlight/logger build", + "clean": "rm -rf dist/ && rm -rf *.tsbuildinfo", + "test": "yarn build && mocha -r ../../node_modules/ts-node/register tests/**.ts tests/**/*.ts" + }, + "dependencies": { + "@greenlight/authentication": "workspace:^", + "@greenlight/logger": "workspace:^" + }, + "devDependencies": { + "@types/chai": "^4", + "@types/debug": "^4.1.7", + "@types/mocha": "^10", + "@types/node": "^20.9.0", + "chai": "^4.3.6", + "mocha": "^10.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/packages/webapi/src/bin/example.ts b/packages/webapi/src/bin/example.ts new file mode 100644 index 00000000..225039bb --- /dev/null +++ b/packages/webapi/src/bin/example.ts @@ -0,0 +1,52 @@ + +import Authentication from '@greenlight/authentication' +import Logger from '@greenlight/logger' +import WebApi from '../index' + +class Cli { + private _auth = new Authentication() + public logger = new Logger('WebApi:Cli') + + constructor(){ + this.logger.log('Greenlight WebApi Cli') + this.logger.log('User:', this._auth.user.getGamertag()) + + this._auth.user.getWebToken().then((token) => { + console.log('Userhash:', this._auth.user.getUserhash()) + console.log('Token:', token.data.Token) + console.log('Token NotAfter:', token.data.NotAfter) + + // Only used for loading all productId's + const webapi = new WebApi({ + uhs: this._auth.user.getUserhash(), + token: { + Token: token.data.Token, + NotAfter: token.data.NotAfter + } + }) + + console.log('Token valid:', webapi.isTokenValid()) + // webapi.provider.smartglass.getConsolesList().then((response) => { + // console.log('Console ID:', response[0].id) + + + // webapi.provider.smartglass.getInstalledApps(response[0].id).then((response) => { + // console.log('Response:', response) + + // response.forEach((app) => { + // console.log(app.contentType, ':', app.name) + // }) + // }) + // }) + + webapi.provider.people.getFriends().then((response) => { + console.log('Response:', response) + }) + + }).catch((error) => { + this.logger.error('Error:', error) + }) + } +} + +new Cli() \ No newline at end of file diff --git a/packages/webapi/src/http.ts b/packages/webapi/src/http.ts new file mode 100644 index 00000000..abb7b628 --- /dev/null +++ b/packages/webapi/src/http.ts @@ -0,0 +1,50 @@ +import Logger from '@greenlight/logger' + +const logger = new Logger('WebApi:HTTP') + +export default { + + get(url:string, headers:any) { + logger.log('GET:', url, headers) + + return fetch(url, { + method: 'GET', + headers: headers + }).then(async (response) => { + logger.log(response.status,'-', url) + try { + return { + response: response, + data: await response.json() + } + } catch (error) { + return { + response: response, + data: response.body + } + } + }) + }, + + post(url:string, headers:any, body:any) { + logger.log('POST:', url, headers, JSON.stringify(body)) + return fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify(body) + }).then(async (response) => { + logger.log(response.status,'-', url) + try { + return { + response: response, + data: await response.json() + } + } catch (error) { + return { + response: response, + data: response.body + } + } + }) + } +} \ No newline at end of file diff --git a/packages/webapi/src/index.ts b/packages/webapi/src/index.ts new file mode 100644 index 00000000..9a8fa8ef --- /dev/null +++ b/packages/webapi/src/index.ts @@ -0,0 +1,46 @@ +import Logger from '@greenlight/logger' + +import SmartglassProvider from './providers/smartglass' +import SocialProvider from './providers/social' +import PeopleProvider from './providers/people' + +export interface xCloudApiConfig { + uhs:string + token:XstsToken + language?:string +} + +export interface XstsToken { + Token:string + NotAfter:string +} + +export default class WebApi { + + public logger:Logger = new Logger('WebApi') + private _uhs:string + private _token:XstsToken + private _language:string + + public provider = { + smartglass: new SmartglassProvider(this), + social: new SocialProvider(this), + people: new PeopleProvider(this), + } + + constructor(config:xCloudApiConfig) { + this.logger.log('constructor() Creating new WebApi instance') + + this._uhs = config.uhs + this._token = config.token + this._language = config.language || 'en-US' + } + + isTokenValid() { + return new Date(this._token.NotAfter) > new Date() + } + + getAuthorizationHeader() { + return 'XBL3.0 x='+this._uhs+';'+this._token.Token + } +} \ No newline at end of file diff --git a/packages/webapi/src/providers/base.ts b/packages/webapi/src/providers/base.ts new file mode 100644 index 00000000..481e69d0 --- /dev/null +++ b/packages/webapi/src/providers/base.ts @@ -0,0 +1,30 @@ +import WebApi from '..' +import Logger from '@greenlight/logger' +import Http from '../http' + +export interface HttpHeaders { + [name:string]: string +} + +export default class BaseProvider { + + private _api:WebApi + public logger:Logger + + public _endpoint = 'https://undefined.xboxlive.com' + public _headers:HttpHeaders = {} + + constructor(api:WebApi){ + this._api = api + this.logger = this._api.logger.extend(this.constructor.name) + } + + async get(path:string, headers:HttpHeaders = {}) { + const reqHeaders = Object.assign({}, { + 'Authorization': this._api.getAuthorizationHeader() + }, headers) + + return await Http.get(this._endpoint+path, reqHeaders) + } + +} \ No newline at end of file diff --git a/packages/webapi/src/providers/people.ts b/packages/webapi/src/providers/people.ts new file mode 100644 index 00000000..d2ceaffa --- /dev/null +++ b/packages/webapi/src/providers/people.ts @@ -0,0 +1,170 @@ +import BaseProvider, { HttpHeaders } from './base' + +export interface GetFriendsResponse { + detail: Detail; + follower: null; + recommendation: null; + isFriend: boolean; + friendedDateTimeUtc: Date | null; + isFriendRequestReceived: boolean; + isFriendRequestSent: boolean; + xuid: string; + appXuid: null; + isFavorite: boolean; + isFollowingCaller: boolean; + isFollowedByCaller: boolean; + isIdentityShared: boolean; + addedDateTimeUtc: Date | null; + displayName: string; + realName: string; + displayPicRaw: string; + showUserAsAvatar: string; + gamertag: string; + gamerScore: string; + modernGamertag: string; + modernGamertagSuffix: string; + uniqueModernGamertag: string; + xboxOneRep: XboxOneRep; + presenceState: PresenceState; + presenceText: string; + presenceDevices: null; + isBroadcasting: boolean; + isCloaked: null; + isQuarantined: boolean; + isXbox360Gamerpic: boolean; + lastSeenDateTimeUtc: Date | null; + suggestion: null; + search: null; + titleHistory: null; + multiplayerSummary: MultiplayerSummary; + recentPlayer: null; + preferredColor: PreferredColor; + presenceDetails: PresenceDetail[]; + titlePresence: null; + titleSummaries: null; + presenceTitleIds: null; + communityManagerTitles: null; + socialManager: null; + broadcast: null; + avatar: null; + linkedAccounts: LinkedAccount[]; + colorTheme: string; + preferredFlag: string; + preferredPlatforms: string[]; +} + +export interface Detail { + canBeFriended: boolean; + canBeFollowed: boolean; + isFriend: boolean; + friendCount: number; + isFriendRequestReceived: boolean; + isFriendRequestSent: boolean; + isFriendListShared: boolean; + isFollowingCaller: boolean; + isFollowedByCaller: boolean; + isFavorite: boolean; + accountTier: AccountTier; + bio: string; + isVerified: boolean; + location: string; + tenure: string; + watermarks: string[]; + blocked: boolean; + mute: boolean; + followerCount: number; + followingCount: number; + hasGamePass: boolean; +} + +export enum AccountTier { + Gold = "Gold", + Silver = "Silver", +} + +export interface LinkedAccount { + networkName: string; + displayName: string; + showOnProfile: boolean; + isFamilyFriendly: boolean; + deeplink: null; +} + +export interface MultiplayerSummary { + joinableActivities: any[]; + partyDetails: any[]; + inParty: number; +} + +export interface PreferredColor { + primaryColor: string; + secondaryColor: string; + tertiaryColor: string; +} + +export interface PresenceDetail { + IsBroadcasting: boolean; + Device: Device; + DeviceSubType: null; + GameplayType: null; + PresenceText: string; + State: State; + TitleId: string; + TitleType: null; + IsPrimary: boolean; + IsGame: boolean; + RichPresenceText: null | string; +} + +export enum Device { + Android = "Android", + PlayStation = "PlayStation", + Scarlett = "Scarlett", + WindowsOneCore = "WindowsOneCore", +} + +export enum State { + Active = "Active", + LastSeen = "LastSeen", +} + +export enum PresenceState { + Offline = "Offline", + Online = "Online", +} + +export enum XboxOneRep { + GoodPlayer = "GoodPlayer", +} + + +export default class PeopleProvider extends BaseProvider { + + _endpoint = 'https://peoplehub.xboxlive.com' + _headers = { + 'x-xbl-contract-version': '7', + 'Accept-Language': 'en-US' + } + + async getFriends():Promise { + const params = [ + 'preferredcolor', + 'detail', + 'multiplayersummary', + 'presencedetail', + ] + const response = await this.get('/users/me/people/social/decoration/'+ params.join(','), this._headers) + return response.data + } + + async get(path:string, headers?:HttpHeaders) { + const response = await super.get(path, headers) + + if(response.response.status !== 200){ + throw new Error('Error: Statuscode is not expected value. Expected 200, got: '+response.response.status) + } + + return response + } + +} \ No newline at end of file diff --git a/packages/webapi/src/providers/smartglass.ts b/packages/webapi/src/providers/smartglass.ts new file mode 100644 index 00000000..83545c6f --- /dev/null +++ b/packages/webapi/src/providers/smartglass.ts @@ -0,0 +1,82 @@ +import BaseProvider, { HttpHeaders } from './base' + +export interface GetConsolesListResponse { + id: string + name: string + locale: string + powerState: string // Change to enum? + consoleType: string // Change to enum? +} + +export interface GetInstalledAppsResponse { + oneStoreProductId: null | string; + titleId: number; + aumid: null | string; + lastActiveTime: Date | null; + isGame: boolean; + name: string; + contentType: ContentType; + instanceId: string; + storageDeviceId: string; + uniqueId: string; + legacyProductId: null | string; + version: number; + sizeInBytes: number; + installTime: Date; + updateTime: Date | null; + parentId: string | null; +} + +export interface GetStorageDevicesResponse { + storageDeviceId: string + storageDeviceName: string + isDefault: boolean + freeSpaceBytes: number + totalSpaceBytes: number + isGen9Compatible: null | boolean +} + +export enum ContentType { + App = "App", + Dlc = "Dlc", + Game = "Game", +} + +export default class SmartglassProvider extends BaseProvider { + + _endpoint = 'https://xccs.xboxlive.com' + _headers = { + 'x-xbl-contract-version': '4', + 'skillplatform': 'RemoteManagement' + } + + async getConsolesList():Promise { + const response = await this.get('/lists/devices?queryCurrentDevice=false&includeStorageDevices=true') + return response.data.result + } + + async getInstalledApps(consoleId:string):Promise { + const response = await this.get('/lists/installedApps?deviceId='+consoleId) + return response.data.result + } + + async getStorageDevices(consoleId:string):Promise { + const response = await this.get('/lists/storageDevices?deviceId='+consoleId) + return response.data.result + } + + async get(path:string, headers?:HttpHeaders) { + const response = await super.get(path, headers) + + if(response.response.status !== 200){ + throw new Error('Error: Statuscode is not expected value. Expected 200, got: '+response.response.status) + } + + if(response.data.status.errorCode !== 'OK'){ + throw new Error('Error: Smartglass status code is not expected value. Expected OK, got: '+response.data.status.errorCode+'\nDetails: '+response.data.status.errorMessage) + } + + return response + } + +} \ No newline at end of file diff --git a/packages/webapi/src/providers/social.ts b/packages/webapi/src/providers/social.ts new file mode 100644 index 00000000..9125645c --- /dev/null +++ b/packages/webapi/src/providers/social.ts @@ -0,0 +1,22 @@ +import BaseProvider, { HttpHeaders } from './base' + +export default class SocialProvider extends BaseProvider { + + _endpoint = 'https://social.xboxlive.com' + + async getSummary() { + const response = await this.get('/users/me/summary') + return response + } + + async get(path:string, headers?:HttpHeaders) { + const response = await super.get(path, headers) + + if(response.response.status !== 200){ + throw new Error('Error: Statuscode is not expected value. Expected 200, got: '+response.response.status) + } + + return response + } + +} \ No newline at end of file diff --git a/packages/webapi/tests/main.ts b/packages/webapi/tests/main.ts new file mode 100644 index 00000000..f6afd991 --- /dev/null +++ b/packages/webapi/tests/main.ts @@ -0,0 +1,16 @@ +import WebApi from '../src/index' + +import { expect } from 'chai' + +describe('WebApi', () => { + + describe('new instance', () => { + it('should create an instance of WebApi', function(){ + const auth = new WebApi({ + host: 'dummy', + token: 'dummytoken' + }) + expect(auth).to.be.an.instanceOf(WebApi) + }) + }) +}) \ No newline at end of file diff --git a/packages/webapi/tsconfig.json b/packages/webapi/tsconfig.json new file mode 100644 index 00000000..f18ad631 --- /dev/null +++ b/packages/webapi/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es6", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"], + } \ No newline at end of file diff --git a/packages/xcloudapi/package.json b/packages/xcloudapi/package.json index 8afc81c3..fc91a592 100644 --- a/packages/xcloudapi/package.json +++ b/packages/xcloudapi/package.json @@ -4,8 +4,8 @@ "license": "MIT", "main": "dist/index", "scripts": { - "start": "yarn build && DEBUG='*' node dist/bin/example.js", - "build": "yarn build:deps && tsc --build", + "start": "yarn build:deps && yarn build && DEBUG='*' node dist/bin/example.js", + "build": "tsc --build", "build:deps": "yarn workspace @greenlight/authentication build && yarn workspace @greenlight/logger build", "clean": "rm -rf dist/ && rm -rf *.tsbuildinfo", "test": "yarn build && mocha -r ../../node_modules/ts-node/register tests/**.ts tests/**/*.ts" diff --git a/server/package.json b/server/package.json index db2876ae..db62c1f6 100644 --- a/server/package.json +++ b/server/package.json @@ -11,8 +11,8 @@ "license": "MIT", "main": "dist/index", "scripts": { - "start": "yarn build && DEBUG='*' node dist/bin/server.js", - "build": "yarn build:deps && tsc --build", + "start": "yarn build:deps && yarn build && DEBUG='*' node dist/bin/server.js", + "build": "tsc --build", "build:deps": "yarn workspace @greenlight/logger build && yarn workspace @greenlight/authentication build && yarn workspace @greenlight/storeapi build && yarn workspace @greenlight/xcloudapi build", "clean": "rm -rf dist/ && rm -rf *.tsbuildinfo", "test": "yarn build && mocha -r ../node_modules/ts-node/register tests/**.ts tests/**/*.ts" diff --git a/yarn.lock b/yarn.lock index fd6f8a54..8ebf1056 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1772,6 +1772,23 @@ __metadata: languageName: unknown linkType: soft +"@greenlight/webapi@workspace:packages/webapi": + version: 0.0.0-use.local + resolution: "@greenlight/webapi@workspace:packages/webapi" + dependencies: + "@greenlight/authentication": "workspace:^" + "@greenlight/logger": "workspace:^" + "@types/chai": ^4 + "@types/debug": ^4.1.7 + "@types/mocha": ^10 + "@types/node": ^20.9.0 + chai: ^4.3.6 + mocha: ^10.1.0 + ts-node: ^10.9.2 + typescript: ^5.3.3 + languageName: unknown + linkType: soft + "@greenlight/xcloudapi@workspace:^, @greenlight/xcloudapi@workspace:packages/xcloudapi": version: 0.0.0-use.local resolution: "@greenlight/xcloudapi@workspace:packages/xcloudapi"