| D-M8-04-1 |
2026-10-23 |
The command lives in cmd/bothy/importtool (package importtool, entry Main(ctx, args, Env)); bothy import discord dispatches to it and passes the process environment. Besides the files of the brief there are server.go (the read calls of Bothy and the error type without body), walk.go (the reading and writing of channels and threads) and redact.go (D-M8-04-10). Env.AfterBatch is exported, not an unexported field, because the crash test runs in test/importtool, another package; it is nil in production. Env.CDN carries the network dependencies of the CDN fetcher apart from those of the Discord client. |
Design note 1; D-W-100 (names may differ). |
| D-M8-04-2 |
2026-10-23 |
The tool schema has exactly the keys of 22 19 over platform/config. Only variables whose name starts with BOTHY_IMPORT__ reach config.Load, so a server variable, BOTHY_CONFIG_FILE and the token variable BOTHY_IMPORT_DISCORD_TOKEN never act as keys. import.attachment_max_bytes is a string key with an empty default meaning file.max_upload_bytes of the server (getInstance); a non-empty value must be a positive integer, and a value above the server limit is lowered with a note in the report. import.lease_stale_after must be larger than import.follow.poll_interval; import.batch_max_items above the maxItems of ImportBatch is lowered with a note. |
Design note 2, D-W-51. |
| D-M8-04-3 |
2026-10-23 |
The mapping file is decoded strictly (yaml.v3 nodes, unknown fields, wrong types, duplicates, non-digit IDs, target forms, guild_id equal to --guild, one document) and mirrors the rules of the server's parser; every error names its YAML line and no value. Name lengths are left to the server, which checks them when the owner creates the source. With --apply exactly one source of the guild in draft, running or paused must exist and be running; its stored mapping must equal the file after the normalization of the server, every lead ignored, else exit 2 with the first differing JSON pointer; the leads always come from the stored mapping. The component cmd of the module register may use the vendor group yaml (the one of platform/config) for this parser; the tool uses the UUID type of api/restclient instead of its own UUID library. |
Design note 3, 22 12.1. |
| D-M8-04-4 |
2026-10-23 |
Preflight: the intent probe reads the first page of every mapped text channel in Discord order (a 403 there is exit 2) and decides on the first one with messages by other authors; a forum whose thread list answers 403 is exit 2 too. Name collisions compare without regard to case, like the unique indexes of spaces and channels, against live spaces and the live channels of a known target space, minus the targets of posted items of the source; a name the mapping creates twice in one place is a collision as well. A mapped channel whose category the mapping does not name or skips is skipped and counted, because the server would refuse or skip it. |
Design note 4, 22 4.4, 5.1 rule 1. |
| D-M8-04-5 |
2026-10-23 |
Plan: structure items only for the channels in scope (and the spaces they need); --only-thread leaves the main thread of its channel out; the sort keys are integer keys of the base-62 fractional index scheme of chat in Discord order; a thread's last_activity_at is the time of its last_message_id (else of its ID). A dry run reads every message from the start and ignores a stored cursor, so its report covers the whole server. A 404 on a channel or thread read during the run is a note, not an abort (it was deleted meanwhile). |
Design note 5. |
| D-M8-04-6 |
2026-10-23 |
Conversion: a poll is the line Poll: <question> followed by - <answer> lines; an embed is a quote block with [title](url) and the converted description; a forward is a quote block with the snapshot's content, embeds and stickers; the attachment line is Attachment not taken over: <name> (<size>, <reason>) <jump link>. content_sha256 is sha256(md + "\n" + sorted attachment IDs joined by ",") over the converted content with the reply quote and without the attachment lines, so a later successful upload is no change at the source. A reply gets reply_to_origin_key only when its target is taken over in the same thread (a target before the cursor is read once again around its ID to decide); otherwise the quote line. A type 21 message is dropped when its starter message is taken over in the main thread of the channel. |
Design note 6, 22 6.1 to 6.3. |
| D-M8-04-7 |
2026-10-23 |
Attachments: a file item posted by an earlier run (read once per run with listImportItems) is reused without a fetch. A fetch error gives the message its line, except a rate limit above the limits or a cancelled run, which end the run with exit 1. A 4xx of the server for one file (its type, its size) gives the line too; 409 conflict with reason import_not_running, 429 and 5xx end the run. |
Design note 7, 22 6.5. |
| D-M8-04-8 |
2026-10-23 |
The cursor object is {version: 1, run: {id, renewed_at}, channels: {<id>: <message id>}, threads: {<id>: <message id>}}. Before every batch and every renewal the run reads the source again: another run ID, no lease or a source that is not running stop it (exit 3, exit 1). Renewal and cursor writes share one mutex; the renewal ticker sleeps import.follow.poll_interval through its own sleeper. After a batch the cursor of its thread moves to the last Discord message consumed so far (skipped messages included), with the lease and the counters in one setImportCursor; the end of a run clears run. "Already present" counts the message items created before the server time of the lease write (the Date header). |
Design note 8, 22 7.3, 7.4. |
| D-M8-04-9 |
2026-10-23 |
The report has one row per channel in scope (Discord name and target name) and a total; --report json prints one object with version 1 (22 11 lists the members). stats.tool receives the totals as flat counters (messages_taken, attachments_failed, ...); no content, no names, no Discord IDs. Sessions stay 0 until M8-06. |
Design note 9, 22 11, 14. |
| D-M8-04-10 |
2026-10-23 |
The CDN fetcher of M8-02 names scheme, host and path of an attachment link in errors and logs, and Discord puts the file name into that path. The tool therefore reduces a fetch error that ends the run to its kind and gives the fetcher a logger that keeps only method, scheme and host of the link, so 22 10.2 rule 4 (no file names in output) holds. |
Found by TestNoSecretInOutput; a gap between 22 6.5 item 4 and 10.2 rule 4. |