Issues (bug reports by email)
Bug reports arrive by email at bugs-k7q4+<repo>@fuzzyworld.net. The address is on every repo's summary page. The
repo name is part of it, and a repo that does not exist under ~/git sends the mail to the bad folder.
server/issues.py (fw issues ..., Python standard library only) turns each report into an issue and, when
allowed, a static page. It reuses the comments plumbing in comments.py (IMAP or Maildir source,
Authentication-Results check, locks, atomic JSON files).
Moderate from the terminal with fw issues, or from the local web page (admin.md).
Modes
fw issues <repo> public|reviewed|private sets git config fuzzyworld.issues in the bare repo and republishes.
Anything other than exactly public or reviewed is private, and a repo that is itself private
(fuzzyworld.visibility) never shows issues, whatever its mode.
| Mode | An issue's page appears |
|---|---|
private |
never; reports are stored and listed by fw issues list only |
reviewed |
after fw issues approve <repo> <n> |
public |
as soon as its first message passes email authentication (unauthenticated mail is stored with authenticated: false and waits for approve) |
In public and reviewed mode each sender may send 5 messages per 24 hours (the rest goes to the bad folder).
Moderating
| Command | Effect |
|---|---|
approve <repo> <n> |
marks the messages the issue has at that moment as shown |
hide <repo> <n> |
takes the issue off the site in any mode and keeps it off until approve |
close / reopen |
change the state shown on the page |
list, show |
read stored issues (plain text) |
publish, publish-all |
rebuild the pages |
ingest, imap-check |
read the mailbox / check the mailbox settings |
Rules for messages:
- A reply (its
In-Reply-ToorReferencesmatches an earlier message of an issue of the same repo) is added to that issue. - A message that arrives after
approveis shown only inpublicmode, and only if its sender passed authentication, or after anotherapprove. An unauthenticated reply is never shown automatically. - A mail that repeats a stored Message-ID or a stored byte-for-byte copy is ignored.
Pages
/<repo>/issues/ lists the visible issues; /<repo>/issues/<n>/ shows one. Every publish rebuilds the whole
issues/ folder, so hiding removes the page. When the folder appears or disappears, the summary page is rebuilt so
its "Issues" link follows.
An issue page has:
- an open/closed badge (also in the list),
- a small details table (version, rules, commit, seed, level, turns, lives, status, modes), filled from the game's own report lines when they match exactly,
- the messages in wrapping text boxes (a line of three or more dashes becomes a thin rule between boxes),
- links to the previous and next visible issue.
Senders are labelled "Reporter A", "Reporter B", ... by first appearance within that one issue. The stored author stays "Reporter"; hashes are never written.
Privacy rules for published text
Never written to a page: any address, name, Message-ID or sender hash.
Replaced or dropped:
- every address-shaped string in the title and text becomes
[address hidden]; - lines starting
Device:orRun record:are left out.
Device lines that wrap: following lines are dropped while they are indented or look like a user agent
(token/version, parentheses or semicolons). A blank line, a template label such as Seed ..., or plain prose ends
the block, so the reporter's own words are kept.
Normalisation before filtering: format=flowed text is re-joined first (an empty line always ends a paragraph; delsp=yes is honoured), and the text is NFKC-normalised, so fullwidth Device: or @ count.
Quoted replies are cut, so quoted names never publish. The text is cut at the first of:
- an attribution line in any language: a colon line with an address, or an
On/Am/Le/El/Op... line with a date that says wrote/schrieb/a écrit/schreef/escribió; any such line directly followed by a quoted>line; - a wrapped attribution: up to three lines ending in a colon, directly followed by a quoted line, or ending in a separate
wrote:line; - a
-----Original Message-----line; - a block of at least three
Label: valuelines that is a mail header (From:/Von:/De :..., with an address or three header labels; an Outlook____line before it is dropped); - a colon line followed only by quoted lines at the end of the text.
The last cut is never made when it would leave nothing, and prose such as I pressed <Enter> and wrote: stays.
Output: all text is HTML-escaped inside <pre class="issue-text">, and no page has a script.
Run records
If the text part holds a line Run record: {json} (the Explodorama bug-report mail does), it is read and checked.
| Rule | Detail |
|---|---|
| Source | the full decoded part (the text shown is cut to 4000 characters; the record is not). If the decoded text has none, the raw mail is decoded as quoted-printable and searched again |
| Validity | at most 64 KB, valid JSON, nesting at most 32 deep, an object with integer seed and simVersion and a list actions, finite numbers only |
| Kept keys | only simVersion, seed, startLevel, startCharge, options and actions; all others are dropped |
| Refused | a record with an address-like string anywhere in it. Every refusal is logged in rejected.log |
| Not recognised | a record split over several lines by a mail client |
| Which one wins | the first line that passes (so the mail's later "too long for an email link" footer is ignored). Only the first record of an issue is kept; a record in an unauthenticated reply is ignored |
| Storage | re-serialised, never as mail text |
Publishing. A record is as public as the message that carried it: one in a reply is published only once that
reply is shown (after approve in reviewed mode; at once in public mode for an authenticated sender). It is published as /<repo>/issues/<n>/record.json at the same moment as the issue page
(approve publishes both in reviewed mode; hide removes both). server/site/htaccess lets
https://apps.fuzzyworld.net read it (CORS) and sends no-cache.
Replay link
If a deployed app of the repo has a replay template, a visible issue with a record shows Watch what happened.
- "A deployed app of the repo" means
apps.fuzzyworld.net/<app>/.fuzzyworld.jsonhasrepoequal to the repo, and the app has anindex.html. - The template comes from the
"fuzzyworld"block of the app'spackage.json, for example"replay": "replay.html?report={record}"(apps.md). - The link goes to
https://apps.fuzzyworld.net/<app>/<replay>with{record}replaced by the URL-encoded address ofrecord.json. - The app named like the repo is preferred, so a
-nextcopy is not chosen. - It is a plain link, not an iframe (the site's CSP blocks framing). No
replaykey, no link.
Machine-readable commands
Used by fw admin. The text of unpublished reports is included (it is a private tool); sender
hashes, mkey and mid never are.
| Command | Prints |
|---|---|
fw issues list [repo] --json |
every stored issue (repo, n, title, created, updated, state, approved, hidden, public, record, messages, shown, waiting) and each repo's mode and public_repo. waiting means a public or reviewed repo's issue that is not hidden and has messages not yet shown |
fw issues show <repo> <n> --json |
one issue with the full text of every message (label, date, authenticated, approved, shown), the details-table rows, previous/next numbers, the public page URL (only when public) and the replay URL (only when the record is shown) |
fw issues record <repo> <n> |
record.json as stored, even for an unpublished issue (error no record for repo#n when there is none) |
Plain list and show are unchanged.
State and mail source
State is in ~/fw/issues/ (override with FW_ISSUES_DIR), outside every web root:
| File | Purpose |
|---|---|
repos/<repo>/<n>/issue.json, record.json |
the stored issue and its run record |
secret |
salt for sender hashes (mode 600) |
msgids.json |
threading |
keys.json |
a mail is never stored twice |
rejected.log |
every refusal |
| locks |
Mail source: ~/fw/issues/imap.json (mode 600, the same format as the comments one; finished mail goes to
issues-done / issues-bad), or, without it, the Maildir ~/Maildir/.bugs (.bugs.done, .bugs.bad).
Requirements:
- The bugs mailbox needs its own account or its own
folderinimap.json. Ingest andimap-checkrefuse to run when it has the same host, user and folder as the commentsimap.json(the two jobs would move each other's mail to their bad folders). - Ingest refuses to run until
authserv_idis set inserver/comments/sites.json. - The hourly job is
issues-ingest.sh(operations.md).