feat(cli): import a Discord server into Chatto #1

Open
f3l1x wants to merge 526 commits from feat/discord-import into main
Owner

chatto operator import discord now brings a whole Discord server into Chatto:
messages, threads, attachments, embeds, reactions, mentions, room groups, and
room placement. A companion export bot produces the bundle from a live server.

The branch merges main (v0.5.0-beta.19 and later). Its records use the current
numbering: ADR-117 and FDR-052, because main took FDR-051 for room
invitations. The merge keeps both extensions of the historical message import
path: this branch's import-source identity and mention facts, and main's
thread placement fields.

What's in the branch

Bundle and CLI

  • Parse and validate a DiscordChatExporter-shaped JSON bundle, with a
    mapping.json sidecar that maps Discord users to Chatto users and names the
    target.
  • --all-channels imports one export per channel; --stream-attachments
    downloads a file the folder lacks, on the operator's machine. The server never
    contacts Discord.
  • --apply-structure reads structure/chatto-plan.json and recreates the
    server layout: one room group per category, the plan's room name, a universal
    room where Discord let every member see the channel, and room members for a
    restricted channel.
  • An embed's words become message text and its images become attachments, so a
    bot post that carried only an embed still arrives. Both sides take a bundle
    path from the URL exactly as the URL spells it.
  • A dry run reports every skipped item with its reason, and every warning.
  • A long channel is sent in request-sized parts, so it no longer fails against
    the operator API request limit.
  • A message that carries neither a body nor a usable attachment is skipped and
    counted, instead of aborting the channel. A body longer than Chatto allows is
    cut and reported.

Server and operator API

  • ImportBundle in core writes messages with their original time through the
    historical-import path, marks the facts historical_import, re-hosts
    attachments, rewrites mentions, and adds one reaction per mapped reactor.
  • Additive operator API: ListRoomGroups, CreateRoomGroup,
    CreateRoomRequest.universal, ImportCreateChannel.{group_id,universal}, and
    ImportDiscordBundleResponse.messages_skipped_empty.
  • An imported reaction that names a Unicode emoji is stored under its primary
    gemoji name, so it matches what the picker writes.
  • A created non-universal room also grants membership to every mapped author of
    the channel, so an imported history never lands in a room nobody can open.

Export bot (tools/discord-export-bot/)

  • Stdlib-only Python. Writes the bundle plus the structure plan: categories,
    channels, overwrites, roles, and member-to-role assignments.
  • Writes each embed's words and images, and writes a thread's opening post into
    the thread export, so an imported thread keeps its first message.
  • Fetches an embed image through Discord's media proxy when Discord provides one,
    so the export does not contact the third-party site the embed pointed at.
  • Resumable and parallel: pooled CDN downloads, attachment reuse, proactive
    rate-limit pacing, and visible API and waiting totals.
  • Tolerates a channel type that rejects archived-thread listing.

End-to-end evidence

A live Discord server (13 visible channels, 32 mapped users) exported by the bot
and imported into a fresh Chatto instance:

Item Result
Channel exports 27, holding 9,063 messages
Messages imported 8,942, with original post times and authors
Messages whose only content was an embed 3,088, all of which now arrive
Reactions 585 added, 6 skipped (Discord custom emoji)
Attachments re-hosted 3,990 (706 poster files, 3,284 embed pictures); 7 skipped (6 HEIC, 1 oversized animated GIF)
Embed images the source has removed 7 reported, each message kept
Messages skipped 2 left empty by a rejected attachment, 3 carrying nothing at all
Room groups text-channels created; the plan's voice-channels group was not created, because no voice channel has messages
Rooms 26 new, plus the reused general
Room members granted 54 grants, in one pass

Read-back through the public ConnectRPC API, signed in as a mapped Discord
author:

  • A bot post that carried only an embed reads back as its title, description, and
    name: value fields, with the embed's poster re-hosted as an attachment
    (image/jpeg, 1920x1080, per-user access ticket).
  • A link-share embed reads back as its link, title, and description, plus its
    thumbnail as an attachment.
  • A thread's first message is its opening post, with the Discord mention token
    rewritten to @hermes_agent_ff.
  • A non-member of a non-universal room gets permission_denied, and the same
    user reads a universal room. The 7 members the import granted can read the
    restricted room.
  • A Discord 🛑 reaction reads back as the stop_sign emoji name.
  • A dry run of the same bundle reports Embeds rendered as text: 3907.

A second run of the same import is a no-op for messages and re-applies the
membership grants.

Verification

  • mise lint-cli: clean.
  • mise test-cli: every package passes.
  • mise license-check: clean.
  • docs-website check: clean.
  • python3 test_discord_export.py: 16 tests pass.
  • The live import above.

Notes for review

  • Protocol changes are additive. MessagePostedEvent.import_source_id = 13
    marks facts written by an import (ADR-117). messages_skipped_empty = 10 is a
    new response counter.
  • The bundle path for an attachment and for an embed image is the URL path as the
    URL spells it. The export writes the bytes there and the import reads from
    there; the name Chatto shows is decoded.
  • 42a88f501 also removes a __pycache__ file that an earlier commit added;
    133116dbc ignores Python bytecode caches.
  • FDR-052 records the open questions this run exposed: sticker and poll content;
    an embed's layout, colour, and buttons; attachment formats the server image
    processing rejects; role grants and slow mode from the plan; and whether a
    resumed run should add reactions for messages it already imported.

Records

`chatto operator import discord` now brings a whole Discord server into Chatto: messages, threads, attachments, embeds, reactions, mentions, room groups, and room placement. A companion export bot produces the bundle from a live server. The branch merges `main` (v0.5.0-beta.19 and later). Its records use the current numbering: ADR-117 and FDR-052, because `main` took FDR-051 for room invitations. The merge keeps both extensions of the historical message import path: this branch's import-source identity and mention facts, and `main`'s thread placement fields. ## What's in the branch **Bundle and CLI** - Parse and validate a DiscordChatExporter-shaped JSON bundle, with a `mapping.json` sidecar that maps Discord users to Chatto users and names the target. - `--all-channels` imports one export per channel; `--stream-attachments` downloads a file the folder lacks, on the operator's machine. The server never contacts Discord. - `--apply-structure` reads `structure/chatto-plan.json` and recreates the server layout: one room group per category, the plan's room name, a universal room where Discord let every member see the channel, and room members for a restricted channel. - An embed's words become message text and its images become attachments, so a bot post that carried only an embed still arrives. Both sides take a bundle path from the URL exactly as the URL spells it. - A dry run reports every skipped item with its reason, and every warning. - A long channel is sent in request-sized parts, so it no longer fails against the operator API request limit. - A message that carries neither a body nor a usable attachment is skipped and counted, instead of aborting the channel. A body longer than Chatto allows is cut and reported. **Server and operator API** - `ImportBundle` in core writes messages with their original time through the historical-import path, marks the facts `historical_import`, re-hosts attachments, rewrites mentions, and adds one reaction per mapped reactor. - Additive operator API: `ListRoomGroups`, `CreateRoomGroup`, `CreateRoomRequest.universal`, `ImportCreateChannel.{group_id,universal}`, and `ImportDiscordBundleResponse.messages_skipped_empty`. - An imported reaction that names a Unicode emoji is stored under its primary gemoji name, so it matches what the picker writes. - A created non-universal room also grants membership to every mapped author of the channel, so an imported history never lands in a room nobody can open. **Export bot** (`tools/discord-export-bot/`) - Stdlib-only Python. Writes the bundle plus the structure plan: categories, channels, overwrites, roles, and member-to-role assignments. - Writes each embed's words and images, and writes a thread's opening post into the thread export, so an imported thread keeps its first message. - Fetches an embed image through Discord's media proxy when Discord provides one, so the export does not contact the third-party site the embed pointed at. - Resumable and parallel: pooled CDN downloads, attachment reuse, proactive rate-limit pacing, and visible API and waiting totals. - Tolerates a channel type that rejects archived-thread listing. ## End-to-end evidence A live Discord server (13 visible channels, 32 mapped users) exported by the bot and imported into a fresh Chatto instance: | Item | Result | | --- | --- | | Channel exports | 27, holding 9,063 messages | | Messages imported | 8,942, with original post times and authors | | Messages whose only content was an embed | 3,088, all of which now arrive | | Reactions | 585 added, 6 skipped (Discord custom emoji) | | Attachments re-hosted | 3,990 (706 poster files, 3,284 embed pictures); 7 skipped (6 HEIC, 1 oversized animated GIF) | | Embed images the source has removed | 7 reported, each message kept | | Messages skipped | 2 left empty by a rejected attachment, 3 carrying nothing at all | | Room groups | `text-channels` created; the plan's `voice-channels` group was not created, because no voice channel has messages | | Rooms | 26 new, plus the reused `general` | | Room members granted | 54 grants, in one pass | Read-back through the public ConnectRPC API, signed in as a mapped Discord author: - A bot post that carried only an embed reads back as its title, description, and `name: value` fields, with the embed's poster re-hosted as an attachment (`image/jpeg`, 1920x1080, per-user access ticket). - A link-share embed reads back as its link, title, and description, plus its thumbnail as an attachment. - A thread's first message is its opening post, with the Discord mention token rewritten to `@hermes_agent_ff`. - A non-member of a non-universal room gets `permission_denied`, and the same user reads a universal room. The 7 members the import granted can read the restricted room. - A Discord 🛑 reaction reads back as the `stop_sign` emoji name. - A dry run of the same bundle reports `Embeds rendered as text: 3907`. A second run of the same import is a no-op for messages and re-applies the membership grants. ## Verification - `mise lint-cli`: clean. - `mise test-cli`: every package passes. - `mise license-check`: clean. - `docs-website` `check`: clean. - `python3 test_discord_export.py`: 16 tests pass. - The live import above. ## Notes for review - Protocol changes are additive. `MessagePostedEvent.import_source_id` = 13 marks facts written by an import (ADR-117). `messages_skipped_empty` = 10 is a new response counter. - The bundle path for an attachment and for an embed image is the URL path as the URL spells it. The export writes the bytes there and the import reads from there; the name Chatto shows is decoded. - `42a88f501` also removes a `__pycache__` file that an earlier commit added; `133116dbc` ignores Python bytecode caches. - FDR-052 records the open questions this run exposed: sticker and poll content; an embed's layout, colour, and buttons; attachment formats the server image processing rejects; role grants and slow mode from the plan; and whether a resumed run should add reactions for messages it already imported. ## Records - [FDR-052: Discord Message Import](docs/fdr/FDR-052-discord-message-import.md) - ADR-117 (historical import path) - `docs/architecture/interfaces.md` — the new operator services
f3l1x added 482 commits 2026-10-24 22:54:02 +01:00
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Hendrik Mans <1061+hmans@users.noreply.github.com>
Co-authored-by: Hendrik Mans <1061+hmans@users.noreply.github.com>
Co-authored-by: Hendrik Mans <1061+hmans@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Adds the architectural design for a folder-based Discord message importer,
the backdated-posting decision (ADR-103), and the Discord Message Import
feature record (FDR-049), plus the Phase 1 implementation plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements ADR-103. Privileged callers (the Discord importer) can preserve
original message timestamps by overriding the single created_at value used
for the message, body, and thread events. End-user posting is unchanged
because the option is never wired to end-user message-post RPCs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the discordimport package: it loads a DiscordChatExporter bundle folder,
resolves attachments from the CDN-mirror layout, maps authors/reactors to
existing Chatto users, validates the whole bundle, and builds a format-neutral
resolved manifest. Surfaces it through 'operator import discord --dry-run'.
Server-side commit arrives in Phase 3. Implements part of FDR-049.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds OperatorImportService and core.ImportBundle: creates or reuses the target
room, joins mapped authors, and posts each message with its original time
(ADR-103) and a stamped import_source_id for idempotent re-runs (FDR-049).
Wires 'operator import discord' to the operator socket. Attachments and
reactions are resolved by the CLI but not yet committed; the command reports
them as pending (Phase 3b / Phase 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends core.ImportBundle to join reactors and add each resolved reaction
through the privileged reaction path (idempotent). Unsupported emoji shortcodes
are counted and skipped rather than failing the import. Wires reactions through
the operator RPC and CLI. Attachments remain pending (FDR-049).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends core.ImportBundle to re-host attachment files (read from the bundle
path, uploaded as room assets owned by the author) and to rewrite Discord
mention tokens to Chatto @login mentions, joining mentioned users so mentions
resolve. Unimportable attachments are skipped and counted. Completes the
FDR-049 import feature: text, timestamps, threads, idempotency, reactions,
attachments, and mentions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adapt the Discord import to the 0.5.x core after rebasing onto main:

- Move new files to the github.com/chattocorp/chatto/cli module paths.
- Renumber the records: ADR-103 becomes ADR-117 and FDR-049 becomes
  FDR-051, because main took the old numbers.
- Post imported messages through ImportHistoricalMessage instead of
  PostMessage with WithOccurredAt. The historical path marks each fact
  as a historical import, so imports never trigger notifications,
  unread state, badges, or realtime delivery (ADR-117).
- Add import_source_id (field 13) and mention facts to the historical
  import input so re-run dedup and mention provenance survive.
- Remove the WithOccurredAt/WithImportSourceID post options and their
  tests; the historical path owns backdating now.
- Regenerate protobufs and apply the pinned modernize fixes.
- Add --timeout (default 30m) for the server-side import, as designed.
- Detect Discord's official account (GDPR) export shape and tell the
  operator to use DiscordChatExporter instead of failing on a type
  mismatch; that format omits authors, reactions, and replies.
- Record in FDR-051 why the official export cannot drive an import.
- Update stale dry-run-only comments now that the command commits.
A re-run with a create-channel target failed on the room-name conflict
before reaching the per-message idempotency check, so a partial import
could not be resumed. resolveImportTarget now reuses an existing channel
with the requested name (created_room=false in the response) and keeps
the conflict error when the name belongs to a non-channel room.
The durable design content lives in FDR-051 and ADR-117; main does not
track agent plan documents.
DiscordChatExporter JSON exports can reference attachments by their
original CDN URL instead of a local file when the export was produced
without local media. Until now such a bundle failed to import unless the
operator pre-downloaded every file or passed --allow-missing-attachments.

Add --stream-attachments. At load time an attachment whose local file is
missing but whose export URL is absolute http(s) is kept without a path.
Just before import, the CLI downloads each distinct URL once into a
temporary directory on the operator's machine and fills in the path, so
core re-hosts the bytes through the normal ImportFile path. The remote
host sees the operator's own connection, not the Chatto server's.

Downloads are bounded (100 MiB per file, 60s per request), local files
always win, and a failed download drops just that attachment and records
it in the report, like a missing file under --allow-missing-attachments.
URLs with signed query strings are stripped from error text.
A DiscordChatExporter export set often holds one JSON per channel. Until
now the command required exactly one export file, so importing a whole
server meant one invocation per channel with a hand-built target each
time.

Add --all-channels. The loader now returns one ResolvedBundle per export
file. With no single target each channel imports into a new channel
named after its Discord channel; with --room-id or --create-channel (or
a mapping.json target) every channel merges into that one room. Two
exports that would create the same channel name are rejected so a folder
stays one-export-per-channel. The command imports each bundle over one
client under a single --timeout budget and prints a per-channel report
plus a total.
Record --stream-attachments and --all-channels in FDR-051: revise the
self-contained-bundle decision to allow CLI-side attachment streaming,
add the multi-channel decision, and resolve the open question about
streaming attachments.

Add an "Import Discord history" section to the public Operator CLI guide:
bundle preparation with DiscordChatExporter JSON mode, the mapping.json
sidecar, target selection including --all-channels, identity mapping,
attachment handling, and a caution that --stream-attachments reveals the
operator's IP address to the remote host.
A first run against a real server exposed four problems:

- Discord rejects archived-thread listing on news and stage channels with
  error 50024. The exporter treated that as fatal and stopped mid-export.
- A re-run downloaded every attachment again, so an interrupted export
  started the transfer over.
- Attachment downloads ran one at a time, which made a media-heavy server
  bound by CDN round trips rather than by the Discord API.
- A text channel and a voice channel that share a Discord name produced
  one plan name, so the import would have reused the wrong room.

The exporter now treats 50024 as "no archived threads", reuses an
attachment whose size matches, downloads through a small pool with
--workers, and waits for the rate-limit bucket Discord publishes instead
of discovering it with a 429. A long run reports message progress and its
request and waiting totals.

Add test_discord_export.py, which stubs the Discord REST API and covers
the bundle shape and each of these failures.
Chatto keys reactions by gemoji shortcode name and rejects a Unicode
character. An external export carries the character, so a live Discord
import skipped every reaction.

- Generate emojiUnicodeToName beside emojiNameToUnicode. It uses gemoji's
  primary name, so an import reaches the same name the picker uses.
- Resolve one imported reaction with resolveImportEmoji: keep a shortcode
  name, map a Unicode character to its primary name, and report a source
  custom emoji as skipped instead of writing it.
A live Discord server exposed four ways an import stopped or left data
unreachable:

- One message with neither a body nor an attachment aborted a whole channel.
  Discord exports one when its content was an embed, a sticker, or a poll.
  The assembler now skips it, and the server counts the same case as
  messages_skipped_empty, because an unreadable or oversized file can also
  empty a message's attachments.
- A long channel exceeded the operator API request limit and failed with
  resource_exhausted. The CLI now splits one channel into request-sized
  parts and reports the part count. A reply may point at an earlier part:
  the server resolves a reply against messages the room already holds.
- Discord declares a smaller size than its CDN serves for some images, so
  the size check reported valid attachments. Only a file shorter than the
  export declared is now reported, as a warning.
- A restricted Discord channel also grants access through a role, which a
  bundle cannot resolve to people, so a created non-universal room got no
  members and its history was unreadable. The import now grants membership
  to every mapped author of the channel beside the plan's explicit members.
  An operator-named target keeps only the plan's list.
Discord declares a smaller size than its content network serves for some
images. The export compared the stored file with the declared size, so a
re-run fetched those files again.

- Reuse a stored file that is at least as long as the declared size. Only a
  shorter file can mean an interrupted download.
- Write each download beside its target and move it into place, so a run
  that stops leaves no half-written file for a later run to reuse.
docs(fdr): record what a Discord import skips, splits, and grants
Some checks failed
ci / workflow-lint (pull_request) Has been cancelled
ci / desktop (macos) (pull_request) Has been cancelled
ci / desktop (linux) (pull_request) Has been cancelled
ci / desktop (windows) (pull_request) Has been cancelled
ci / desktop-macos-capture (pull_request) Has been cancelled
ci / codegen-proto-drift (pull_request) Has been cancelled
ci / check-workspace (pull_request) Has been cancelled
ci / test-workspace (packages) (pull_request) Has been cancelled
ci / test-workspace (frontend) (pull_request) Has been cancelled
ci / test-workspace (pull_request) Has been cancelled
ci / test-cli-shared (pull_request) Has been cancelled
ci / test-cli-chatto (pull_request) Has been cancelled
ci / lint-cli (pull_request) Has been cancelled
ci / test-oidc-conformance (pull_request) Has been cancelled
ci / test-cli (pull_request) Has been cancelled
ci / test-authling-e2e (pull_request) Has been cancelled
ci / test-e2e (1/) (pull_request) Has been cancelled
ci / test-e2e (2/) (pull_request) Has been cancelled
ci / test-e2e (3/) (pull_request) Has been cancelled
ci / test-e2e (4/) (pull_request) Has been cancelled
ci / test-e2e (5/) (pull_request) Has been cancelled
ci / test-e2e (6/) (pull_request) Has been cancelled
ci / test-e2e (7/) (pull_request) Has been cancelled
ci / test-e2e (8/) (pull_request) Has been cancelled
ci / test-e2e (pull_request) Has been cancelled
ci / test-e2e-media (pull_request) Has been cancelled
ci / test-e2e-performance (pull_request) Has been cancelled
ci / publish-main-image (pull_request) Has been cancelled
ci / check-docs (pull_request) Has been cancelled
build Authling image / image (pull_request) Has been cancelled
7f880fab57
- FDR-051 gains decisions for the empty-message skip, request-sized parts,
  and the reaction emoji name, and lists the content a live server showed
  as still open: embeds, stickers, polls, thread starters, and attachment
  formats the server rejects.
- The operator guide lists what an import leaves out, and says that a
  created non-universal room also gets the channel's authors as members.
A live server showed two content gaps: messages whose only content was an
embed, and threads that started without their first post.

- The export bot writes each embed with its title, description, URL, fields,
  image, and thumbnail, and stores each embed image under
  embeds/<host>/<path>, because an embed image can come from any site.
- The import appends the embed's words to the message text and re-hosts each
  image as an attachment. An embed with no words contributes its link. A
  missing embed image costs only the image: its host can remove the file, so
  the import reports the skip instead of failing the channel.
- The export bot replaces a thread's ThreadStarterMessage entry with the
  opening post itself, so an imported thread keeps its first message.
- A Discord embed holds more text than Chatto allows. The import keeps the
  body inside the server limit: the poster's own words lead, the embed words
  are cut on a character boundary with a mark, and the run reports the cut.
An embed image can point at any site. Fetching it there would reveal the
operator's address to a server the operator never chose to contact, and a
link the source has since removed yields nothing.

- The export bot downloads the proxy URL when Discord provides one, and
  stores the bytes at the path derived from the original URL.
- The CLI streams an embed image from the proxy URL for the same reason.
- The bundle keeps the original URL, so the source stays traceable.
- The export names the kind of file it could not fetch, so a lost embed
  image is not read as a lost attachment.
fix(tools): keep one export worker's staging file from another's
Some checks failed
ci / workflow-lint (pull_request) Has been cancelled
ci / desktop (macos) (pull_request) Has been cancelled
ci / desktop (linux) (pull_request) Has been cancelled
ci / desktop (windows) (pull_request) Has been cancelled
ci / desktop-macos-capture (pull_request) Has been cancelled
ci / codegen-proto-drift (pull_request) Has been cancelled
ci / check-workspace (pull_request) Has been cancelled
ci / test-workspace (packages) (pull_request) Has been cancelled
ci / test-workspace (frontend) (pull_request) Has been cancelled
ci / test-workspace (pull_request) Has been cancelled
ci / test-cli-shared (pull_request) Has been cancelled
ci / test-cli-chatto (pull_request) Has been cancelled
ci / lint-cli (pull_request) Has been cancelled
ci / test-oidc-conformance (pull_request) Has been cancelled
ci / test-cli (pull_request) Has been cancelled
ci / test-authling-e2e (pull_request) Has been cancelled
ci / test-e2e (1/) (pull_request) Has been cancelled
ci / test-e2e (2/) (pull_request) Has been cancelled
ci / test-e2e (3/) (pull_request) Has been cancelled
ci / test-e2e (4/) (pull_request) Has been cancelled
ci / test-e2e (5/) (pull_request) Has been cancelled
ci / test-e2e (6/) (pull_request) Has been cancelled
ci / test-e2e (7/) (pull_request) Has been cancelled
ci / test-e2e (8/) (pull_request) Has been cancelled
ci / test-e2e (pull_request) Has been cancelled
ci / test-e2e-media (pull_request) Has been cancelled
ci / test-e2e-performance (pull_request) Has been cancelled
ci / publish-main-image (pull_request) Has been cancelled
ci / check-docs (pull_request) Has been cancelled
build Authling image / image (pull_request) Has been cancelled
3fe7a2c764
The media pool downloads with several threads, and one bot can post the same
embed image many times, so two workers can name one target. Both staged the
bytes at <target>.part, and one move took the other's file away: a live export
stopped with FileNotFoundError.

- Stage each download under a name that carries the thread id.
- Report a file the export could not write instead of ending the run. The
  import already reports what a bundle is missing.
- FDR-051 adds the decision that cuts a body longer than Chatto allows, and
  the bundle layout the export bot writes.
- The operator guide lists an embed image the export could not fetch among the
  things an import leaves out, because the report names it.
A bot can post one picture many times, and Chatto stores an attachment per
message. FDR-051 now says so, and the export bot README covers embed images in
its media options and its limits.
The export stores a bundle file under the URL path exactly as the URL spells
it. The import decoded that path, so a picture whose URL writes a space as
%20 was looked for under another name: a live import reported two embed
images as missing although the bundle held them.

- Both bundle path rules now use the URL's own spelling.
- The export names the file for Chatto with the decoded name, so the
  attachment reads naturally in the client.
docs(fdr): state that both sides read a bundle path as the URL spells it
Some checks failed
ci / workflow-lint (pull_request) Has been cancelled
ci / desktop (macos) (pull_request) Has been cancelled
ci / desktop (linux) (pull_request) Has been cancelled
ci / desktop (windows) (pull_request) Has been cancelled
ci / desktop-macos-capture (pull_request) Has been cancelled
ci / codegen-proto-drift (pull_request) Has been cancelled
ci / check-workspace (pull_request) Has been cancelled
ci / test-workspace (packages) (pull_request) Has been cancelled
ci / test-workspace (frontend) (pull_request) Has been cancelled
ci / test-workspace (pull_request) Has been cancelled
ci / test-cli-shared (pull_request) Has been cancelled
ci / test-cli-chatto (pull_request) Has been cancelled
ci / lint-cli (pull_request) Has been cancelled
ci / test-oidc-conformance (pull_request) Has been cancelled
ci / test-cli (pull_request) Has been cancelled
ci / test-authling-e2e (pull_request) Has been cancelled
ci / test-e2e (1/) (pull_request) Has been cancelled
ci / test-e2e (2/) (pull_request) Has been cancelled
ci / test-e2e (3/) (pull_request) Has been cancelled
ci / test-e2e (4/) (pull_request) Has been cancelled
ci / test-e2e (5/) (pull_request) Has been cancelled
ci / test-e2e (6/) (pull_request) Has been cancelled
ci / test-e2e (7/) (pull_request) Has been cancelled
ci / test-e2e (8/) (pull_request) Has been cancelled
ci / test-e2e (pull_request) Has been cancelled
ci / test-e2e-media (pull_request) Has been cancelled
ci / test-e2e-performance (pull_request) Has been cancelled
ci / publish-main-image (pull_request) Has been cancelled
ci / check-docs (pull_request) Has been cancelled
build Authling image / image (pull_request) Has been cancelled
79f5a596b6
The export and the import must compute one path. FDR-051 now records that
rule and the decoded name the export gives the file for Chatto.
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
Merge origin/main into feat/discord-import
Some checks failed
ci / test-botto (pull_request) Has been cancelled
ci / test-chattobot (pull_request) Has been cancelled
ci / desktop (macos) (pull_request) Has been cancelled
ci / desktop (linux) (pull_request) Has been cancelled
ci / desktop (windows) (pull_request) Has been cancelled
ci / desktop-macos-capture (pull_request) Has been cancelled
ci / codegen-proto-drift (pull_request) Has been cancelled
ci / check-workspace (pull_request) Has been cancelled
ci / test-workspace (packages) (pull_request) Has been cancelled
ci / test-workspace (frontend) (pull_request) Has been cancelled
ci / test-workspace (pull_request) Has been cancelled
ci / test-cli-shared (pull_request) Has been cancelled
ci / test-cli-chatto (pull_request) Has been cancelled
ci / lint-cli (pull_request) Has been cancelled
ci / test-oidc-conformance (pull_request) Has been cancelled
ci / test-cli (pull_request) Has been cancelled
ci / test-authling-e2e (pull_request) Has been cancelled
ci / test-e2e (1/) (pull_request) Has been cancelled
ci / test-e2e (2/) (pull_request) Has been cancelled
ci / test-e2e (pull_request) Has been cancelled
ci / test-e2e (3/) (pull_request) Has been cancelled
ci / test-e2e (4/) (pull_request) Has been cancelled
ci / test-e2e (5/) (pull_request) Has been cancelled
ci / test-e2e (6/) (pull_request) Has been cancelled
ci / test-e2e (7/) (pull_request) Has been cancelled
ci / test-e2e (8/) (pull_request) Has been cancelled
ci / test-e2e-media (pull_request) Has been cancelled
ci / test-e2e-performance (pull_request) Has been cancelled
ci / publish-main-image (pull_request) Has been cancelled
ci / check-docs (pull_request) Has been cancelled
f1dccb4532
Bring in the Slack import, room invitations, and the current main fixes.

This branch's Discord import record moves to FDR-052, because main took
FDR-051 for room invitations. The historical message import path keeps both
sides: this branch's import-source identity and mention facts, and main's
thread placement fields.
Some checks failed
build Authling image / image (pull_request) Has been cancelled
build media E2E image / image (pull_request) Has been cancelled
ci / changes (pull_request) Has been cancelled
ci / build-main-image-contexts (pull_request) Has been cancelled
ci / license-check (pull_request) Has been cancelled
ci / workflow-lint (pull_request) Has been cancelled
ci / test-translatto-package (pull_request) Has been cancelled
ci / test-runling (pull_request) Has been cancelled
ci / test-botto (pull_request) Has been cancelled
ci / test-chattobot (pull_request) Has been cancelled
ci / desktop (macos) (pull_request) Has been cancelled
ci / desktop (linux) (pull_request) Has been cancelled
ci / desktop (windows) (pull_request) Has been cancelled
ci / desktop-macos-capture (pull_request) Has been cancelled
ci / codegen-proto-drift (pull_request) Has been cancelled
ci / check-workspace (pull_request) Has been cancelled
ci / test-workspace (packages) (pull_request) Has been cancelled
ci / test-workspace (frontend) (pull_request) Has been cancelled
ci / test-workspace (pull_request) Has been cancelled
ci / test-cli-shared (pull_request) Has been cancelled
ci / test-cli-chatto (pull_request) Has been cancelled
ci / lint-cli (pull_request) Has been cancelled
ci / test-oidc-conformance (pull_request) Has been cancelled
ci / test-cli (pull_request) Has been cancelled
ci / test-authling-e2e (pull_request) Has been cancelled
ci / test-e2e (1/) (pull_request) Has been cancelled
ci / test-e2e (2/) (pull_request) Has been cancelled
ci / test-e2e (pull_request) Has been cancelled
ci / test-e2e (3/) (pull_request) Has been cancelled
ci / test-e2e (4/) (pull_request) Has been cancelled
ci / test-e2e (5/) (pull_request) Has been cancelled
ci / test-e2e (6/) (pull_request) Has been cancelled
ci / test-e2e (7/) (pull_request) Has been cancelled
ci / test-e2e (8/) (pull_request) Has been cancelled
ci / test-e2e-media (pull_request) Has been cancelled
ci / test-e2e-performance (pull_request) Has been cancelled
ci / publish-main-image (pull_request) Has been cancelled
ci / check-docs (pull_request) Has been cancelled
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/discord-import:feat/discord-import
git switch feat/discord-import

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff feat/discord-import
git switch feat/discord-import
git rebase main
git switch main
git merge --ff-only feat/discord-import
git switch feat/discord-import
git rebase main
git switch main
git merge --no-ff feat/discord-import
git switch main
git merge --squash feat/discord-import
git switch main
git merge --ff-only feat/discord-import
git switch main
git merge feat/discord-import
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
f3l1x/chatto!1
No description provided.