Nucleus.API 24.6.20-prerelease

Prefix Reserved
This is a prerelease version of Nucleus.API.
dotnet add package Nucleus.API --version 24.6.20-prerelease
                    
NuGet\Install-Package Nucleus.API -Version 24.6.20-prerelease
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Nucleus.API" Version="24.6.20-prerelease" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nucleus.API" Version="24.6.20-prerelease" />
                    
Directory.Packages.props
<PackageReference Include="Nucleus.API" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Nucleus.API --version 24.6.20-prerelease
                    
#r "nuget: Nucleus.API, 24.6.20-prerelease"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Nucleus.API@24.6.20-prerelease
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Nucleus.API&version=24.6.20-prerelease&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Nucleus.API&version=24.6.20-prerelease&prerelease
                    
Install as a Cake Tool

Nuget (with prereleases) Nuget Discord

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. Types 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 Applications .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 (urls 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 a ValueTask.CompletedTask

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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