XMPP Chat Features

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Chatterbox v1 defines twelve envelope types (see Message Design and Serialization). Every one of them already has a standard XMPP counterpart, usually a small XML element added to a message or an iq request. This page walks through those extensions one at a time. Each section gives the specification and its status, the stanzas on the wire, the @xmpp/client code that produces and consumes them, and the matching Chatterbox feature. The complete client that wires them together is on XMPP in the Browser; the servers and the Docker Compose file are on XMPP Servers; the protocol background is on XMPP Fundamentals.

Statuses were read from the XEP list on 2026-10-24, and the stanzas were exchanged with ejabberd 26.09 using @xmpp/client 0.14.0 (Node.js 24). The statuses matter because several extensions that chat clients rely on are still Experimental, and their wire format can change between versions; where a namespace carries a version suffix (urn:xmpp:message-retract:1), use the one your server advertises in service discovery.

Feature Status XEP Chatterbox v1 equivalent

Multi-user chat

Draft

XEP-0045 (1.35.5)

join, leave, rooms

Message archive

Draft

XEP-0313 (1.1.3)

history

Message carbons

Draft

XEP-0280 (1.0.1)

multi-device sync

Chat states

Final

XEP-0085 (2.1)

typing

Delivery receipts

Draft

XEP-0184 (1.4.0)

receipt (delivered)

Displayed markers

Draft

XEP-0333 (1.0.0)

receipt (read)

Last message correction

Draft

XEP-0308 (1.2.1)

edit

Message retraction

Proposed

XEP-0424 (0.5.0)

delete (not in v1)

Message moderation

Experimental

XEP-0425 (0.3.0)

moderator delete

Message reactions

Experimental

XEP-0444 (0.2.1)

react

Message replies

Experimental

XEP-0461 (0.2.1)

replyTo

HTTP File Upload

Draft

XEP-0363 (1.2.0)

attachments

User avatar

Draft

XEP-0084 (1.1.5)

profile picture

Blocking command

Draft

XEP-0191 (1.3)

block a user

Push notifications

Deferred

XEP-0357 (0.4.1)

offline push

Multi-user chat (XEP-0045)

A multi-user chat (MUC) room is addressed as room@conference.chat.example.test, and each participant has an occupant JID, room@conference.chat.example.test/nickname. The room is a service of the server, a component, so it is discovered with disco#items on the server (it appeared as conference.chat.example.test).

Joining is a directed presence to the occupant JID with the MUC namespace. The optional history element controls how much recent history the room replays on join; maxstanzas="0" asks for none, which is what a client does when it reads history through the archive instead:

<presence to="general@conference.chat.example.test/alice">
  <x xmlns="http://jabber.org/protocol/muc">
    <history maxstanzas="0"/>
  </x>
</presence>

The room answers with the presence of every occupant, then its own presence for the new occupant with status code 110 ("this is you"). The first join of a room that does not exist creates it. Depending on the server it stays locked until the owner submits the configuration form, and an empty submitted form accepts the defaults:

await xmpp.send(xml("presence", { to: `${room}/alice` }, xml("x", { xmlns: "http://jabber.org/protocol/muc" })));
await xmpp.iqCaller.request(xml("iq", { type: "set", to: room },
  xml("query", { xmlns: "http://jabber.org/protocol/muc#owner" }, xml("x", { xmlns: "jabber:x:data", type: "submit" }))));

Sending a room message is a message of type="groupchat" addressed to the room (the bare room JID), not to an occupant; the room reflects it to every occupant, including the sender. Leaving is a directed presence with type="unavailable".

Roles and affiliations

MUC separates what a person is in the room permanently from what they may do in this session:

Affiliation (persistent) Meaning

owner

Configures and destroys the room, assigns other owners and admins.

admin

Manages members and moderators; cannot change owners.

member

Allowed into a members-only room.

none

An ordinary visitor of an open room.

outcast

Banned.

Role (per session) Meaning

moderator

Can kick, grant and revoke voice, and moderate messages.

participant

Can send messages (has "voice") in a moderated room.

visitor

Can read but not speak in a moderated room.

none

Not in the room.

Affiliations are changed with an iq set to the muc#admin namespace, naming a JID; roles with an item naming a nickname:

<iq type="set" to="staff@conference.chat.example.test" id="a1">
  <query xmlns="http://jabber.org/protocol/muc#admin">
    <item affiliation="admin" jid="bob@chat.example.test"/>
  </query>
</iq>

<iq type="set" to="staff@conference.chat.example.test" id="r1">
  <query xmlns="http://jabber.org/protocol/muc#admin">
    <item nick="bob" role="visitor"><reason>cool down</reason></item>
  </query>
</iq>

Both requests returned an iq result on ejabberd, and the room broadcast a presence update for the occupant with the new affiliation or role in the muc#user extension. A role of none kicks the occupant, and an affiliation of outcast bans the JID. Rooms are also semi-anonymous by default: occupants see nicknames, and only moderators see real JIDs. Rooms advertise the extra features the server provides in their disco#info; the test room listed http://jabber.org/protocol/muc#stable_id, urn:xmpp:occupant-id:0 (a stable anonymous identifier for each occupant), urn:xmpp:mam:2, urn:xmpp:message-retract:1 and urn:xmpp:message-moderate:1.

Source: XEP-0045: Multi-User Chat. In Chatterbox terms a room is the same room name, join is the join presence, the member list is the occupant presence list, and the moderation roles map to the moderation features of History, Offline Delivery and Moderation.

MUC rather than MIX

Mediated Information eXchange (MIX, XEP-0369) was designed as a successor to MUC, built on publish-subscribe and MAM, with channels that a user’s server joins on the user’s behalf. It has stayed at Experimental status (version 0.14.6, last updated in December 2020), it is not part of the XMPP Compliance Suites 2023 (XEP-0479), and few clients and servers implement it. MUC (XEP-0045) therefore remains the room protocol to build on, and every Chatterbox room on this page is a MUC room.

Message archive management (XEP-0313)

MAM stores messages on the server and lets a client query them. It replaces the room history replay and the offline queue with one mechanism: a query is an iq set whose result is a sequence of message stanzas wrapping the archived stanza in forwarded, followed by a final iq with a fin element. The page of results is selected with Result Set Management (RSM, XEP-0059): max limits the page, before (empty for the newest page) or after set the cursor, and the fin reports first, last and count.

<iq type="set" to="general@conference.chat.example.test" id="m1">
  <query xmlns="urn:xmpp:mam:2" queryid="f27">
    <set xmlns="http://jabber.org/protocol/rsm"><max>50</max><before/></set>
  </query>
</iq>

<!-- one result, repeated for each archived message -->
<message to="alice@chat.example.test/web">
  <result xmlns="urn:xmpp:mam:2" queryid="f27" id="1792877206962405">
    <forwarded xmlns="urn:xmpp:forward:0">
      <delay xmlns="urn:xmpp:delay" stamp="2026-10-24T21:26:46.962405Z"/>
      <message type="groupchat" from="general@conference.chat.example.test/alice" id="01JAB3W7Q8Z4N5R6T7V8X9Y0ZA">
        <body>Hello, Chatterbox</body>
      </message>
    </forwarded>
  </result>
</message>

<iq type="result" id="m1">
  <fin xmlns="urn:xmpp:mam:2" complete="false">
    <set xmlns="http://jabber.org/protocol/rsm"><first>1792877206962405</first><last>1792877219221967</last><count>4</count></set>
  </fin>
</iq>

To go further back, repeat the query with <before> set to the first id of the previous page, until complete="true". To catch up after a disconnect, ask for messages after the last archive id you stored. A one-to-one archive is queried by sending the iq without a to (the user’s own archive), and filtered with a data form:

const field = (name, value) => xml("field", { var: name }, xml("value", {}, value));
const result = await xmpp.iqCaller.request(xml("iq", { type: "set" },
  xml("query", { xmlns: "urn:xmpp:mam:2", queryid: "q1" },
    xml("x", { xmlns: "jabber:x:data", type: "submit" },
      xml("field", { var: "FORM_TYPE", type: "hidden" }, xml("value", {}, "urn:xmpp:mam:2")),
      field("with", "bob@chat.example.test"), field("start", "2026-01-01T00:00:00Z")),
    xml("set", { xmlns: "http://jabber.org/protocol/rsm" }, xml("max", {}, "50")))), 15000);
const fin = result.getChild("fin", "urn:xmpp:mam:2");
console.log("complete:", fin.attrs.complete, "next cursor:", fin.getChild("set").getChildText("first"));

Source: XEP-0313: Message Archive Management and XEP-0059: Result Set Management. The ejabberd run returned a page of two with count 4 and complete="false". Details to remember:

  • The archive id (the id of result) is assigned by the archive, is monotonic, and is also stamped on live messages as a stanza-id (XEP-0359, urn:xmpp:sid:0). It is the XMPP counterpart of the Chatterbox seq: store the last one per room and use it as the after cursor to catch up.

  • Archives hold everything the client sent: edits, reactions and retractions appear as extra messages (a reaction has no body). The client folds them into the messages they target.

  • Archiving must be enabled (default: always in the ejabberd configuration) and retention limited, because the archive grows without bound.

Message carbons (XEP-0280)

When a user is online with several clients (the browser, a phone), a message sent to the bare JID goes to the resource the server picks, and a message sent from one client is invisible to the others. Carbons copy both directions to the other resources. A client opts in with one request after binding:

await xmpp.iqCaller.request(xml("iq", { type: "set" }, xml("enable", { xmlns: "urn:xmpp:carbons:2" })));

ejabberd answered with an iq result. Carried copies arrive wrapped in sent or received elements containing a forwarded message; the client must unwrap them and must ignore a carbon whose from is not its own bare JID (spoofing). Carbons are for one-to-one chats; rooms already deliver to every joined resource. Source: XEP-0280: Message Carbons. Bind 2 (see the fundamentals page) can enable them inline in the login. This is Chatterbox’s multi-device sync.

Chat states (XEP-0085)

Chat states are the typing indicators. They are empty elements in the http://jabber.org/protocol/chatstates namespace added to a message (active, composing, paused, inactive, gone), usually in a body-less message:

<message to="general@conference.chat.example.test" type="groupchat">
  <composing xmlns="http://jabber.org/protocol/chatstates"/>
</message>

Send composing when the user starts typing, paused after a few seconds of inactivity, and nothing more for a long while; throttle sends (Chatterbox uses at most one per three seconds) and expire a composing on the receiving side after a timeout, because the sender may have lost its connection before sending paused. Chat states are not archived by default. Source: XEP-0085: Chat State Notifications. Maps to typing (start is composing, stop is paused).

Delivery receipts (XEP-0184)

A sender asks for a receipt by adding request; the receiving client answers with a received message naming the id:

<message to="bob@chat.example.test" type="chat" id="a1">
  <body>psst</body>
  <request xmlns="urn:xmpp:receipts"/>
</message>

<message to="alice@chat.example.test/web" type="chat">
  <received xmlns="urn:xmpp:receipts" id="a1"/>
</message>

In the test, Bob’s client answered received as soon as it handled the message and Alice’s client matched it to the id. A receipt means the other client got it, which is stronger than the server accepting the stanza (a Stream Management ack). Receipts are not sent for rooms. Source: XEP-0184: Message Delivery Receipts. Maps to receipt with kind delivered.

Displayed markers (XEP-0333)

Markers are the read-state companion, and the only way to implement "read" in a room. The sender marks a message markable; a client reports progress with received, displayed or acknowledged markers:

<message to="general@conference.chat.example.test" type="groupchat" id="01JAB3W7Q8Z4N5R6T7V8X9Y0ZA">
  <body>Hello, Chatterbox</body>
  <markable xmlns="urn:xmpp:chat-markers:0"/>
</message>

<message to="general@conference.chat.example.test" type="groupchat">
  <displayed xmlns="urn:xmpp:chat-markers:0" id="01JAB3W7Q8Z4N5R6T7V8X9Y0ZA"/>
</message>

A displayed marker is cumulative: it means "everything up to this message", which is how Chatterbox’s receipt with upTo works. In a room the marker is reflected to every occupant, so a receipt costs one stanza per reader and per message; throttle it and send it for the newest visible message only. Source: XEP-0333: Displayed Markers. Maps to receipt with kind read.

Last message correction (XEP-0308)

Editing sends a new message that carries the old id in a replace element; receivers swap the text of the original and show an "edited" mark:

<message to="general@conference.chat.example.test" type="groupchat" id="b2">
  <body>Hello, Chatterbox!</body>
  <replace xmlns="urn:xmpp:message-correct:0" id="01JAB3W7Q8Z4N5R6T7V8X9Y0ZA"/>
</message>

The room delivered the correction to the other participant, whose client logged the replaced id, and the archive stored both messages. Rules: only the original sender may correct, and only the last message of a conversation, in the specification; many clients and servers relax that to any message of the author. Keep the original id as the message’s identity and treat the correction as a revision; the archive in the test returned the original and the corrected message as two entries. Source: XEP-0308: Last Message Correction. Maps to edit.

Message retraction and moderation (XEP-0424, XEP-0425)

Retraction lets an author delete a message for everyone, and moderation lets a room moderator do it for someone else with a reason. Both are still young (XEP-0424 Proposed, XEP-0425 Experimental), and their namespaces were bumped when the format changed, so check what the server advertises.

<!-- an author retracts their own message (urn:xmpp:message-retract:1) -->
<message to="staff@conference.chat.example.test" type="groupchat">
  <retract xmlns="urn:xmpp:message-retract:1" id="b9a1c2d3"/>
</message>

<!-- a moderator removes someone else's message; the id is the stanza-id of the target -->
<iq type="set" to="staff@conference.chat.example.test" id="mod1">
  <moderate xmlns="urn:xmpp:message-moderate:1" id="1792878250097176">
    <retract xmlns="urn:xmpp:message-retract:1"/>
    <reason>spam</reason>
  </moderate>
</iq>

ejabberd 26.09 accepted both: the author’s retraction reached the other occupant carrying the original id, and the moderator’s iq returned a result while every occupant received a retract element carrying the room-assigned id plus a moderated child naming the moderator. The server also sent a second copy in an older format (apply-to with urn:xmpp:fasten:0 and a message-moderate:0 child) for older clients, so a client that listens for the old and the new format may see two events for one removal; deduplicate on the target id. An author’s retraction names the id the sender chose; the moderator request names the stanza-id (the archive id) that the room stamped on the target. Remember that a retraction is a request: clients that already rendered the message hide it, and the server may rewrite or drop the stored copy, but nothing guarantees that a screenshot or a third-party client forgets it. Sources: XEP-0424: Message Retraction and XEP-0425: Moderated Message Retraction. This is the delete feature of the moderation page, absent from Chatterbox v1.

Message reactions (XEP-0444)

A reaction is a message with a reactions element naming the target and the complete current set of emoji of the sender; to remove a reaction, send the set without it:

<message to="general@conference.chat.example.test" type="groupchat" id="c3">
  <reactions xmlns="urn:xmpp:reactions:0" id="01JAB3W7Q8Z4N5R6T7V8X9Y0ZA">
    <reaction>👍</reaction>
    <reaction>🎉</reaction>
  </reactions>
  <store xmlns="urn:xmpp:hints"/>
</message>

store is a processing hint asking the server to archive the message even though it has no body. The set-replace semantics differ from the custom protocol: Chatterbox sends add and remove operations, while XMPP sends the whole set and the client computes the difference. Rooms deliver a reaction to every occupant, and a client applies it to the target by id (in the ejabberd test the reaction named the sender-chosen id of the original message and was delivered unchanged). Source: XEP-0444: Message Reactions. Maps to react.

Message replies (XEP-0461)

A reply references the message it answers with reply, optionally with a fallback that tells older clients which part of the body is quoted text to hide when the client can render the reply itself:

<message to="staff@conference.chat.example.test" type="groupchat" id="d4">
  <body>> bob: spam link
Thanks, removed</body>
  <reply xmlns="urn:xmpp:reply:0" to="staff@conference.chat.example.test/bob" id="1792878250097176"/>
  <fallback xmlns="urn:xmpp:fallback:0" for="urn:xmpp:reply:0"><body start="0" end="17"/></fallback>
</message>

The room delivered the message and the quoted text intact. The start and end offsets count characters of the body and mark the quoted prefix; XEP-0461 is Experimental, and Fallback Indication (XEP-0428) is the mechanism behind the fallback element. Maps to replyTo. Source: XEP-0461: Message Replies.

HTTP File Upload (XEP-0363)

Attachments are not sent through the XMPP stream. The client asks an upload component for a slot, receives a pair of URLs, uploads the file with an HTTP PUT to the first, and shares the second in the message:

<iq type="get" to="upload.chat.example.test" id="u1">
  <request xmlns="urn:xmpp:http:upload:0" filename="note.txt" size="12" content-type="text/plain"/>
</iq>

<iq type="result" id="u1">
  <slot xmlns="urn:xmpp:http:upload:0">
    <put url="http://localhost:5280/upload/f0a9.../note.txt"><header name="Authorization">placeholder</header></put>
    <get url="http://localhost:5280/upload/f0a9.../note.txt"/>
  </slot>
</iq>
const slot = (await xmpp.iqCaller.request(xml("iq", { type: "get", to: "upload.chat.example.test" },
  xml("request", { xmlns: "urn:xmpp:http:upload:0", filename: file.name, size: String(file.size), "content-type": file.type })))
).getChild("slot", "urn:xmpp:http:upload:0");
const put = slot.getChild("put");
const headers = { "Content-Type": file.type };
for (const h of put.getChildren("header")) headers[h.attrs.name] = h.text();   // copy only the allowed headers
await fetch(put.attrs.url, { method: "PUT", body: file, headers });

// Share the GET URL in the message (out-of-band data, XEP-0066), with the URL also in the body for older clients
const url = slot.getChild("get").attrs.url;
await xmpp.send(xml("message", { to: room, type: "groupchat", id: crypto.randomUUID() },
  xml("body", {}, url), xml("x", { xmlns: "jabber:x:oob" }, xml("url", {}, url))));

The slot request on ejabberd returned a PUT URL under /upload, PUT returned 201 and a GET of the second URL returned the stored content. The service enforces a size limit and a quota (Prosody’s defaults are 10 MiB per file and 100 MiB per day per user) and a retention time, and the specification says that only the headers Authorization, Cookie and Expires may be copied from the slot. Validate the size and type on the client before asking for the slot, and see the file handling in History, Offline Delivery and Moderation. Source: XEP-0363: HTTP File Upload. Maps to attachments.

Avatars (XEP-0084 over PEP, XEP-0163)

A user avatar is stored in the user’s own Personal Eventing Protocol (PEP) pub/sub nodes: the image in urn:xmpp:avatar:data and its description in urn:xmpp:avatar:metadata, with the SHA-1 of the image as the item id. Contacts subscribe by advertising interest in the node in their capabilities and receive a notification when it changes.

const sha = createHash("sha1").update(png).digest("hex");
await xmpp.iqCaller.request(xml("iq", { type: "set" },
  xml("pubsub", { xmlns: "http://jabber.org/protocol/pubsub" },
    xml("publish", { node: "urn:xmpp:avatar:data" },
      xml("item", { id: sha }, xml("data", { xmlns: "urn:xmpp:avatar:data" }, png.toString("base64")))))));
await xmpp.iqCaller.request(xml("iq", { type: "set" },
  xml("pubsub", { xmlns: "http://jabber.org/protocol/pubsub" },
    xml("publish", { node: "urn:xmpp:avatar:metadata" },
      xml("item", { id: sha }, xml("metadata", { xmlns: "urn:xmpp:avatar:metadata" },
        xml("info", { bytes: String(png.length), id: sha, type: "image/png", height: "64", width: "64" })))))));

Both publishes returned a result on ejabberd. Publish the data first, then the metadata, because the metadata announcement is what triggers other clients to fetch. Keep avatars small (a few kilobytes) because the notification fan-out is paid per contact. Source: XEP-0084: User Avatar and XEP-0163: Personal Eventing Protocol. The same PEP mechanism carries bookmarks and OMEMO device lists (see OMEMO Encryption).

Blocking (XEP-0191)

Blocking is a server-side list: blocked JIDs cannot send stanzas to the user. It is two requests plus a retrieval:

await xmpp.iqCaller.request(xml("iq", { type: "set" }, xml("block", { xmlns: "urn:xmpp:blocking" }, xml("item", { jid: "spam@chat.example.test" }))));
const list = await xmpp.iqCaller.request(xml("iq", { type: "get" }, xml("blocklist", { xmlns: "urn:xmpp:blocking" })));
await xmpp.iqCaller.request(xml("iq", { type: "set" }, xml("unblock", { xmlns: "urn:xmpp:blocking" })));   // no items: unblock all

ejabberd listed spam@chat.example.test in the blocklist and accepted the unblock. The user’s other clients are notified by an iq push, so the block takes effect everywhere. Blocking applies to one-to-one stanzas; blocking someone in a room is done with room bans (the outcast affiliation). Source: XEP-0191: Blocking Command.

Push notifications (XEP-0357)

A web page cannot keep a WebSocket open when the browser is closed, so a mobile or Web Push companion needs the server to wake the device. With XEP-0357 the client registers an app server (the push service) and a node, and the XMPP server sends it a small notification, without content, when a message arrives while the client is offline:

<iq type="set" id="p1">
  <enable xmlns="urn:xmpp:push:0" jid="push.example.test" node="device-token-placeholder"/>
</iq>

ejabberd answered the request with a result. The push service then delivers to APNs, FCM or, for a web page, a Web Push endpoint with VAPID (see History, Offline Delivery and Moderation); the XMPP side only needs the enable request and the server-side module (mod_push in ejabberd, cloud_notify in Prosody). The specification status is Deferred, but the mechanism is in wide use on mobile clients and is part of the Mobile Compliance Suite. Source: XEP-0357: Push Notifications.

Security

  • Transport. Use wss:// and an Origin allow-list in the proxy; ejabberd accepts the WebSocket upgrade from any origin. Authenticate with SCRAM or a token (see XMPP Fundamentals).

  • Rooms and spam. A public room invites flooding and unsolicited invitations. Use members-only or moderated rooms when the audience is known, restrict who can create rooms, limit message rates and nickname changes, and keep a ban list (the outcast affiliation). Moderators need a reason field and an audit trail for retractions.

  • Account enumeration. Open registration and answers to presence subscriptions or room joins can reveal which accounts exist. Rate-limit them and do not return different errors for a missing account and a refused one.

  • Real addresses in rooms. Rooms are semi-anonymous by default, but a non-anonymous room shows every occupant’s real JID to everyone. Say so in the user interface before joining, and use the occupant-id extension instead of nicknames to recognise an occupant.

  • Archive privacy and retention. MAM stores every message on the server. Decide whether the archive is on by default, how long it is kept and who may query a room archive, delete the archive when a user deletes the account, and tell users. A retraction is a request, so a copy may remain in backups and third-party clients.

  • Roster and presence. Presence, avatars and last-activity data go to every subscribed contact, and Carbons copy messages to all of a user’s clients. Show which devices are connected and let the user remove one.

  • File upload. The upload URL is the only protection of the file, so make it unguessable, keep the retention short, and enforce size, quota and type limits in the upload service and the proxy. Serve downloads with a safe content type and Content-Disposition, scan them if users share files widely, and copy only the three allowed slot headers.

  • Notifications. A push payload travels through APNs or FCM; send no message content in it.

  • End-to-end encryption. None of these features encrypts the body from the server. See OMEMO Encryption.

Version notes

  • Compliance. XEP-0479 (Compliance Suites 2023) is the newest published list of the extensions a current client and server are expected to support; it names MUC, MAM, Carbons, HTTP File Upload and the blocking command, and it is the baseline for the table at the top of this page.

  • Rooms. MIX (XEP-0369) is Experimental (0.14.6, 2020) and has no wide deployment, so MUC (XEP-0045) remains the room protocol on current servers and clients.

  • Encryption. OMEMO in XEP-0384 version 0.9.1 uses the namespace urn:xmpp:omemo:2 and is Experimental. It coexists with the legacy eu.siacs.conversations.axolotl namespace, so a client may need to support both to reach older contacts.

  • Push. XEP-0357 is Deferred, but it is widely deployed and part of the Mobile suite.

  • Young extensions. Retraction (XEP-0424, Proposed), moderation (XEP-0425), reactions (XEP-0444) and replies (XEP-0461) are not yet Draft, and the retraction namespace changed (apply-to and message-moderate:0 became message-retract:1 and message-moderate:1). ejabberd 26.09 sends both forms, so read the namespaces in disco#info and deduplicate.

  • Archive namespace. Use urn:xmpp:mam:2; Carbons use urn:xmpp:carbons:2.

Common errors and pitfalls

  • Sending a room message to the occupant JID. A groupchat message goes to the bare room JID. Sent to room/nick with type="groupchat" it is an error; to send a private message use type="chat".

  • Joining with a nickname that is taken. The room answers with a conflict error presence; choose another nickname.

  • Applying a correction from anyone. Verify that the correction’s sender is the sender of the original, and, in a room, the same occupant (use the occupant-id extension where the room provides it).

  • Forgetting that history contains corrections, reactions and retractions. Render by folding them into their targets.

  • Treating delivered as read. Receipts and markers are different stanzas; a client may implement one and not the other.

  • Reactions that overwrite each other. Every reaction message replaces the sender’s whole set for that target.

  • Receipts for every message of every room. A cumulative displayed for the newest visible message is enough.

  • Assuming the archive is complete. Check complete in fin, and remember the server enforces limits on the page size.

  • Copying every header from an upload slot. Only the three allowed ones.

  • Using a namespace version the server does not advertise. Read disco#info for the version before sending.