mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-26 09:00:57 -05:00
feat(experiment): basic plugin API
This commit is contained in:
parent
f9f0d5c55a
commit
b3be822568
3 changed files with 263 additions and 3 deletions
|
@ -17,6 +17,7 @@ import Layout from "./stores/Layout";
|
||||||
import LocaleOptions from "./stores/LocaleOptions";
|
import LocaleOptions from "./stores/LocaleOptions";
|
||||||
import MessageQueue from "./stores/MessageQueue";
|
import MessageQueue from "./stores/MessageQueue";
|
||||||
import NotificationOptions from "./stores/NotificationOptions";
|
import NotificationOptions from "./stores/NotificationOptions";
|
||||||
|
import Plugins from "./stores/Plugins";
|
||||||
import ServerConfig from "./stores/ServerConfig";
|
import ServerConfig from "./stores/ServerConfig";
|
||||||
import Settings from "./stores/Settings";
|
import Settings from "./stores/Settings";
|
||||||
import Sync, { Data as DataSync, SyncKeys } from "./stores/Sync";
|
import Sync, { Data as DataSync, SyncKeys } from "./stores/Sync";
|
||||||
|
@ -39,10 +40,13 @@ export default class State {
|
||||||
queue: MessageQueue;
|
queue: MessageQueue;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
sync: Sync;
|
sync: Sync;
|
||||||
|
plugins: Plugins;
|
||||||
|
|
||||||
private persistent: [string, Persistent<unknown>][] = [];
|
private persistent: [string, Persistent<unknown>][] = [];
|
||||||
private disabled: Set<string> = new Set();
|
private disabled: Set<string> = new Set();
|
||||||
|
|
||||||
|
client?: Client;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct new State.
|
* Construct new State.
|
||||||
*/
|
*/
|
||||||
|
@ -57,6 +61,7 @@ export default class State {
|
||||||
this.queue = new MessageQueue();
|
this.queue = new MessageQueue();
|
||||||
this.settings = new Settings();
|
this.settings = new Settings();
|
||||||
this.sync = new Sync(this);
|
this.sync = new Sync(this);
|
||||||
|
this.plugins = new Plugins(this);
|
||||||
|
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
this.register();
|
this.register();
|
||||||
|
@ -118,6 +123,11 @@ export default class State {
|
||||||
* @returns Function to dispose of listeners
|
* @returns Function to dispose of listeners
|
||||||
*/
|
*/
|
||||||
registerListeners(client?: Client) {
|
registerListeners(client?: Client) {
|
||||||
|
if (client) {
|
||||||
|
this.client = client;
|
||||||
|
this.plugins.onClient(client);
|
||||||
|
}
|
||||||
|
|
||||||
const listeners = this.persistent.map(([id, store]) => {
|
const listeners = this.persistent.map(([id, store]) => {
|
||||||
return reaction(
|
return reaction(
|
||||||
() => stringify(store.toJSON()),
|
() => stringify(store.toJSON()),
|
||||||
|
@ -191,7 +201,10 @@ export default class State {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => listeners.forEach((x) => x());
|
return () => {
|
||||||
|
delete this.client;
|
||||||
|
listeners.forEach((x) => x());
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -228,6 +241,9 @@ export default class State {
|
||||||
|
|
||||||
// Dump stores back to disk.
|
// Dump stores back to disk.
|
||||||
await this.save();
|
await this.save();
|
||||||
|
|
||||||
|
// Post-hydration, init plugins.
|
||||||
|
this.plugins.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,16 @@ import Store from "../interfaces/Store";
|
||||||
/**
|
/**
|
||||||
* Union type of available experiments.
|
* Union type of available experiments.
|
||||||
*/
|
*/
|
||||||
export type Experiment = "dummy" | "offline_users";
|
export type Experiment = "dummy" | "offline_users" | "plugins";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently active experiments.
|
* Currently active experiments.
|
||||||
*/
|
*/
|
||||||
export const AVAILABLE_EXPERIMENTS: Experiment[] = ["dummy", "offline_users"];
|
export const AVAILABLE_EXPERIMENTS: Experiment[] = [
|
||||||
|
"dummy",
|
||||||
|
"offline_users",
|
||||||
|
"plugins",
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Definitions for experiments listed by {@link Experiment}.
|
* Definitions for experiments listed by {@link Experiment}.
|
||||||
|
@ -32,6 +36,11 @@ export const EXPERIMENTS: {
|
||||||
description:
|
description:
|
||||||
"If you can take the performance hit (for example, you're on desktop), you can re-enable offline users for big servers such as Revolt Lounge.",
|
"If you can take the performance hit (for example, you're on desktop), you can re-enable offline users for big servers such as Revolt Lounge.",
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
title: "Experimental Plugin API",
|
||||||
|
description:
|
||||||
|
"This will enable the experimental plugin API. Only touch this if you know what you're doing.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
|
|
235
src/mobx/stores/Plugins.ts
Normal file
235
src/mobx/stores/Plugins.ts
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
import localforage from "localforage";
|
||||||
|
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||||
|
import { Client } from "revolt.js";
|
||||||
|
|
||||||
|
import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
|
import State from "../State";
|
||||||
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
|
type Plugin = {
|
||||||
|
/**
|
||||||
|
* Plugin Format Revision
|
||||||
|
*/
|
||||||
|
format: 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semver Version String
|
||||||
|
*/
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Namespace
|
||||||
|
*
|
||||||
|
* This will usually be the author's name.
|
||||||
|
*/
|
||||||
|
namespace: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Id
|
||||||
|
*
|
||||||
|
* This should be a valid URL slug, i.e. cool-plugin.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entrypoint
|
||||||
|
*
|
||||||
|
* Valid Javascript code, must be function which returns object.
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* function (state: State) {
|
||||||
|
* return {
|
||||||
|
* onClient: (client: Client) => {},
|
||||||
|
* onUnload: () => {}
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
entrypoint: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this plugin is enabled
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Instance = {
|
||||||
|
format: 1;
|
||||||
|
onClient?: (client: Client) => {};
|
||||||
|
onUnload?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example plugin:
|
||||||
|
// state.plugins.add({ format: 1, version: "0.0.1", namespace: "insert", id: "my-plugin", entrypoint: "(state) => { console.log('[my-plugin] Plugin init!'); return { onClient: c => console.log('[my-plugin] Acquired Client:', c, '\\nHello', c.user.username + '!'), onUnload: () => console.log('[my-plugin] bye!') } }" })
|
||||||
|
|
||||||
|
export interface Data {
|
||||||
|
"revite:plugins": Record<string, Plugin>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles loading and persisting plugins.
|
||||||
|
*/
|
||||||
|
export default class Plugins implements Store, Persistent<Data> {
|
||||||
|
private state: State;
|
||||||
|
|
||||||
|
private plugins: ObservableMap<string, Plugin>;
|
||||||
|
private instances: Map<string, Instance>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct new Draft store.
|
||||||
|
*/
|
||||||
|
constructor(state: State) {
|
||||||
|
this.plugins = new ObservableMap();
|
||||||
|
this.instances = new Map();
|
||||||
|
makeAutoObservable(this);
|
||||||
|
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "revite:plugins";
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
"revite:plugins": mapToRecord(this.plugins),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@action hydrate(data: Data) {
|
||||||
|
Object.keys(data["revite:plugins"]).forEach((key) =>
|
||||||
|
this.plugins.set(key, data["revite:plugins"][key]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin by id
|
||||||
|
* @param namespace Namespace
|
||||||
|
* @param id Plugin Id
|
||||||
|
*/
|
||||||
|
@computed get(namespace: string, id: string) {
|
||||||
|
return this.plugins.get(namespace + "/" + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an existing instance of a plugin
|
||||||
|
* @param plugin Plugin Information
|
||||||
|
* @returns Plugin Instance
|
||||||
|
*/
|
||||||
|
private getInstance(plugin: Pick<Plugin, "namespace" | "id">) {
|
||||||
|
return this.instances.get(plugin.namespace + "/" + plugin.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise all plugins
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
if (!this.state.experiments.isEnabled("plugins")) return;
|
||||||
|
this.plugins.forEach(
|
||||||
|
({ namespace, id, enabled }) => enabled && this.load(namespace, id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a plugin
|
||||||
|
* @param plugin Plugin Manifest
|
||||||
|
*/
|
||||||
|
add(plugin: Plugin) {
|
||||||
|
if (!this.state.experiments.isEnabled("plugins"))
|
||||||
|
return console.error("Enable plugins in experiments!");
|
||||||
|
|
||||||
|
let loaded = this.getInstance(plugin);
|
||||||
|
if (loaded) {
|
||||||
|
this.unload(plugin.namespace, plugin.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plugins.set(plugin.namespace + "/" + plugin.id, plugin);
|
||||||
|
|
||||||
|
if (typeof plugin.enabled === "undefined" || plugin) {
|
||||||
|
this.load(plugin.namespace, plugin.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a plugin
|
||||||
|
* @param namespace Plugin Namespace
|
||||||
|
* @param id Plugin Id
|
||||||
|
*/
|
||||||
|
remove(namespace: string, id: string) {
|
||||||
|
this.unload(namespace, id);
|
||||||
|
this.plugins.delete(namespace + "/" + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a plugin
|
||||||
|
* @param namespace Plugin Namespace
|
||||||
|
* @param id Plugin Id
|
||||||
|
*/
|
||||||
|
load(namespace: string, id: string) {
|
||||||
|
let plugin = this.get(namespace, id);
|
||||||
|
if (!plugin) throw "Unknown plugin!";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let ns = plugin.namespace + "/" + plugin.id;
|
||||||
|
|
||||||
|
let instance: Instance = eval(plugin.entrypoint)();
|
||||||
|
this.instances.set(ns, {
|
||||||
|
...instance,
|
||||||
|
format: plugin.format,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.plugins.set(ns, {
|
||||||
|
...plugin,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.client) {
|
||||||
|
instance.onClient?.(this.state.client);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load ${namespace}/${id}!`);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unload a plugin
|
||||||
|
* @param namespace Plugin Namespace
|
||||||
|
* @param id Plugin Id
|
||||||
|
*/
|
||||||
|
unload(namespace: string, id: string) {
|
||||||
|
let plugin = this.get(namespace, id);
|
||||||
|
if (!plugin) throw "Unknown plugin!";
|
||||||
|
|
||||||
|
let ns = plugin.namespace + "/" + plugin.id;
|
||||||
|
let loaded = this.getInstance(plugin);
|
||||||
|
if (loaded) {
|
||||||
|
loaded.onUnload?.();
|
||||||
|
this.plugins.set(ns, {
|
||||||
|
...plugin,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset everything
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
localforage.removeItem("revite:plugins");
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push client through to plugins
|
||||||
|
*/
|
||||||
|
onClient(client: Client) {
|
||||||
|
for (const instance of this.instances.values()) {
|
||||||
|
instance.onClient?.(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue