# moho architecture How the two halves fit together, what the daemon speaks, and where it puts things. The [README](README.md) is the short version; this is everything else. ## Why two parts - **`nobilis`** (`nobilis/`, Rust) speaks each protocol directly and translates it into a small unified JSON model (accounts, buffers, messages) over a Unix socket. What every service has in common - sending, editing, reacting, backlog, search - is one set of calls and events. What only one service has - a Matrix room's power levels, a Discord poll, a Kick prediction - is a call of its own, so a client never has to pretend the services are the same where they are not. - **The Electron client** (`src/`) spawns and supervises `nobilis` and renders whatever it reports. Keeping the protocol work in a separate long-lived daemon means connections, Tor circuits, and Matrix sync survive the UI being closed, restarted, or reloaded during development. ``` nobilis/src/backend/ one module per protocol (irc, discord, sneedchat, matrix, kick) nobilis/src/rpc/ the Unix-socket JSON-RPC server nobilis/src/store.rs SQLite-backed scrollback persistence nobilis/src/runtime.rs protocol-agnostic connection/buffer/message state src/main/ Electron main: daemon supervision, RPC socket, tray, notifications src/preload/ the contextBridge surface exposed to the renderer src/renderer/ React UI: buffer list, message log, member list, accounts, settings resources/icons/ bundled artwork (service marks, IRC network logos, tray icons) ``` ### One daemon, many clients Every packaged form bundles `nobilis`, the Sneedchat smilies and the icons under `resources/`, and the app resolves them from there rather than from the source tree - so the app and the daemon in one download always match. nobilis holds an `flock`-based singleton lock on its data directory, so launching a second client does not start a second daemon — it attaches to the running one. Quitting the app sends the daemon a clean shutdown (real QUITs to IRC, rather than dropping the connections), so your nick isn't left ghosted. ## The client Three panes: an account-grouped buffer sidebar, the message log, and a member list, with the sidebar and member list independently foldable. Closing the window hides it to the tray; nobilis keeps running, so connections and unread tracking survive. - **Message rendering** - an IRC-style log by default ("Classic"), with nick colouring, BBCode and markdown, spoilers, code and quote blocks, inline image/video embeds, custom emoji, emotes and stickers (Discord's animated Lottie ones included), reactions, replies, inline edit, and Sneedchat's bundled smilies. "Comfy" groups consecutive messages under one avatar, and "Bubbles" draws them as chat bubbles. A link is described by the daemon for every service, not only the ones that do it themselves: a YouTube link becomes a titled card, a Tenor or Giphy link is drawn as the GIF, and a GIF picker sends one from the composer. What somebody did on a Kick stream - KICKs sent, subscriptions gifted and to whom, a raid, a redeemed reward - is drawn as a card. - **Settings** - a searchable rail of categories: Accounts and Downloads; General, Appearance, Notifications and Keybinds; a page for each network (IRC, Sneedchat, Tor, Discord, Matrix, Kick); and About. Message-kind filters (joins, parts, mode changes) are applied client-side, so toggling one takes effect immediately without a reconnect. - **Moving around** - Ctrl+K opens a quick switcher over every conversation on every account. A conversation opens at its first unread message when the unread run is short, and at its end, with a bar to go back by, when it is long - as Discord does it. Escape marks the conversation read and Shift+Escape the whole server; a muted conversation stays quiet but still counts the times your name was used in it. - **Presence and the tray** - the status menu offers only the statuses each service has (Discord all four, IRC away and back, Matrix online and offline) and sends nothing to a service that has no such thing. Do not disturb silences desktop notifications everywhere and is Discord's real do-not-disturb on Discord. When nothing has been used for a while an account is put on Idle and back, from the system's idle time where it will say and moho's own windows where it will not. The tray draws a number counted by the window by the rule the server rail uses. - **Per-protocol join pages** - each service's mechanism is genuinely different (IRC joins by channel name, Discord offers the friends list and sends joining, adding a friend and making a server to discord.com - see below - Matrix takes a room address or searches every homeserver's directory at once, Sneedchat lists the rooms the site itself publishes, Kick takes a streamer's handle), so each gets its own page. - **Matrix security** - device verification by emoji or QR, cross-signing, server-side key backup, key export and import, and signing other sessions out, all under the account's own row in the Accounts pane. A session verified here is verified in Element too. - **Service interruptions** - a conversation that is not receiving is drawn in italics, with a spinner while something is trying, and says why in a banner when opened: its account is reconnecting, an IRC channel refused the join, a Kick channel is still being looked up. A Sneedchat room that cannot connect is diagnosed - Tor, the forum, or only the chat - by checking the route, then the forum's front page, then concluding it is the chat. - **Calls and streams** - a call stage like Discord's, which can sit in the corner or pop out into a window of its own, with tiles per person, volume per person and for the whole call on their right-click menu, and the camera and screen buttons in one row. A Kick stream is drawn in the same surface, resizable in the corner or popped out. - **Tagging** - typing "@" lists the people here, the roles the service lets anybody ping, and its whole-room words, matched fuzzily. What is sent is what the service understands: an id on Discord, `m.mentions` on Matrix, "nick: " on IRC. - **Profiles** - right-click somebody in the member list or on a message. Each service answers what it keeps: an idle time and shared channels on IRC, an account age and a join date on Discord, a power level and a last-seen on Matrix. ### Security notes Message bodies are fully attacker-controlled. The renderer runs with `contextIsolation` and no Node integration, and its preload surface is a fixed list of calls - there is no `ipcRenderer` passthrough and no filesystem access. Formatted message HTML is never injected directly: it is parsed into an inert document and rebuilt through a tag and attribute whitelist, so unknown tags degrade to text and `javascript:`/`data:` URLs, inline event handlers and `