Forum teuk.org

๐Ÿช„๐Ÿ›ก๏ธ๐Ÿ“š Mediabot v3: Accents Survive the Owl Post, the Castle Checks Its Own Walls, and the Hundredth Gift Counts โ€” MB436 to MB456

in Mediabot ยท started by TeuK ยท 1mo ago

TeuK ยท 1mo ago

The previous chapter ended with every channel using one canonical name, every specialised URL handler checking the real host, and Claude summaries finally able to speak aloud in the correct IRC hall.

The next chapter began with what looked like small language and display issues. It ended much closer to the foundations of the castle.

Accented words were being rejected or double-encoded. Persisted notes could vanish from export and search after a restart. A stored ban could fail to catch the nick it was written for. One horoscope command quietly reseeded the random number generator used by the entire process. A partially deployed installation could survive startup and then collapse hours later when traffic reached a method missing from an older module.

The release work also needed more than source tests. It needed disposable lifecycle checks, a live games smoke test, and a permanent lint preventing one known class of broken UTF-8 truncation from returning.

MB436 through MB456 handle that work.

Mediabot remains on the 3.2dev line. This is still not the 3.3 release.

As always for this code delta:

No database table, column or migration was added or altered.


๐Ÿ—ฃ๏ธ MB436โ€“MB439 โ€” Accents stop getting lost between IRC and the Web

IRC input and several database paths in Mediabot deliberately carry UTF-8 as raw bytes. That is safe, but only when every boundary knows whether it is handling bytes or decoded Perl characters.

Several commands used uri_escape_utf8() on text that was already encoded as UTF-8 bytes. The result was a second encoding pass:

cafรฉ
โ†’ correct: caf%C3%A9
โ†’ broken : caf%C3%83%C2%A9

๐Ÿ“– MB436 โ€” define accepts real French words

m define cafรฉ had two independent obstacles:

  • the validation regex rejected the bytes forming รฉ;
  • the Wiktionary URL then double-encoded them.

The command now accepts UTF-8 byte sequences in a word and percent-escapes the already encoded bytes exactly once.

๐ŸŒฆ๏ธ MB438 โ€” Weather locations and emoji formats use the same byte-safe gate

A location such as Genรจve and the weather format containing symbols like ๐Ÿ’ง, ๐ŸŒฌ and โ˜” now pass through one byte-aware URL helper.

ASCII remains unchanged. Accents and emoji reach wttr.in without being turned into a second layer of UTF-8.

๐ŸŽฌ MB439 โ€” YouTube search follows the same rule

Both YouTube search paths now use that helper as well. A query such as:

cafรฉ musique

is sent to the YouTube API as the query the user actually typed, not as the mojibake version of it.


๐Ÿ“ MB437 and MB456 โ€” Notes remain present after a restart

Personal notes live in the database, with an in-memory cache for the active process.

Before MB437, the add path checked the ten-note cap against memory without first loading the persisted notes. Immediately after a restart, a user with ten notes could appear to have none and add an eleventh.

MB437 introduced a shared database loader and used it for adding and listing.

The independent MB456 audit found the remaining two doors: note export and note search still ran before the loader. After a restart they could say:

No notes to export.
No notes matching 'word'.

while the notes were safely present in SQL.

All note operations now see the same persisted state before they decide what exists, what matches and whether the cap has been reached.


๐Ÿšช MB440 โ€” A ban is matched against the person entering, not a wildcard copy

The JOIN enforcement path built a full hostmask for the user, then immediately normalised it into a broad mask before looking for stored bans.

That made nick-specific bans ineffective. A stored pattern such as:

spammer!*@host.example

cannot match a subject whose nick has already been replaced by *.

MB440 now performs the wildcard comparison against the real joining identity:

nick!ident@host

The normalised mask remains available for the actual IRC +b operation. The comparison target and the ban placed on the server finally have distinct, correct roles.


โœ‚๏ธ MB441, MB454 and MB455 โ€” No rune is cut through the middle

๐ŸŒŒ MB441 โ€” Chronos excerpts

The first-message excerpt in chronos used a raw byte substr. An accented character crossing the 60-byte boundary could be split in half.

It now uses the shared UTF-8-safe truncation helper.

๐Ÿ“œ MB454 โ€” Database command pagination

Eight paginated administrative views used the same raw pattern at 357 bytes. They now preserve complete UTF-8 characters before adding the three-dot suffix.

๐Ÿงน MB455 โ€” Close the whole class and lock the door

Eleven remaining occurrences were converted across channel, login, helper, Partyline and user-command modules.

More importantly, test 668 scans the source tree and requires:

raw substr(..., 0, 357) + "..." occurrences = 0

This is a deliberately narrow lint. It does not pretend that every substr is wrong; parsing offsets are legitimate. It permanently bans one display idiom that is never safe for Mediabotโ€™s UTF-8 byte pipeline.


๐Ÿงฎ MB442 โ€” % no longer silently converts floats into integers

Perlโ€™s % operator is integer arithmetic. Accepting:

10.5 % 3

silently discarded the fractional part before calculating the remainder.

SafeCalc now rejects non-integer operands for % and tells the user to use the floating-point remainder function instead. Integer modulo remains unchanged.

The calculator now refuses to invent precision it does not have.


๐Ÿง  MB443 and MB445 โ€” Accented text is not a word boundary

Byte-oriented matching created another subtle error. Bytes above ASCII were mistaken for separators.

โ“ MB443 โ€” Trivia answers

A short answer such as on could be accepted inside the UTF-8 bytes of a longer word such as garรงon, because the bytes forming รง looked like boundaries to an ASCII-only rule.

Trivia now treats all UTF-8 bytes as part of the neighbouring word.

๐ŸŽญ MB445 โ€” Quote-game authors

The same principle now protects quote-game masking and answer validation. Accented nick text cannot create an accidental boundary around a partial answer.


๐Ÿ”ฎ MB444 โ€” The horoscope keeps its hands off everyone elseโ€™s dice

The horoscope is deterministic for a nick and a day. The old implementation achieved that by calling:

srand($seed)

on the process-wide random generator, drawing its values, then calling srand() again in an attempt to restore normal randomness.

That does not restore the previous sequence. It replaces it.

One horoscope invocation could therefore perturb every other feature sharing the global generator:

rolls
duels
8ball
random quotes
Hailo probability
Trivia selection

MB444 uses a small local deterministic generator instead. The horoscope remains stable for the day and the rest of the bot never notices that it ran.


๐Ÿ˜€ MB446 โ€” Mood can finally see emoji stored in SQL

The mood analyser searched raw UTF-8 bytes with a regex made of Unicode codepoints. Those two representations cannot match, so the promised โ€œtop emojiโ€ line remained empty.

MB446 decodes a tolerant copy solely for the emoji scan. The existing byte-aware French word tokenisation remains unchanged.

Emoji statistics now operate on characters; the rest of the log pipeline stays in its established byte representation.


๐Ÿ“š MB447 โ€” The release handbook stops recommending doors that do not exist

The README and deployment helper referred to historical ./start and ./daemon wrappers that are not present in the repository.

The documentation now describes the supported paths:

foreground : perl mediabot.pl --conf=mediabot.conf
production : systemd template mediabot@<instance>

The migration guide was also aligned with the complete ordered migration list. Fresh installs use the current schema directly; existing databases apply only missing historical migrations and then run strict drift validation.

Documentation is now tested as part of the release contract instead of being a collection of hopeful commands.


๐ŸŒ MB448 โ€” Chromium validates the final argument at the final boundary

The Chromium fallback is launched with an argv list rather than through a shell, but an untrusted value beginning with - can still be interpreted as a browser option.

All current callers already provide HTTP URLs. MB448 enforces that requirement inside the function immediately before open3().

Only absolute http:// or https:// values cross that execution boundary.

The protection no longer depends on every future caller remembering an unstated rule.


๐Ÿฐ MB449 and MB456 โ€” The castle checks whether all of its walls come from the same build

A live instance exposed the danger of partial deployment.

Its main program was new enough to call hailo_record_activity, while its local Hailo module came from an older tree and did not provide the method. Perl syntax checks remained green because method resolution happens at runtime.

The bot started, received traffic and then collapsed when execution finally reached the missing method.

MB449 adds a startup integrity inventory. It derives the methods invoked on the main Mediabot object from the running source and verifies that the loaded package provides every one of them.

A mismatched installation now fails immediately with an actionable instruction to deploy the full tree.

The MB456 audit found one last weakness: if the main source could not be opened, the inventory stayed empty and the guard could report a misleading success for zero methods.

The scan is now anchored to the compiled source file and fails closed when the file is unreadable or the inventory is empty.

A broken wall can no longer hide behind a successful inspection containing no inspection at all.


โšก MB450 โ€” The achievement oracle stops rereading a million-line ledger

The message-achievement hook already runs on a five-minute cache, but eligible users could still trigger an expensive hourly-distribution query over a large channel log.

MB450 adds two mathematically safe guards:

fewer than 50 total messages
โ†’ impossible to own 50 night or morning messages
โ†’ skip the hourly GROUP BY entirely

and:

eligible high-volume nick
โ†’ at most one hourly-distribution scan per hour and channel

The achievement thresholds are unchanged. Only the frequency of an expensive question changes.

A possible database index remains an explicit operational decision outside this code commit. No schema change is applied automatically.


๐Ÿงช MB451, MB452 and MB456 โ€” Release tests become real programs

๐Ÿ” MB451 โ€” Disposable lifecycle test

A standalone live script creates a throwaway configuration and checks two full cycles of:

PID created
second instance refused
first instance remains owner
SIGTERM exits cleanly
PID removed
restart succeeds

It contains guards against production database and PID paths.

The MB456 audit caught a bug in the test itself. Because the script enables set -e, waiting for the deliberately refused second process returned the expected non-zero code and aborted the script before it could count the result as a PASS.

Both child exit codes are now captured through shell conditions. Expected refusal is a PASS; an abnormal shutdown is recorded as a FAIL; neither bypasses the report.

๐ŸŽฒ MB452 โ€” Games smoke test

A new live closure exercises the stateless public commands:

morse
calc
8ball
roll
flip
choose
slap

It ends by checking that the bot still answers version after the series.

The deterministic Morse and calculator responses provide strong anchors while the random commands are checked against bounded output contracts.


๐ŸŽ MB453 โ€” The hundredth gift counts as the hundredth

The gift_giver achievement unlocks after one hundred positive karma gifts.

The current vote was added to the in-memory history only after the achievement check. At the hundredth gift, the counter therefore still saw ninety-nine and unlocked on the hundred-and-first.

MB453 includes the current positive vote before counting the historical ring buffer. Negative votes do not increment it.

The milestone now arrives when the user actually reaches it.


๐Ÿ” MB456 โ€” The snapshot was audited, not merely admired

The final review found three gaps before commit:

note export/search skipped persisted state after restart
startup integrity could succeed after an empty scan
lifecycle test aborted on the expected lock-refusal exit code

Test 669 protects all three contracts with eighteen assertions.

The focused selection in the independent audit container reached:

212 passing checks out of 214

The two remaining execution failures are the runtime-dependent groups that need DBI/JSON::MaybeXS and the complete Perl environment available on the real server. The final installer also extracts the exact startup-integrity block from mediabot.pl and compiles it separately under use strict, so its lexical scope is checked even in the reduced audit container.

The full post-MB456 suite and both live tests remain explicit pre-commit gates; this article does not invent a green result before those commands are run.

The last fully recorded server suite at MB449 was:

9039/9039

The known static additions make the expected post-MB456 total approximately:

9099/9099

The actual harness output remains the source of truth.


๐Ÿงฑ Database and release impact

For the tracked code in this chapter:

0 new tables
0 altered columns
0 committed migrations
0 automatic schema operations
0 release to 3.3

Mediabot remains:

3.2dev-*

โœจ What this chapter leaves behind

Mediabot now:

  • accepts and correctly sends accented definition, weather and YouTube queries;
  • loads persisted notes before export, search, listing and cap enforcement;
  • enforces stored bans against the real joining hostmask;
  • truncates Chronos and administrative pagination without breaking UTF-8;
  • rejects silent float truncation in integer modulo;
  • treats accented bytes as part of Trivia and quote-game words;
  • generates deterministic horoscopes without reseeding the global RNG;
  • counts emoji as Unicode characters;
  • documents real startup commands and the complete migration order;
  • validates Chromium arguments at the execution boundary;
  • detects mixed-version installations at startup and fails closed;
  • avoids repeated expensive achievement hour-band scans;
  • owns disposable lifecycle and games live tests;
  • unlocks gift_giver on the real hundredth positive vote;
  • permanently lints one known unsafe pagination-truncation pattern out of the source tree.

The castle did not gain a flashy new tower.

Instead, its owls learned to carry accents without mangling them, its ledgers stopped forgetting pages after a restart, its dice were freed from the horoscope, and the front gate began checking that every stone belonged to the same construction.

That is the work that makes a stable release boring in exactly the right way.

You must be logged in to reply.