Skip to content

Commit

Permalink
add docs for shelter injector integration
Browse files Browse the repository at this point in the history
  • Loading branch information
yellowsink committed Sep 20, 2024
1 parent 9e8bea7 commit 83cdee9
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/shelter-docs/docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default defineConfig({
{ text: "Patterns", link: "/guides/patterns" },
{ text: "Ideals", link: "/guides/ideals" },
{ text: "Background", link: "/guides/background" },
{ text: "Injector Integration", link: "/guides/injectors" },
],
},
],
Expand Down
157 changes: 157 additions & 0 deletions packages/shelter-docs/docs/guides/injectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# shelter Injector Integration

shelter, as a client mod, provides a lot of functionality that is useful to writers of custom clients and the like.

Examples of things you may do, which uses (as of writing) shelter to:
- display its settings ui in a stable way
- fix some screenshare issues with your custom discord client
- reimplement RPC, perhaps using arRPC

Whatever you want to do, shelter can provide you with very stable settings UI capability and a platform to inject
custom code into discord from, which can be more convenient than somehow injecting your own modifications,
especially since you get a robust set of APIs to build your tweaks on top of!

To better facilitate this, shelter has some special integration options for use in custom clients or other injectors,
to make the experience less buggy and more seamless for the end user.

## Injector Settings Sections

shelter can [inject sections](/reference#shelter-settings) into the sidebar of the Discord user settings.

Without custom integration, it is very hard to predict where your settings will show, and making it a separate zoned
off section is very difficult to do well, if not impossible.

To fix this issue, shelter provides _injector sections_, which are displayed above shelter's sections, and above all
plugin's sections too.

To use them you need to do one of two things: provide via a global, or use [Injector Plugins](#injector-plugins).

### Settings with Injector Plugins

From within injector plugins, all settings sections created via the usual APIs go to injector sections.
That's it. Easy.
This includes via the scoped API.

You also have access to one special new API: `shelter.settings.setInjectorSections`.

This overwrites the injector settings with the same content that you would set on `SHELTER_INJECTOR_SETTINGS`.

Note that this will override anything added with `registerSection`, but is useful if you want to set a lot of sections at once.

### Settings with a Global

Note that this method is flawed in a couple of ways, most chiefly that you don't have access to solidjs at this point.

To provide a global, you must ensure that `SHELTER_INJECTOR_SETTINGS` is defined at shelter init and follows the structure:
```ts
// this is the same as the standard settings API
type SettingsSection =
| ["divider"]
| ["header", string]
| ["section", string, string, Component, object?]
| ["button", string, string, () => void];

let SHELTER_INJECTOR_SETTINGS: SettingsSection[] | solidjs.Accessor<SettingsSection[]>;
```

If it is an array, it will be set at shelter init time.
If a solidjs accessor (for a signal), it can be changed later and will be reactively updated.

Example:
```js
window.SHELTER_INJECTOR_SETTINGS = [
["divider"]
["header", "My Client"],
["section", "myclient_settings", "Settings",
() => {
// solidjs component here
const [toggle, setToggle] = shelter.solid.createSignal();
// have fun without jsx i guess - thats the other of the flaws i mentioned
return document.createElement("div");
}
],
["buttton", "myclient_clog", "Changelog", MyClient.ShowChangelogModal]
];

(0,eval)(shelter);
```

You can also keep this off the global:
```js
new Function("SHELTER_INJECTOR_SECTIONS", shelter)([/*...*/]);
```

## Injector Plugins

You may wish to use shelter plugins as a base to build your functionality upon.
We have special support for injector supplied plugins, that allow you more control over how they behave.

You can do the following things:
- Ensure that your plugins exist at startup and work as you expect
- Hide the plugins from view
- Disable all plugin actions except showing the settings modal (if you wanted to disable that, just don't provide one!)
- Set injector user settings sections

When your injector plugins are loaded, if they are new, they will be created and turned on.
If they existed already, they will be overwritten with the following exceptions:
- [`shelter.plugin.store` content](/reference#shelter-plugin-store) is kept
- if the following conditions are met, the plugin's on/off state will be kept, else it is just forced on:
* the plugin is visible to the user (not hidden)
* the user is allowed to toggle the plugin themselves

Note that while functionality can be disabled in the settings UI, the `shelter.plugins` APIs will completely ignore your
prohibitions - hidden plugins will be accessible, and disallowed actions can be performed.
This is a purposeful choice - injector plugins are not here to break stuff,
they're here to stop the 99% of users shooting themselves in the foot, and provide you a nicer integration experience.

Remote injector plugins ALWAYS have auto-update enabled.

### Setting Up Injector Plugins

You set up your injector plugins via a global called `SHELTER_INJECTOR_PLUGINS`, that must be defined at shelter load.
It must match this type:
```ts
type InjectorIntegrationOptions = {
// is the plugin visible in shelter's plugin list?
isVisible: boolean;
// what actions are visible to the user on the plugin card
// irrelevant if hidden but MUST be provided nonetheless
// opt-in so that future new options are hidden for your plugins by default.
allowedActions: { toggle?: true; delete?: true; edit?: true, update?: true };
};

type RemotePlugin = [string, InjectorIntegrationOptions]; // string is the plugin url
type LocalPlugin = {
js: string;
manifest: Record<string, string>; // expected: name, author, description
injectorIntegration: InjectorIntegrationOptions
};

let SHELTER_INJECTOR_PLUGINS: Record<string, RemotePlugin | LocalPlugin>;
```

For example:
```js
window.SHELTER_INJECTOR_PLUGINS = {
// obviously in a client you're unlikely to use both remote and local together, but we do here for education purposes
"myclient-settings": ["myclient://shelter-plugins/settings.js", { isVisible: false, allowedActions: {} }],
"myclient-rpc": {
js: MyClient.ShelterPlugins.Rpc.CodeText,
manifest: {
name: "MyClient RPC Fix",
author: "MyClient Authors",
description: "Provides Rich Presence support for MyClient"
},
injectorIntegration: {
isVisible: true,
allowedActions: { toggle: true }
}
}
};
(0, eval)(shelter);
```

Alternatively, you need not use window, to keep it all private:
```js
new Function("SHELTER_INJECTOR_PLUGINS", shelter)({/*...*/});
```
30 changes: 30 additions & 0 deletions packages/shelter-docs/docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,15 @@ function addRemotePlugin(id: string, src: string, update = true): Promise<void>

`addRemotePlugin` installs a plugin from a URL.

### `shelter.plugins.updatePlugin`

```ts
function updatePlugin(id: string): Promise<boolean>
```

`updatePlugin` checks for an update, and if there is one, updates the plugin.
It returns whether or not an update was installed.

### `shelter.plugins.removePlugin`

```ts
Expand All @@ -776,6 +785,27 @@ function getSettings(id: string): solid.Component | undefined

`getSettings` grabs the Solid settings component for the plugin, if the plugin has settings.

### `shelter.plugins.showSettingsFor`

```ts
function showSettingsFor(id: string): Promise<void>
```

Shows a settings modal for the plugin of the given ID.
The returned promise resolves when the modal is closed.

### `shelter.plugins.editPlugin`

```ts
function editPlugin(id: string, overwrite: StoredPlugin): void
```

`editPlugin` edits a plugin. Pass a new plugin object to overwrite the old one.

Note that this is a little more basic than you may expect - the edit modal plays some extra tricks,
notably, the auto-updating behaviour for remote plugins is not provided by this function,
and the edit modal does it in a cleverer way than just calling this then update, too.

## Plugin exports

These are not what shelter provides to you, but rather what your plugin should provide back to shelter.
Expand Down
2 changes: 1 addition & 1 deletion packages/shelter/src/components/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const PluginCard: Component<{

const isDev = () => props.id === devModeReservedId;

const ldi = props.plugin.loaderIntegration;
const ldi = props.plugin.injectorIntegration;

return (
<div class={classes.plugin}>
Expand Down
14 changes: 7 additions & 7 deletions packages/shelter/src/plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type StoredPlugin = {
local: boolean;
manifest: Record<string, string>;
// non existent for normal plugins
loaderIntegration?: LoaderIntegrationOpts;
injectorIntegration?: LoaderIntegrationOpts;
};

export type EvaledPlugin = {
Expand Down Expand Up @@ -67,9 +67,9 @@ function createStorage(pluginId: string): [Record<string, any>, () => void] {
];
}

function createPluginApi(pluginId: string, { manifest, loaderIntegration }: StoredPlugin) {
function createPluginApi(pluginId: string, { manifest, injectorIntegration }: StoredPlugin) {
const [store, flushStore] = createStorage(pluginId);
const scoped = createScopedApiInternal(window["shelter"].flux.dispatcher, !!loaderIntegration);
const scoped = createScopedApiInternal(window["shelter"].flux.dispatcher, !!injectorIntegration);

return {
store,
Expand Down Expand Up @@ -102,7 +102,7 @@ export function startPlugin(pluginId: string) {
};

// injector plugins have superpowers
if (data.loaderIntegration)
if (data.injectorIntegration)
shelterPluginEdition.settings = {
...shelterPluginEdition.settings,
setInjectorSections,
Expand Down Expand Up @@ -221,7 +221,7 @@ export function addLocalPlugin(id: string, plugin: StoredPlugin) {
throw new Error("plugin ID invalid or taken");

if (!plugin.local) plugin.local = true;
delete plugin.loaderIntegration;
delete plugin.injectorIntegration;

if (
typeof plugin.js !== "string" ||
Expand Down Expand Up @@ -304,7 +304,7 @@ export async function ensureLoaderPlugin(id: string, plugin: [string, LoaderInte
await Promise.all([waitInit(internalData), waitInit(pluginStorages)]);

const isRemote = Array.isArray(plugin);
const integration = isRemote ? plugin?.[1] : plugin?.loaderIntegration;
const integration = isRemote ? plugin?.[1] : plugin?.injectorIntegration;

if (typeof integration?.isVisible !== "boolean")
throw new Error("cannot add a loader plugin without an isVisible setting");
Expand Down Expand Up @@ -348,7 +348,7 @@ export async function ensureLoaderPlugin(id: string, plugin: [string, LoaderInte
// replace object to force db write
internalData[id] = {
...internalData[id],
loaderIntegration: integration,
injectorIntegration: integration,
};
}

Expand Down

0 comments on commit 83cdee9

Please sign in to comment.