Full plugin example

Every hook type in one plugin.

Complete v7 plugin with config, request, route, JSON, and CloudPanel hooks. Production source: plugins/hello ↗

use featherfly_plugin_sdk::{
    declare_plugin, hook, hook_cloudpanel, hook_config, hook_json, hook_request, log_info, route,
    write_cloudpanel_cancel, write_json_output, write_route_response, write_yaml_output,
    CloudPanelCommandContext, ConfigMutateContext, EventContext, HookResult, HostApi,
    JsonMutateContext, JsonMutateTarget, PluginEvent, RequestHookContext, RequestHookPhase,
    RouteHandlerContext, CONFIG_MUTATE_UNCHANGED, CLOUDPANEL_CONTINUE, HTTP_METHOD_GET,
    JSON_MUTATE_UNCHANGED, REQUEST_CONTINUE,
};

extern "C" fn init(host: *const HostApi) -> i32 {
    hook!(host, PluginEvent::DaemonStarted, on_started);
    hook_config!(host, on_config);
    hook_request!(host, RequestHookPhase::Intercept, "/api/*", on_intercept);
    hook_json!(host, JsonMutateTarget::ResponseBody, "/api/system", on_body);
    hook_cloudpanel!(host, on_cloudpanel);
    route!(host, HTTP_METHOD_GET, "/plugins/hello", on_route);
    unsafe { log_info(host, "ready") };
    0
}

extern "C" fn on_started(_ctx: *const EventContext) -> HookResult {
    HookResult::r#continue()
}

extern "C" fn on_config(ctx: *const ConfigMutateContext) -> i32 {
    let ctx = unsafe { &*ctx };
    let input = unsafe { std::slice::from_raw_parts(ctx.yaml_in_ptr, ctx.yaml_in_len) };
    if input.starts_with(b"# my-plugin\n") {
        return CONFIG_MUTATE_UNCHANGED;
    }
    let mut out = b"# my-plugin\n".to_vec();
    out.extend_from_slice(input);
    write_yaml_output(ctx, &out)
}

extern "C" fn on_intercept(_ctx: *const RequestHookContext) -> i32 {
    REQUEST_CONTINUE
}

extern "C" fn on_body(ctx: *const JsonMutateContext) -> i32 {
    let ctx = unsafe { &*ctx };
    let input = unsafe { std::slice::from_raw_parts(ctx.json_in_ptr, ctx.json_in_len) };
    let Ok(mut value) = serde_json::from_slice::<serde_json::Value>(input) else {
        return JSON_MUTATE_UNCHANGED;
    };
    if let Some(map) = value.as_object_mut() {
        map.insert("my_plugin".into(), true.into());
    }
    let Ok(out) = serde_json::to_vec(&value) else { return JSON_MUTATE_UNCHANGED };
    write_json_output(ctx, &out)
}

extern "C" fn on_cloudpanel(ctx: *const CloudPanelCommandContext) -> i32 {
    let ctx = unsafe { &*ctx };
    let command = unsafe { std::slice::from_raw_parts(ctx.command_ptr, ctx.command_len) };
    if command == b"site:delete" {
        return write_cloudpanel_cancel(ctx, b"site deletion disabled by my-plugin");
    }
    CLOUDPANEL_CONTINUE
}

extern "C" fn on_route(ctx: *const RouteHandlerContext) -> i32 {
    let ctx = unsafe { &*ctx };
    write_route_response(ctx, 200, br#"{"ok":true}"#)
}

declare_plugin! { name: "my-plugin", version: "0.1.0", init: init }