notifier plugin - degoya/rDownloader GitHub Wiki

Writing a notification destination

A notification destination delivers rDownloader's messages to one more service — a chat, a push service, a server of the person's own. It delivers one message and reports how it went; retries, backoff and quiet hours belong to the application.

Scaffold rdownloader plugin new --type notifier
plugin_type notifier
World notifier-plugin
Exports deliver
Imports beyond host and http destination-settings
Template sdk/templates/notifier
Contract Interface notifier

What the host promises

  • Retrying is not your decision. Report a failure and say what kind it is; the delivery hub applies its own backoff and quiet hours, the same ones the built-in webhook uses.
  • A secret reaches your request without reaching you. Put {{secret}} in a header, a query value or the body and the host substitutes the destination's token on the way out.
  • Only a few headers are sent at all — authorization, content-type, accept and a handful more; anything else goes in the query or the body.

Permissions

The scaffold asks for net_http with the service's API. A destination that may live on a server of the person's own adds * beside the services it names; the host then narrows every delivery to the host its destination names, and requires https outside the person's own network.

[extension] claims stays empty: a destination is chosen by name when a notification target is created.

Settings of a target

A destination can let each target choose among fixed values — ntfy's priority per severity is one — or take free text, such as the path mapping of the media-server destinations. Declare each setting in the manifest:

[[extension.settings]]
name = "priority_info"                  # a-z, 0-9 and _
choices = ["1", "2", "3", "4", "5"]     # every value offered, in this order
default = "2"                           # optional; without it the setting may stay unset

A setting without choices is free text: one line of at most 256 characters, or several lines with multiline = true (4096 together), no control character but the line breaks; a default follows the same rule.

[[extension.settings]]
name = "path_mapping"
multiline = true                        # a text area; without it a one-line field

The target editor offers a setting with choices as a select, free text as a field or a text area. Its label is your own code <slug>.setting.<name> in locales/*.json (an optional hint <slug>.setting.<name>.description), each value's label <slug>.choice.<value>; without them the name and the value are shown. Saving a target refuses a name you did not declare, a value you do not offer and free text outside the rule; an empty value leaves the setting at its default. During deliver, ask the host through the interface destination-settings:

use rdownloader::plugin::destination_settings;

let priority = destination_settings::setting("priority_info"); // Option<String>

You hear the target's value, otherwise your default, otherwise None. Read None and anything unexpected as "use the default" rather than failing the delivery. At most 16 settings, each with 1–32 choices or none.

The finished package's folder

For package_completed, notification.package-path names the folder of the finished package as rDownloader sees it — its destination, where post-processing left the files; it is none for every other event and for a test message. A retry, after a restart too, names the same folder. Its last segments are a release name someone else wrote, so the host makes its braces inert like those of the title; a path that held one is no longer the folder on disk, and a plugin that maps it onto another server should not use it. The bundled Plex, Jellyfin and Emby destinations map it through their free-text path_mapping setting onto the path the media server sees.

Ask for exactly what you use: the install preview shows every grant, and a component importing an interface its manifest does not grant cannot be instantiated. Every capability is listed under Capabilities.

Step by step

1. Scaffold it

plugin new reads its templates from sdk/templates, so run it from a checkout of the repository:

git clone https://github.com/degoya/rDownloader.git
cd rDownloader
rdownloader plugin new --type notifier --out ../mynotifier
cd ../mynotifier

Without an installation of rDownloader, the packaging tool rd-pack — cargo build --profile release-test -p rd-pack in the checkout — takes the same arguments: target/release-test/rd-pack plugin new --type notifier --out ../mynotifier.

The directory name becomes the crate name and the plugin's slug (--name sets another). The scaffold gets its own plugin id, its own copy of the contract in wit/rdownloader.wit and its own Ed25519 key pair: plugin-signing.key, whose public half is already in manifest.toml. Keep that file out of version control.

2. Make it yours

  • src/lib.rs builds the address and the JSON body and decides how an answer is treated — delivered, retry, unauthorised or refused.
  • src/guest.rs holds deliver.
  • manifest.toml: [capabilities.net_http] domains.
  • locales/en.json: the English text of every failure code you report.

Change description and author under [metadata], and read the comment at the top of src/lib.rs: it states what the host promises this type.

3. Test it

cargo test

The unit tests need no WebAssembly toolchain: only src/guest.rs is wasm32-specific. Give every answer of your provider you read a test of its own.

4. Build the component

cargo build --release --target wasm32-unknown-unknown
wasm-tools component new target/wasm32-unknown-unknown/release/mynotifier.wasm -o target/mynotifier.wasm

This needs the wasm32-unknown-unknown target and wasm-tools 1.261.0, the version the bundled plugins are made with: cargo build makes a core module that carries the world src/guest.rs generates, and wasm-tools component new turns it into the component. The plugin is built without WASI: a component importing anything outside rdownloader:plugin is refused — see Building without WASI.

5. Package and sign it

rdownloader plugin package --manifest manifest.toml \
  --component target/mynotifier.wasm \
  --locales locales --key plugin-signing.key --output mynotifier.rdplug
rdownloader plugin conformance mynotifier.rdplug --json

conformance answers whether this application would run the package; it has to be clean before you publish. For a key that outlives the scaffold, rdownloader plugin keygen --role plugin --output ./keys writes a pair: the Base64 in the .pub file goes into public_key, and key_id gets a name of your own. In CI, RDOWNLOADER_PLUGIN_SIGNING_KEY holding the private key's PEM text replaces --key. Details: Packaging, signing and distribution.

6. Install and trust it

For a first try without signing, package with --development instead of --key and start rdownloader serve --plugin-development-mode, the only mode that accepts an unsigned package.

Install a signed package under Settings → Plugins → Add as an upload, or with rdownloader plugin install mynotifier.rdplug --root data/plugins. Review before installing shows every permission and the signing key; a key this installation has not seen asks to be trusted — compare its fingerprint. The message after installing says whether the plugin runs at once or after the next restart; see Installing and trust.

Create a target of the kind Plugin destination under Notifications, with your destination and its token, then send a test.

7. Publish it

Publish the .rdplug together with your key's fingerprint, or — so that people get your new versions in the Catalogue — in a plugin repository of your own: a signed index built with rdownloader plugin index build and a repository-role key. A repository only delivers: every package in it still needs your plugin key, approved by the person installing it. See A repository index and Running a plugin repository.

The SDK's reusable CI workflow, sdk/ci/plugin.yml, builds, checks the imports, packages and runs conformance on every push and pull request; it signs only on a push to the default branch or a tag, so a pull request never sees the key.

Read next

⚠️ **GitHub.com Fallback** ⚠️