# VTubeStudioJS

![npm](https://img.shields.io/npm/v/vtubestudio)
![GitHub](https://img.shields.io/github/license/Hawkbat/VTubeStudioJS)

An implementation of the WebSocket-based VTube Studio API for Node and browser JavaScript.

See the [generated API documentation](https://hawk.bar/VTubeStudioJS/) for type definitions and comments for the various public classes and interfaces, or use an editor with TypeScript support to automatically get types and comments when inspecting objects and fields.

See the official [VTube Studio API documentation](https://github.com/DenchiSoft/VTubeStudio) for more details on the semantic meaning of individual fields and the behaviors of the various API calls.

## Install

```bash
# NPM
npm i vtubestudio
# Yarn
yarn add vtubestudio
```

This package has no runtime dependencies. To avoid node_modules bloat, use `npm install --only=prod` or `NODE_ENV=production` to skip unnecessary dev dependencies.

## Usage

### Basic Usage

This library is platform-agnostic, allowing it to be used for both Node projects and for browsers via tools like webpack. Use it by importing and instantiating an ApiClient:

```javascript
// ES Modules (NodeJS or browser with bundler)
import { ApiClient } from "vtubestudio";

// CommonJS/Require (NodeJS)
const vts = require("vtubestudio");
const ApiClient = vts.ApiClient;

// ES Modules (CDN)
import { ApiClient } from "https://unpkg.com/vtubestudio/lib/esm/vtubestudio.min.js?module";

// Global Variable (CDN)
// <script src="https://unpkg.com/vtubestudio/lib/iife/vtubestudio.min.js">
const ApiClient = VTubeStudio.ApiClient;

function setAuthToken(authenticationToken) {
  // store the authentication token somewhere
}

function getAuthToken() {
  // retrieve the stored authentication token
}

const options = {
  authTokenGetter: getAuthToken,
  authTokenSetter: setAuthToken,
  pluginName: "Your Plugin Name",
  pluginDeveloper: "Your User Name",

  // Optionally set the URL or port to connect to VTube Studio at; defaults are as below:

  //port: 8001,
  //url: "ws://localhost:${port}",
};

const apiClient = new ApiClient(options);
```

> See [IApiClientOptions](https://hawk.bar/VTubeStudioJS/interfaces/IApiClientOptions.html) in the docs for all available options.

The ApiClient automatically handles connecting to the VTube Studio API, including seamlessly reconnecting broken WebSocket connections, and authenticating your plugin with VTube Studio. To enable these features, you must provide the `authTokenGetter` and `authTokenSetter` functions above, to persist the authentication token generated by VTube Studio.

Barebones implementations, for reference:

```javascript
// Browser

function setAuthToken(authenticationToken) {
  localStorage.setItem("VTS_AUTH_TOKEN", authenticationToken);
}

function getAuthToken() {
  return localStorage.getItem("VTS_AUTH_TOKEN");
}

// NodeJS

function setAuthToken(authenticationToken) {
  fs.writeFileSync("./auth-token.txt", authenticationToken, {
    encoding: "utf-8",
  });
}

function getAuthToken() {
  return fs.readFileSync("./auth-token.txt", "utf-8");
}
```

### Avoiding Timeout Errors

API calls may throw an error if the API client is not prepared to send requests; it may not be able to connect to VTube Studio, it may be waiting for the user to accept the authentication prompt, or the Plugin API might be disabled in VTube Studio settings. For best results, wrap every call in a try-catch, and use the following patterns where applicable:

```javascript
// Run commands when the client connects:
apiClient.on("connect", async () => {
  // The API client just finished authenticating with VTube Studio, or reconnecting if it lost connection.

  // Run any commands you need to persist across reconnections here, such as event subscriptions:
  apiClient.events.modelLoaded.subscribe((data) => {
    // ...
  });
  // ...
});

// Check if the client is connected before running commands:
if (apiClient.isConnected) {
  const response = await apiClient.currentModel();
  // ...
}
```

### Making API Calls

The API client exposes the request/response message pairs provided by the VTube Studio API as single asynchronous functions. You can use them like so:

```javascript
// async/await
async function loadModel() {
  try {
    const response = await apiClient.modelLoad({ modelID: "YourNewModelID" });
    console.log("Successfully loaded model:", response.modelID);
  } catch (e) {
    console.error("Failed to load model:", e.errorID, e.message);
  }
}
// Promise callbacks
apiClient
  .modelLoad({ modelID: "YourNewModelID" })
  .then((response) => {
    console.log("Successfully loaded model:", response.modelID);
  })
  .catch((e) => {
    console.error("Failed to load model:", e.errorID, e.message);
  });
```

> See [ApiClient](https://hawk.bar/VTubeStudioJS/classes/ApiClient.html) in the docs for all available calls, and review the official [VTube Studio API documentation](https://github.com/DenchiSoft/VTubeStudio) for more details on what calls are available and what each field means. In general, you pass an object representing the `data` property of the request to the library method, and get an object back representing the `data` property of the response. If the request `data` object is empty, the request method instead takes no parameters, and if the response `data` object is empty, the request method returns `Promise<void>`.

### Subscribing to API Events

To subscribe to VTube Studio events, such as the `ModelLoadedEvent`, call the functions in `apiClient.events`, like this:

```javascript
apiClient.events.modelLoaded.subscribe((data) => {
  // this callback will fire every time a model is loaded or unloaded in VTube Studio
  console.log("Model loaded:" + data.modelName);
});
```

### Setting API Call Options

An additional options object may be passed as the second parameter to control the execution of the API call. For example, to change the default timeout to 1 minute:

```javascript
const stats = await apiClient.statistics(undefined, { timeout: 1 * 60 * 1000 });
```

### WebSockets

The API client expects a WebSocket implementation to be available. In the browser, the native WebSocket will be used automatically. In NodeJS or other contexts, you must provide an external implementation, such as the one from the `ws` package. In this case, you can explicitly provide a `webSocketFactory` function, like so:

```javascript
const WebSocket = require("ws");

const options = {
  // ...
  webSocketFactory: (url) => new WebSocket(url),
};

const apiClient = new ApiClient(options);
```

## Examples

Examples are included in the repository's `examples` folder for a React app (created with `create-react-app`) and a plain NodeJS app.

## Breaking Changes in 3.2.0

Version `v3.2.0` contains a minor breaking change from previous versions:

- The `ApiClient` no longer attempts to import the `ws` package, due to complications with bundling for browser environments when the `ws` package is present in the package hierarchy. In NodeJS and other server environments, you must explicitly pass a WebSocket constructor via the `webSocketFactory` option. See the example above for reference.

## Breaking Changes in 3.x.x

Version `v3.x.x` contains several breaking changes from previous versions:

- The constructor for `ApiClient` was changed to take an options object, instead of the factory methods or a taking a message bus.
- The message bus abstractions (`WebSocketBus` and related functions) have been removed.
- The `ApiClient` automatically handles instantiating WebSockets, connecting to VTube Studio, and making authentication calls. These no longer need to be performed manually by plugin code.
- The object-oriented wrapper classes (`Plugin`, `CurrentModel`, `Expression`, etc.) have been fully removed. The automatic authentication flow provided by `Plugin` has been rolled into `ApiClient`.
- Error codes related to message bus failures (`MessageBusError`, and `MessageBusClosed`) have been removed, as WebSocket errors are now handled silently by `ApiClient`.
- The typings provided for API call methods that take no data payload now explicitly call for `undefined` as the data object instead of `void`, which would accept anything.

## Breaking Changes in 2.x.x

Version `v2.x.x` contains several breaking changes from previous versions:

- The constructor for `ApiClient` was marked as private, and replaced with two factory methods: `ApiClient.fromWebSocket(ws)` and `ApiClient.fromMessageBus(bus)`, to avoid unnecessarily exposing the end user to the library's message bus abstractions.
  - It is recommended that normal users should pass the websocket object to `ApiClient.fromWebSocket(ws)` directly instead of creating a `WebSocketBus` and passing that to `new ApiClient(bus)` or `ApiClient.fromMessageBus(bus)`.
- The object-oriented wrapper classes (`Plugin`, `CurrentModel`, `Expression`, etc.) have been deprecated, and will be removed in the next major version. The stateful nature of the wrappers implied that they were somehow synchronized with VTube Studio, but this was not the case. This became even more obvious with the introduction of highly dynamic concepts like Live2D items and expressions.
  - Users should switch to making calls to the API directly using the `ApiClient` class instead of the methods on `Plugin`. This unfortunately also includes handling the authentication workflow yourself for now. A basic example is provided above.
  - A future release may extend the `ApiClient` class to provide the automatic authentication handling that the `Plugin` class provided.
- The `MockApiServer` class has been removed (as well as the related class `EchoBus`). This class was not intended to be used in production code and was a poor substitute for testing against VTube Studio itself.
- Expression parameters were changed to use the new shape `{ name: string, value: number }` instead of the deprecated `{ id: string, target: number }`.
- The `ErrorCode` enum values `InternalClientError`, `MessageBusError`, and `MessageBusClosed` were changed to `-100`, -`101`, and `-102` to avoid conflicting with `-1` (used in the VTube Studio API to indicate the absence of an error).
  - Code using the enum itself should continue to work as expected, but any code directly checking for the error code numbers will need to be manually fixed.

## Contributing

Pull requests welcome! Please adhere to the linting and default formatting provided by the TypeScript compiler, and ensure that your code successfully compiles before opening your PR.

After cloning the repository or fetching the latest code:

```
npm install
```

Before opening a pull request, ensure your code compiles:

```
npm run build
```

## License

MIT © 2022 Joshua Thome
