Skip to content

Commit

Permalink
Merge pull request #6 from maciejpedzich/gh5
Browse files Browse the repository at this point in the history
Implement Client#getHosts method
  • Loading branch information
maciejpedzich authored Nov 8, 2021
2 parents 6194f4d + 0bc0e8b commit 0352e8d
Show file tree
Hide file tree
Showing 16 changed files with 292 additions and 66 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.2.0

- Add a `Client#getHosts` method for obtaining a list of hosts matching specified `mode` and `public` criteria
- Create `Host`, `GetHostsQueryParams`, `GetHostsPayload` interfaces
- Rename `AuthSuccessBody` interface to `AuthPayload`
- Change `Client#authPersonal` method, so that it takes a single `AuthPersonalCredentials` object argument, instead of three separate `email`, `password` and `tfa` strings

## 0.1.3

- Fix accidentally publishing a codeless package via Github Actions workflow
Expand Down
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# node-parsec-sdk

JavaScript and TypeScript SDK for Parsec remote desktop.

## DISCLAIMER

This is an **UNOFFICIAL** package, which is also very early in development. The vast majority of necessary features is missing at this point. See the _Roadmap_ project and issues on Github to get a list of upcoming functionalities.
Expand All @@ -14,7 +16,7 @@ npm install --save parsec-sdk

## Documentation

Online documentation is generated using [Typedoc](https://typedoc.org/) and hosted on Github Pages. It's available here: [https://maciejpedzich.github.io/node-parsec-sdk/](https://maciejpedzich.github.io/node-parsec-sdk/)
Online documentation is automatically generated using [TypeDoc](https://typedoc.org/) and hosted on Github Pages. It's available on [https://maciejpedzich.github.io/node-parsec-sdk/](https://maciejpedzich.github.io/node-parsec-sdk/)

## Code example

Expand All @@ -27,21 +29,32 @@ import { Client } from 'parsec-sdk';

const parsec = new Client();

async function demo() {
async function authDemo() {
try {
await parsec.authPersonal(
'parsec-account-email@example.com',
'ParsecAccountP4ssword!',
'123456' // OPTIONAL 2FA code
);
await parsec.authPersonal({
email: 'parsec-account-email@example.com',
password: 'ParsecAccountP4ssword!',
tfa: '123456' // OPTIONAL TFA code
});

console.log(`Peer ID: ${parsec.peerID}\nSession ID: ${parsec.sessionID}`);
} catch (error) {
console.error(error);
}
}

demo();
async function hostsDemo() {
try {
const { data } = await parsec.getHosts({ mode: 'desktop' });

console.log(data);
} catch (error) {
console.error(error);
}
}

authDemo();
hostsDemo();
```

## Development
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parsec-sdk",
"version": "0.1.3",
"version": "0.2.0",
"description": "UNOFFICIAL Parsec remote desktop SDK for JavaScript",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
67 changes: 52 additions & 15 deletions src/classes/Client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { AxiosError } from 'axios';
import { stringify } from 'querystring';

import { httpClient } from '../utils/httpClient';
import { http } from '../utils/http';
import { Status } from '../enums/Status';

import { InvalidCredentialsError } from '../errors/InvalidCredentials';
import { TFARequiredError } from '../errors/TFARequired';
import { AuthErrorBody } from '../interfaces/AuthErrorBody';
import { AuthSuccessBody } from '../interfaces/AuthSuccessBody';
import { AuthRequiredError } from '../errors/AuthRequired';

import { AuthPersonalCredentials } from 'src/interfaces/auth/PersonalCredentials';
import { AuthErrorBody } from '../interfaces/auth/ErrorBody';
import { AuthPayload } from '../interfaces/auth/Payload';

import { GetHostsPayload } from '../interfaces/host/GetPayload';
import { GetHostsQueryParams } from '../interfaces/host/GetQueryParams';

export class Client {
public status: Status = Status.PARSEC_NOT_RUNNING;
Expand All @@ -17,30 +25,25 @@ export class Client {
/**
* Authenticate client using the _personal_ strategy
*
* @param {string} email Parsec account's email
* @param {string} password Parsec account's password
* @param {string} [tfa] TFA code
* @param {@link AuthPersonalCredentials} credentials Credentials to use
* in the authentication request
*/
public async authPersonal(email: string, password: string, tfa?: string) {
public async authPersonal(credentials: AuthPersonalCredentials) {
try {
if (this.sessionID && this.peerID && this.status === Status.PARSEC_OK) {
return;
}

this.status = Status.PARSEC_CONNECTING;

const response = await httpClient.post<AuthSuccessBody>('/v1/auth/', {
email,
password,
tfa
});
const { host_peer_id, session_id } = response.data;
const { data } = await http.post<AuthPayload>('/v1/auth', credentials);
const { host_peer_id, session_id } = data;

this.peerID = host_peer_id;
this.sessionID = session_id;
this.status = Status.PARSEC_OK;
} catch (error) {
this.status = Status.ERR_DEFAULT;

const httpErrorBody = (error as AxiosError<AuthErrorBody>).response?.data;

if (httpErrorBody?.tfa_required) {
Expand All @@ -49,7 +52,41 @@ export class Client {
throw new InvalidCredentialsError();
}

throw error;
throw new Error(httpErrorBody?.error || (error as Error).message);
}
}
/**
* Obtain a list of hosts matching specified `mode` and `public` criteria
*
* @param {@link GetHostsQueryParams} queryParams get hosts request
* query params object representation
*/
public async getHosts(queryParams: GetHostsQueryParams) {
try {
const queryString = stringify({ ...queryParams });
const { data } = await http.get<GetHostsPayload>(
`/v2/hosts?${queryString}`,
{
headers: {
Authorization: `Bearer ${this.sessionID}`
}
}
);

return data;
} catch (error) {
this.status = Status.ERR_DEFAULT;

const httpErrorBody = (error as AxiosError<AuthErrorBody>).response?.data;

if (
!this.sessionID &&
httpErrorBody?.error === 'no session ID in request header'
) {
throw new AuthRequiredError();
}

throw new Error(httpErrorBody?.error || (error as Error).message);
}
}
}
5 changes: 5 additions & 0 deletions src/errors/AuthRequired.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class AuthRequiredError extends Error {
constructor() {
super('Authentication is required to proceed');
}
}
16 changes: 15 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@ import { Client } from './classes/Client';

import { Status } from './enums/Status';

import { AuthPersonalCredentials } from './interfaces/auth/PersonalCredentials';
import { Host } from './interfaces/host/Host';
import { GetHostsQueryParams } from './interfaces/host/GetQueryParams';
import { GetHostsPayload } from './interfaces/host/GetPayload';

import { InvalidCredentialsError } from './errors/InvalidCredentials';
import { TFARequiredError } from './errors/TFARequired';

export { Client, Status, InvalidCredentialsError, TFARequiredError };
export {
Client,
Status,
AuthPersonalCredentials,
Host,
GetHostsQueryParams,
GetHostsPayload,
InvalidCredentialsError,
TFARequiredError
};
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @internal */
export interface AuthSuccessBody {
export interface AuthPayload {
instance_id: string;
user_id: number;
session_id: string;
Expand Down
13 changes: 13 additions & 0 deletions src/interfaces/auth/PersonalCredentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Representation of the _personal_ authentication credentials/request body
*/
export interface AuthPersonalCredentials {
/** Parsec account's email address */
email: string;

/** Parsec account's password */
password: string;

/** Optional TFA code */
tfa?: string;
}
7 changes: 7 additions & 0 deletions src/interfaces/host/GetPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Host } from './Host';

/** Payload of the `getHosts` call */
export interface GetHostsPayload {
data: Host[];
has_more: boolean;
}
10 changes: 10 additions & 0 deletions src/interfaces/host/GetQueryParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Object representation of `getHosts` request query params
*/
export interface GetHostsQueryParams {
/** Host's mode */
mode: 'desktop' | 'game';

/** Host's visibility */
public?: boolean;
}
42 changes: 42 additions & 0 deletions src/interfaces/host/Host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Host object representation
*/
export interface Host {
/** Host computer's peer ID */
peer_id: string;

/** User that created the host */
user: {
id: number;
name: string;
warp: boolean;
};

/** Internal Parsec game ID */
game_id: string;

/** Parsec build number */
build: string;

/** Host's description */
description: string;

/** Maximal number of players allowed to be connected simultaneously */
max_players: number;

/** Host's mode, can be either _desktop_ or _game_ */
mode: 'desktop' | 'game';

/** Host's name */
name: string;

/** Number of players currently connected to the host */
players: number;

/** Host's visibility */
public: boolean;

/** Determines if the host that made the `GET /hosts` call
* is attached to the same sessionID */
self: boolean;
}
2 changes: 1 addition & 1 deletion src/utils/httpClient.ts → src/utils/http.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from 'axios';

/** @internal */
export const httpClient = axios.create({
export const http = axios.create({
baseURL: 'https://kessel-api.parsecgaming.com'
});
Loading

0 comments on commit 0352e8d

Please sign in to comment.