Plug-in API
An event-driven C API for extending the hub.
Each plug-in registers a set of callback functions for handling events when it is loaded. Plug-ins can also register user commands that show up in the hub for users to interact with — see the example plug-in for how. The full API is defined in the source code.
uhub 0.8.0 uses plug-in API version 8, with a minimum of 8. The hub refuses
to load a plug-in built against an older version. The breaking change is that
struct plugin_user is now opaque — see
Reading user properties. If you would rather write hub logic in
a scripting language, see the JavaScript plug-in API.
Creating a plug-in
A plug-in must implement at least two functions, called when loading and unloading it:
/**
* Implemented by the plugin.
*
* @param handle[out] Sets all information by the plugin
* @param config A configuration string
* @return 0 on success, -1 on error.
*/
int plugin_register(struct plugin_handle* handle, const char* config);
/** @return 0 on success, -1 on error. */
int plugin_unregister(struct plugin_handle*);
The configuration string is passed verbatim from the plug-in configuration file.
struct plugin_handle
The plugin_handle struct is passed to every callback. It exposes hub functions
(hub) for the plug-in to use, and a data pointer (ptr) to anchor
memory the plug-in allocates.
struct plugin_handle
{
struct uhub_plugin* handle;
const char* name;
const char* version;
const char* description;
void* ptr;
const char* error_msg;
size_t plugin_api_version;
size_t plugin_funcs_size;
struct plugin_funcs funcs;
struct plugin_hub_funcs hub;
};
plugin_register() is the only function allowed to modify most of the struct,
conveniently done with the PLUGIN_INITIALIZE macro:
int plugin_register(struct plugin_handle* handle, const char* config)
{
PLUGIN_INITIALIZE(handle, "test", "1.0", "A simple test plug-in");
return 0;
}
Registering callbacks
Set the relevant function pointer in the funcs member of plugin_handle.
For example, a plug-in that prints to the console when users join the hub:
static void event_user_login(struct plugin_handle* plugin, struct plugin_user* user)
{
printf("User \"%s\" logged in.\n", plugin->hub.get_user_nick(plugin, user));
}
int plugin_register(struct plugin_handle* handle, const char* config)
{
PLUGIN_INITIALIZE(handle, "test", "1.0", "A simple test plug-in");
handle->funcs.on_user_login = event_user_login;
return 0;
}
See the full list of available callbacks in
struct plugin_funcs.
Four hooks that were declared but never fired before 0.8.0 —
on_hub_started, on_hub_shutdown, on_check_ip_late and
on_change_nick — now do, and on_validate_nick /
on_validate_cid are wired into the login pipeline.
Reading user properties
Changed in API 8. struct plugin_user is opaque: a plug-in can no
longer reach into its fields, and reads go through accessors on plugin->hub.
This is what lets the hub change its internal user representation without breaking every
plug-in in existence.
/* Before (API 7 and earlier) */
const char* nick = user->nick;
/* Now (API 8) */
const char* nick = plugin->hub.get_user_nick(plugin, user);
| Accessor | Returns |
|---|---|
get_user_sid | Session ID. Recycled as users come and go. |
get_user_nick | Nickname. |
get_user_cid | Client ID. |
get_user_user_agent | Client user-agent string. |
get_user_address | const struct ip_addr_encap* for the connection. |
get_user_credentials | enum auth_credentials. |
get_user_connection_id | A stable, non-recycled uint64_t — the safe key for correlating a user across callbacks. |
get_tls_version | Negotiated TLS version, e.g. "TLSv1.3", or NULL if not over TLS. |
Strings and addresses returned by these accessors point into the hub's own storage and are
valid for the duration of the callback only. Copy anything that must
outlive the call. All of them tolerate a NULL user, returning
""/NULL/0, so they can be chained without a guard.
Per-user storage
New in API 6. Each plug-in gets one opaque pointer per user, keyed by its own handle, with a cleanup function. The cleanup is guaranteed to run exactly once — when the value is replaced or cleared, when the user is destroyed, or when the plug-in is unloaded while the user is still connected — so a plug-in no longer has to track logouts to free per-user state.
struct user_info { int warnings; };
static void free_user_info(struct plugin_handle* plugin, void* data)
{
hub_free(data);
}
static struct user_info* get_info(struct plugin_handle* plugin, struct plugin_user* user)
{
struct user_info* info = plugin->hub.get_user_data(plugin, user);
if (!info)
{
info = hub_malloc_zero(sizeof(*info));
plugin->hub.set_user_data(plugin, user, info, free_user_info);
}
return info;
}
Pass set_user_data(plugin, user, NULL, NULL) to clear the slot; setting over an
existing value runs the previous value's cleanup first.
Hub functions
The hub member of plugin_handle is the plug-in's way to act on the
hub. Alongside the accessors above and the existing messaging, command and hub-name functions,
API 8 adds:
| Function | Description |
|---|---|
ban_user(plugin, user, seconds, reason) | Ban by CID and nick, disconnect, persist through a storage plug-in and propagate to linked hubs. seconds <= 0 is permanent; reason may be NULL. |
unban(plugin, target) | Lift a ban by nick, CID or IP/range, cluster-wide. |
auth_get_user / auth_register_user / auth_update_user | Read and write stored accounts through whichever auth storage plug-in is loaded. This is how mod_selfregister works without owning any storage. |
send_rich_message(plugin, user, message) | Send a message marked as rich text (RTF0), the body being CommonMark. |
user_supports_rich_text(plugin, user) | Whether this client negotiated RTF0 and the hub allows rich text. Branch on it whenever the plain and rich forms would differ. |
Storage plug-ins
A plug-in becomes an authentication storage backend by implementing the
auth_* callbacks in plugin_funcs, and a ban storage backend
by implementing auth_ban_add, auth_ban_del and
auth_is_banned. The hub is the only caller: it writes bans through as they are
created and queries at login. With no plug-in implementing them, bans live only in the hub's
in-memory ACL and are lost on restart —
mod_auth_sqlite is the reference implementation,
with per-ban expiry and reason.