sayamaapps / kirby-fude
Change declared Kirby site content by chat message: drafts, preview links, explicit publish.
Requires
- php: ^8.2
- ext-ctype: *
- ext-filter: *
- ext-mbstring: *
- getkirby/composer-installer: ^1.2
Requires (Dev)
- ergebnis/composer-normalize: ^2.54
- getkirby/cms: ^5.0
- icanhazstring/composer-unused: ^0.9.7
- maglnet/composer-require-checker: ^4.0
- phpmd/phpmd: ^2.15
- phpunit/phpunit: 11.5.55
- vimeo/psalm: ^6.20
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-31 18:15:15 UTC
README
Change declared website content by chat message, for Kirby CMS 5.
A chat bot proposes a change, for example a new team member with a photo. Fude stores it as an unpublished Kirby content version with a preview link, and publishes only after an explicit confirmation, with the draft's token. Anything the site does not declare stays unreachable.
Features
- Declarative and validated: only declared collections and fields are reachable, checked against the blueprint too.
- Drafts are Kirby
changesversions; publishing needs a token and an unchanged page. - Photos are resized to the declared width and get alt text.
- One log line per call: account, channel tag, route, outcome.
- Own bot role without Panel access; HTTPS only; English and German replies.
Kirby 5 · PHP 8.2 to 8.5 · gd (or ImageMagick) and mbstring · a writable storage root (drafts and the log live in <storage>/fude/)
Installation
With Composer:
composer require sayamaapps/kirby-fude
Composer installs to site/plugins/fude/. Add that path to .gitignore and deploy it like vendor/.
From a ZIP archive: unpack it to site/plugins/fude/.
As a Git submodule: git submodule add https://github.com/sayamaapps/kirby-fude.git site/plugins/fude.
Requirements
Kirby 5 is supported; PHP 8.2 to 8.5 is tested in CI.
Usage
Declare what the bot may change in site/config/fude.php:
<?php return [ 'collections' => [ 'team' => [ 'kind' => 'structure', // rows of a structure field on one page 'page' => 'standort', 'field' => 'team', 'fields' => [ 'name' => ['type' => 'text', 'required' => true], 'foto' => ['type' => 'image', 'maxWidth' => 1200, 'alt' => 'name'], ], ], ], ];
Enable Fude with 'sayamaapps.fude.active' => true in site/config/config.php, then create a Panel account with the role Fude (bot) and a long random password.
Collections
| Option | Meaning |
|---|---|
kind |
structure (default; needs page, field), pages (child pages; needs parent, template; status after publish: listed (default) or unlisted), or page (the page itself; update only) |
key |
Identifies an entry: a text, select or date field, always required. Default: the first text field. |
label, description, fields |
Shown to the bot; fields as below (a pages collection needs title) |
actions |
create, update (default), delete, replace (structure only) |
maxEntries |
structure: at most 1–100 rows, checked on create and replace |
insert |
structure: top or bottom (default) for a created row |
touch |
structure, page: ['field' => 'stand', 'value' => 'today'] sets that page date to today on every draft. With 'editable' => true, a draft may name another day; 'default' => 'saturday' (any weekday) gives a draft without a date the next such day instead of today. |
readImages |
true: the model may read attached photos. Fude itself does not use it. |
publishRoles |
Roles that may publish (default ['fude']) |
Field types
| Type | Notes |
|---|---|
text |
One line; url, email or tel follow the blueprint |
textarea |
Plain text or Markdown; HTML tags, javascript:-style URLs, the gist KirbyTag and style: attributes are refused |
select |
Options from the declaration, the blueprint or its query |
date, time |
YYYY-MM-DD (min: 'today' refuses earlier days); H:MM or HH:MM, 24 h |
number |
Decimal comma or point; optional min and max |
toggle |
true or false |
image |
Upload. maxWidth (default 1600), formats (default jpg, png, webp; also gif), template, alt (text field, default the key) |
page |
One page of a blueprint pages field's query, chosen by title |
Every field takes description (shown to the bot), required and maxlength; the last two merge with the blueprint, and the stricter one wins. ask: true (or fudeAsk: true in the blueprint) makes the bot ask for a field once before the first draft. Fields can also be marked in page blueprints (fude: keys); Fude then derives the collections, and fude.php overrides them by name. A marked pages section makes the children with its template one pages collection, with the fields marked in their blueprint. Marker and rule reference: docs/reference.md.
Chat flow
curl -u fude@example.com:… -F collection=team -F action=create \
-F 'fields={"name":"Max Muster"}' -F image=@photo.jpg https://example.com/fude/draft
curl -u fude@example.com:… -H 'Content-Type: application/json' \
-d '{"id":"…","token":"…"}' https://example.com/fude/publish
Routes
All routes need Basic Auth. Replies are JSON with ok, format (currently 1) and the data, or error.
| Route | Request | Reply |
|---|---|---|
GET /fude/schema |
— | site, language, collections (declarations and published entries) |
POST /fude/draft |
collection, action, fields, key (update, delete), entries (replace), touch, actor (log tag), upload image |
draft, token, preview |
POST /fude/publish |
id, token, actor |
id, url, hash |
POST /fude/discard |
id, token, actor |
id, kept (true if a Panel editor's work stays) |
Errors: 400 malformed request, 401 login, 403 HTTPS, role, token or publish permission, 404 unknown collection, entry or draft, 409 conflict (entry exists, page or its files changed, draft edited in the Panel, file used on another page), 410 draft expired or gone, 422 validation, 423 page locked or with unsaved Panel changes, 500 configuration error.
Options
Every option is sayamaapps.fude.<name>, written flat: 'sayamaapps.fude.active' => true. The nested form 'fude' => ['active' => true] works for the 1.x name only (see Deprecations). null means "not set". The 1.x name fude.<name> still works as a fallback until 2.0, flat or nested, and is read without a warning. If both names are set, the new one wins.
Example, site/config/config.php:
return [ 'sayamaapps.fude.active' => true, 'sayamaapps.fude.language' => 'de', ];
Option (sayamaapps.fude.<name>) |
Default | Meaning |
|---|---|---|
active |
false |
Master switch. Off: routes answer 404 and the role does not exist. |
config |
site/config/fude.php |
Declaration file, relative to the project root or absolute |
roles |
['fude'] |
Roles that may call the routes; admins are not included. |
withoutHttps |
false |
Accept plain HTTP. Local tests only: Basic Auth sends the password with every request. |
trustedProxies |
[] |
IP addresses and CIDR ranges of TLS-terminating proxies (see HTTPS) |
draftLifetime |
172800 |
Seconds a draft can still be published (48 h). Older drafts answer 410. |
maxUploadBytes |
15728640 |
Largest uploaded image (15 MB) |
maxPixels |
40000000 |
Largest uploaded image, width × height, checked before decoding |
language |
site language | en or de for replies |
blueprints |
true |
Derive collections from fude: markers. false: fude.php alone decides. |
maxCollections |
30 |
Most collections, derived and declared together; more is a 500. |
siteId |
hostname | Identifier in the schema, for readers that tell sites apart |
timezone |
Europe/Berlin |
Time zone for "today" (touch, min: 'today'); an unknown zone is a 500. |
HTTPS
Routes answer 403 unless the connection itself is TLS. Behind a TLS-terminating proxy, list it in sayamaapps.fude.trustedProxies and have it send X-Forwarded-Proto: https; the header counts only from a listed address. Check a site with curl -s -o /dev/null -w '%{http_code}\n' https://example.com/fude/schema: 401 means the check passed; a 403 names the cause in error.
What happens on the page
- Structure and page drafts go into the page's
changesversion; a new child page is a draft page until published. Publish validates against the blueprint again; a Panel editor can publish or discard the draft there. - Images become Kirby files on the page. Discard removes a file only if the draft owned it alone: unchanged since the draft and used nowhere else on the site.
- Deleting a page takes its files only as the draft saw them: a file added or edited since, or one another page uses, answers 409.
Stored data
Fude processes no visitor data and stores no IP addresses, user agents or full-size photo originals. The chat side logs in with Kirby's own password check and its lockout, using an account without Panel access. On the server, Fude keeps:
- Proposed content as a
changesversion or draft page, until published or discarded. - Draft records in
storage/fude/drafts/(field values, account, channel tag), deleted on publish or discard. - A log in
storage/fude/log.jsonl: who, when, route, collection, action, key, outcome. No field values, no images; it restarts at 4 MB.
Deviations from Kirby's plugin conventions
These stay as in 1.x. Each one is a choice, not an oversight.
- Role and blueprint names carry no plugin prefix. The role
fudeand the blueprintusers/fudeare public names. Renaming them would detach existing bot accounts, so the change waits for 2.0. - Drafts, the log and resized images use PHP's file functions. Kirby's
F::write()andDir::make()throw Kirby's exception class on a path they cannot write, where 1.x throwsRuntimeException. The plugin keeps its own calls so that the exception class and the messages stay the same. - Reply texts come from
I18n::t(), not from Kirby's translation lookup. The replies are written in the language of the request (sayamaapps.fude.language) and must also work without a Kirby app, as in 1.x. The same tables are registered as Kirby plugin translations, so the Panel reads the same text.
Deprecations
| 1.x name | Since 1.10 | Note |
|---|---|---|
fude.<option> in config.php |
sayamaapps.fude.<option> |
Still read as fallback. Rename the keys in your site config. |
Planned for 2.0
1.x keeps every public name, option key, translation key, exception class and route. The names below are marked @deprecated in the code, and 1.x prints no runtime notice for them. 2.0 is planned to:
- register plugin translations as
sayamaapps.fude.<key>only, - remove
fude_option()andfude_not_found(): useSayamaapps\Fude\Options::get()andSayamaapps\Fude\Service::notFound(), - remove the
Fude\class names: the classes stay inSayamaapps\Fude\, - throw Kirby's exception class for internal failures (drafts, log, image files) instead of
RuntimeException, - give the role and the blueprint a plugin prefix. Bot accounts with the role
fudemust then be switched to the new role by hand.
Routes, the JSON format and the storage paths are unchanged in 1.x and planned unchanged for 2.0.
Tests: composer test. Issues and pull requests are welcome, without a support promise. Report security problems privately through GitHub's vulnerability reporting. Changes: CHANGELOG.md.
License
MIT, see LICENSE.