“The owls are not what they seem… but at least these ones validate DNS, preserve UTF-8, and do not flood your IRC channel.” 🪄
A new owl has left the Mediabot tower.
MB692 introduces the foundations of native per-channel RSS/Atom support in Mediabot v3, with a strong focus on security, durability, clean IRC output, and real-world runtime validation.
This was not a “write the code, run the tests, ship it” change.
The feature was exercised on the live DEV instance, against a real public RSS feed, and that live gate caught several issues that static validation alone did not expose. Every one of them was fixed before the commit was allowed to leave the castle.
Mediabot now has a dedicated RSS command family:
m rss list [#channel]
m rss info [#channel] <feed>
m rss add [#channel] <feed name> <url> [interval=30] [max=5]
m rss del [#channel] <feed name>
m rss set [#channel] <feed name> <interval|max|enabled> <value>
m rss probe <url>
m rss show [#channel] <feed name>
Feed names may contain spaces.
For example:
m rss add Journal du Geek https://example.org/feed interval=30 max=5
The command parser uses the first http:// or https:// token as the boundary between the human-readable feed name and the URL.
Mutating commands require either:
global Administrator
or:
channel level >= 400
The existing news command remains completely separate and unchanged.
MB692 adds two new database tables:
RSS_FEED
RSS_ITEM
RSS_FEED stores per-channel subscriptions and their runtime metadata:
RSS_ITEM provides durable item identity and future deduplication state:
The migration is:
install/migrations/20260822_rss_feeds.sql
It was applied to the DEV database, then applied a second time to verify idempotence.
Result:
RSS_FEED : OK
RSS_ITEM : OK
No automatic polling is enabled yet. That remains deliberately reserved for MB693.
While integrating the RSS migration, an older test still relied on a hard-coded migration count:
18 SQL files
That spell was too fragile.
The migration contract now verifies exact parity between:
install/migrations/*.sql
and the authoritative:
Current migration order
documented in:
install/migrations/README.md
This removes the magic number while making the check stronger.
The public migration documentation was also updated in:
docs/DB_MIGRATIONS.md
The fetch path is intentionally strict.
Only:
http://
https://
are accepted.
The implementation rejects:
DNS is resolved before the HTTP request.
Every resolved address must pass the public-address policy.
Redirects are not delegated blindly to the HTTP library. They are followed manually, with the destination revalidated on every hop.
The redirect limit is:
3
and the response body limit is:
2 MiB
TLS verification remains enabled.
Environment HTTP proxies are disabled for this fetch path.
The first implementation validated DNS correctly, but the real live review exposed a subtle remaining issue.
After validating the DNS answer, this:
HTTP::Tiny->get($url)
could still resolve the hostname again during the actual connection.
That opened a small validate-then-resolve-again window.
MB692 now pins the connection to an already validated peer:
peer => $validated_ip
The original hostname is still retained for:
No unvalidated peer may be used.
Multiple validated peers are supported, with transport fallback only after an HTTP::Tiny 599 transport failure.
IPv4 peers are preferred when available, which also avoids a dead IPv6 route masking a valid IPv4 path.
The live test used:
https://korben.info/feed
The hostname correctly resolved to Cloudflare IPv4 and IPv6 addresses.
The IPv4 addresses were classified as public.
The IPv6 addresses were incorrectly rejected.
The culprit was not Cloudflare.
It was a Perl precedence trap around grep.
The original logic effectively relied on expressions shaped like:
!grep { ... } @bytes[...] && ...
The result was not grouped the way the code intended.
The fix now explicitly evaluates the scalar result of grep:
(grep { ... } @bytes[...]) == 0
After the correction:
2606:4700:20::ac43:4873 => PUBLIC
2606:4700:20::681a:35e => PUBLIC
2606:4700:20::681a:25e => PUBLIC
::1 => BLOCKED
::ffff:127.0.0.1 => BLOCKED
::ffff:8.8.8.8 => PUBLIC
Exactly what we wanted.
m rss probe and m rss show perform network work and therefore run through Mediabot’s asynchronous command infrastructure.
The first real DEV test produced this clue:
CommandAsync: 'rss probe' completed ... (0 line(s))
The worker had executed.
The response had simply disappeared.
The problem was that CommandAsync captured output through:
Mediabot::Helpers::bot*
Mediabot::UserCommands::bot*
but not the path used by:
Context->reply()
which ultimately calls:
Mediabot::botPrivmsg
Mediabot::botNotice
Mediabot::botAction
MB692 extends the async collector to capture that path as well.
After the fix, the same real command returned correctly on IRC.
The next live test worked functionally, but revealed this:
vérolé
Â
â...
That was not acceptable.
HTTP::Tiny returns response content as bytes, and the RSS parser was consuming those bytes without first decoding the feed charset.
The fix now decodes feed content before parsing using, in order:
Content-Type charsetWindows-1252 is also handled.
Invalid or unknown encodings fail closed instead of silently replacing characters.
The final real feed test produced:
Geekom retire enfin un pilote que Windows Defender signalait comme vérolé depuis 2024
oMLX – Faites tourner vos agents IA en local sur votre Mac
Surfshark lance une protection anti-scam SMS et enterre son moteur de recherche - La vie est faite de choix
No mojibake.
No cursed runes.
Hermione approves. 📚
The display style is inspired by the long-running Eggdrop RSS setup that motivated the feature:
ACTION - news : [Source] Title - URL
For example:
* mediabotv3 - news : [Les news de Korben] Geekom retire enfin un pilote que Windows Defender signalait comme vérolé depuis 2024 - https://...
Feed lists also retain the compact IRC-oriented style rather than turning the bot into a wall of text.
The final DEV runtime gate exercised the real feature, not a mocked substitute.
The live sequence included:
m rss probe https://korben.info/feed
m rss add MB692 Test https://korben.info/feed interval=30 max=3
m rss show MB692 Test
m rss del MB692 Test
This validated:
The temporary feed was removed afterward.
The DEV process remained stable.
Undernet was not restarted or modified.
MB692 adds:
893_mb692_rss_foundation.t
894_mb692_rss_commands_repository.t
895_mb692_rss_live_async_peer_pin.t
896_mb692_rss_public_ipv6.t
897_mb692_rss_feed_encoding.t
These contracts cover, among other things:
news commandBefore the commit was allowed to fly:
Encoding sentinels : 97/97 PASS
Targeted RSS/news : 389/389 PASS
Fast lane : 6049/6049 PASS
Full suite : 15818/15818 PASS
Full suite:
780/780 files
15818/15818 tests
214 seconds
Runtime state at commit:
DEV PID : 3714279
DEV NRestarts : 0
Undernet PID : 3699450
The historical DEV schema drift was also preserved exactly:
31 issue(s)
SHA256:
9d1672072bbc8bf7132c97c8b5510184165a2d9e0f37e4239d5bf113aaddb77c
MB692 did not silently “repair” unrelated historical schema differences.
8a94ea4e6b444b9cbe7fd9500cb194454ab2034b
Commit message:
🦉 Give Channels a Safe Native RSS Owl Post
Version:
3.4dev-20260822_180846
The commit was pushed successfully to master.
MB692 deliberately stops before automatic polling.
The next spell will focus on automation:
MB693
Planned work:
RSS_ITEMThe most important rule is already decided:
The first poll must establish a baseline silently.
Adding an existing feed must never dump its entire history into an IRC channel.
Only genuinely new items discovered afterward should be announced.
MB692 ended up being larger than the first sketch suggested, but for a good reason.
A purely test-driven pass would have shipped something that looked green while still containing:
The real DEV gate caught all four.
That is exactly the kind of trouble worth discovering before the owl reaches production.
Now the owl can fly. 🦉✨
You must be logged in to reply.