Nucleus.API
24.6.20-prerelease
Prefix Reserved
dotnet add package Nucleus.API --version 24.6.20-prerelease
NuGet\Install-Package Nucleus.API -Version 24.6.20-prerelease
<PackageReference Include="Nucleus.API" Version="24.6.20-prerelease" />
<PackageVersion Include="Nucleus.API" Version="24.6.20-prerelease" />
<PackageReference Include="Nucleus.API" />
paket add Nucleus.API --version 24.6.20-prerelease
#r "nuget: Nucleus.API, 24.6.20-prerelease"
#:package Nucleus.API@24.6.20-prerelease
#addin nuget:?package=Nucleus.API&version=24.6.20-prerelease&prerelease
#tool nuget:?package=Nucleus.API&version=24.6.20-prerelease&prerelease
Plugin API
The C# plugin API for NucleusBot Companion allows for customization and integration.
This API is designed with longevity in mind, and will avoid introducing break changes across versions.
Example Plugins
- Discord Call Overlay (TBA)
- BTTV Emotes (TBA)
Getting Started
Dependency
The Plugin API is published on NuGet, you can add it to your project by adding the following to your .csproj
- NuGet contains regular releases and prereleases for the API. Pre-Releases are designed for use during the Companion Development cycle and generally should not be used.
<PackageReference Include="Nucleus.API" Version="2022.9.18-prerelease">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Setting the Private
attribute to false
will disable copying a duplicate copy of the API as part of your Plugin, as the API is provided as part of the Companion Application
Your .csproj
file should look something like this:
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nucleus.API" Version="2022.9.18-prerelease">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Registration
In order for your DLL to be loaded you need to create a plugin.json
file in your plugins folder.
Below is the minimum required configuration
{
"schema": 1,
"id": "example-plugin",
"version": "1.0.0",
"name": "Example Plugin",
"entrypoints": [
"MyAssembly.dll"
]
}
The plugin.json
file can contain the following types:
Property | Type | Description |
---|---|---|
schema |
Number | Schema Version (Currently only supports 1 ) |
id |
String | Unique identifier for your Plugin |
version |
SemVer | Version of your plugin to determine updates |
name |
String | Name of your plugin that shows on the plugins page |
description |
String | Description of what your plugin does |
icon |
String | A local path to the icon used for your plugin |
entrypoints |
String / String[] | DLLs that should be loaded from your plugin folder |
contributors |
String / String[] | Contributors credited for the plugin |
contact |
Object | Contact details for support with your plugin |
license |
String | The license for your plugin (eg; MIT /GPL /other ) |
Main Class
You can start your plugin by creating a class
that implements the IPlugin
interface. Type
s that meet the following rules will automatically be called when your plugin DLL is loaded:
- Has to implement
IPlugin
- Cannot be static
- Cannot be an interface
- Cannot be abstract
If your class has a constructor that accepts an IPluginContext
, it will be passed in during initialization
class MyPlugin : IPlugin {
private readonly IPluginContext Context;
public MyPlugin(IPluginContext context) {
this.Context = context;
}
}
Registration
IPlugin
contains a Register
method that allows for asynchronous Plugin initialization after the constructor has been called.
class MyPlugin : IPlugin {
public async ValueTask Register() {
// ... Write your async code here
}
}
Disposing
When your Plugin is unloaded or disabled by the user, you can clean up any resources being used by your plugin by implementing IDisposable
or IAsyncDisposable
. Any Disposable objects returned by the IPluginContext
will be disposed of automatically after the Plugin class is disposed.
Web Plugins
The Companion app provides a local HTTP server to allow providing embeddable HTML/JavaScript pages (eg; for adding as OBS Browser Sources) using Blazor server/Razor pages.
To make sure that your Plugin has access to the tools necessary for creating razor pages, make sure that you change your Console Application
s .csproj
to use the Razor SDK:
<Project Sdk="Microsoft.NET.Sdk.Razor">
</Project>
Server Access
⚠️ The URL for accessing your plugins content will always start with
/{your-plugin-id}
The local HTTP Server runs on a variable port that the user can change at any time. You can get a URI
for pages created by your Plugin by using the IServerHelper
provided in the IPluginContext
IServerHelper helper = ...;
Uri uri = helper.GetPath<MyPageModel>();
string host = uri.Host;
int port = uri.Port;
string path = uri.PathAndQuery;
Blazor Pages
Plugins can make use of Blazor by creating .razor
files as part of your project. The Companion will load your compiled classes automatically
@page "my-page"
<html>
<head>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
Static content
If your plugin needs to host static CSS, HTML, Images, etc. you can do so by moving them into the www
folder of your plugin. Static files will become available at the root of your plugin /{your-plugin-id}
on the webserver.
If you're making use of Blazor pages and want to be able to live reload CSS files you can use the PluginSource
razor component. Modifying the content of the CSS file will trigger a File Watcher that will trigger a reload on that file.
@page "my-page"
<html>
<head>
<PluginSource Path="styles/my-style.css" />
</head>
</html>
Service Provider
Services that you configured using the IPluginContext
, as well as several services provided by the Companion, can be injected into your Razor pages if you need to access them from there
@page "my-page"
@inject IPluginContext Plugin
@inject IServerHelper ServerHelper
@inject IFileSystemHelper IO
@inject IComponentsHelper Components
If your pages need to access to variables provided by your Plugin classes, you can also inject those too.
MyCoolPlugin.class
class MyCoolPlugin { public readonly IList<object> SharedObject = new List<object>(); }
SomePage.razor
@page "my-page" @inject MyCoolPlugin Plugin @{ IList<object> items = this.Plugin.SharedObject; }
Commands
One core functionality present in many chat bots is Commands. The Companion app supports Auto-Complete for command in the Input box.
Slash commands
The Companion app supports handling chat commands as slash commands as long a handler has been created to receive the commands to a web API
// TODO: Write something here
throw new NotImplementedException();
Emotes
The Companion app is capable of supporting custom emotes. Emotes only require a name
and a url
(url
s must be loaded over HTTPS).
ValueTask Register() {
Uri uri = new Uri("https://test.dev/emotes");
// Register Emotes with the NucleusBot format
this.Context.RegisterEmotes(uri);
// Register Emotes with the NucleusBot format and add an Authorization header
// (If your API requires authorization to access files)
this.Context.RegisterEmotes(uri, headers => {
headers.Add("Authorization", "Bearer xxxxxxxxxxx");
});
// Register Emotes with a custom format
this.Context.RegisterEmotes(uri, converter: (content) => {
IList<Emote> emotes = new List<Emote>();
// If your 'content' is a JSONArray
if (content.TryParseJSON(out JsonArray? json)) {
// ... Parse your Emotes here
string name = "...";
string name = new Uri("https://...");
emotes.add(new Emote(name, uri));
}
return emotes;
});
// ... Or a mix of both
this.Context.RegisterEmotes(uri,
headers => {
// ... Add headers
},
content => {
// ... Parse the response body
}
);
return ValueTask.CompletedTask;
}
Scopes
Emotes
and Commands
can be registered in two different scopes:
- Globally
- Per Channel
Registration can be done on the IScopedRegistryContext
interface, which is implemented by both IPluginContext
(For Global) and IChannelContext
(For Channels).
Global Example:
Registering globally is done when Registering your plugin, or can be done later if the IPluginContext
is stored.
ValueTask Register() {
// Register your global emotes here
this.Context.RegisterEmotes(new Uri("https://test.dev/emotes"));
}
Channel Example:
Registering per channel is done after Joining to a channel. Emotes are stored on the channel for as long as it is Joined, and are automatically discarded when leaving the channel. Per-channel Emotes
and Commands
are only shown as long as that Channel is currently selected.
ValueTask Register() {
// Listen for Channel joins
this.Context.ChannelJoinEvent(channel => {
// Register channel emotes with the channel
channel.RegisterCommands(new Uri($"https://test.dev/emotes/{channel.Id}"));
});
}
Messages
Plugins are capable of updating, modifying, or completely cancelling chat messages. For example, if your plugin introduced new emotes, you'll need to replace the text contents with an Image
Middleware
To intercept messages you'll need to create a MessageHandler middleware in your Register
method.
ValueTask Register() {
this.Context.MessageHandlerMiddleware(async (context, next) => {
// Do what you need to do here
// Call the next middleware
await next();
});
}
Priority
Your Middleware can be assigned one of three priorities, FIRST
, INSERT
, or LAST
(The default is INSERT
).
- After all priority stages are completed (At the end of
LAST
) is when the Message is populated to the UI. After this point it cannot be modified or cancelled.
private async ValueTask MyMethod(IMessageContext context, Func<ValueTask> next)
=> await next();
ValueTask Register() {
// This will run last
this.Context.MessageHandlerMiddleware(MyMethod, Ordering.LAST);
// This will run first
this.Context.MessageHandlerMiddleware(MyMethod, Ordering.FIRST);
// This will run in the middle
this.Context.MessageHandlerMiddleware(MyMethod, Ordering.INSERT);
}
Priority | Description |
---|---|
FIRST |
Will run with highest priority. Anything that modifies the message should run here so that Middleware that reads the final message will do so after these modifications are run |
INSERT |
Runs based off Insert-Order , plugins that are loaded first will be called first. |
LAST |
Runs after all other processing is completed, if your plugin reads messages in their final form, it should be done here |
Circuits
Message middleware runs in a circuit. When the first Middleware is run it is passed a delegate next
which will begin the next middleware.
- If you do not invoke the
next
middleware, short-circuiting (see below) will occur. - Calling
next
multiple times may cause unintended side effects.- If the next middleware short-circuits, invoking
next
will fix the short-circuit - If short-circuiting has not occurred, invoking
next
will return aValueTask.CompletedTask
- If the next middleware short-circuits, invoking
Where you invoke next
may change how your middleware runs.
If you invoke next
at the start of your method, all other middleware will run (Including populating the message to the UI):
this.Context.MessageHandlerMiddleware(async (context, next) => {
await next();
// Do read-only operations here
});
Short-circuiting
It is possible to "short-circuit" the middleware by simply not calling the next
delegate. Doing so can prevent the message from populating in the UI, which may be the desired effect.
Content
A number of indexers and methods exist for updating the contents of a Message.
Components
Messages consist of Components
that are implemented by the main Companion application. Some examples of Components are Text
or Image
. Since components are only available as interfaces in the API, they must be constructed and accessed through the IPluginContext
(IPluginContext
provides an IComponentsHelper
interface)
IPluginContext context = ...;
IComponentsHelper components = context.Components;
// Text is created using a String
ITextComponent text = components.Text("This is text");
// Images require a "text" and an Image URI
IImageComponent image = components.Image("Kappa", new Uri("https://static-cdn.jtvnw.net/emoticons/v2/25/default/dark/2.0"));
// Text and Image are both `IComponent`s
IComponent component = text;
Ranges
Once we have an IComponent
, sections of messages can easily be replaced using a Range
.
async (context, next) => {
// This will replace the start of every message with "Neat!"
context.Contents[..4] = components.Text("Neat!");
};
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 7.0.0)
- TheElm.Text.Json (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
24.6.20-prerelease | 93 | 6/20/2024 |
24.2.2-prerelease | 87 | 2/2/2024 |
23.11.12-prerelease | 79 | 11/12/2023 |
23.11.11-prerelease | 81 | 11/11/2023 |
23.11.10-prerelease2 | 80 | 11/10/2023 |
23.11.10-prerelease | 76 | 11/10/2023 |
23.10.26-prerelease | 95 | 10/26/2023 |
23.10.21-prerelease | 114 | 10/21/2023 |
23.10.12-prerelease2 | 98 | 10/12/2023 |
23.10.12-prerelease | 91 | 10/12/2023 |
23.10.11-prerelease3 | 94 | 10/11/2023 |
23.10.11-prerelease2 | 91 | 10/11/2023 |
23.10.11-prerelease | 101 | 10/11/2023 |
23.10.9-prerelease | 95 | 10/9/2023 |
23.10.8-prerelease | 93 | 10/8/2023 |
23.10.6-prerelease | 97 | 10/6/2023 |
23.10.3-prerelease | 95 | 10/3/2023 |
23.9.29-prerelease | 90 | 9/29/2023 |
23.9.22-prerelease | 94 | 9/22/2023 |